CH 21 · 훅 플러그인과 가로채기: 도구 실행 전 개입하기
본 장 목표
CH 20에서 모델이 본인이 만든 도구를 호출하도록 했습니다. 이 장에서는 한 걸음 더 나아가: 도구가 실행되기 전후에 본인의 로직을 걸어 — 누가 무엇을 호출했는지 기록하고, 호출해서는 안 되는 도구를 가로채고, 허용/거부를 결정합니다.
이것이 "훅 플러그인"이며, dsh의 권한 시스템, 샌드박스, 감사(audit) 능력의 기초이고, "모든 것이 플러그인"의 가장 전형적인 구현입니다.
도구 호출은 일직선이 아니다
CH 11에서 한 가지 개념을 언급했습니다: 모델이 도구를 호출하라고 말해도 직접 실행되는 것이 아니라, 확장 가능한 파이프라인을 거칩니다. 공식 팀은 이것을 "보초가 있는 파이프라인"으로 만들었습니다 — 모든 단계를 플러그인이 가로채고 강화할 수 있습니다.
도구 호출은 다음 단계를 거칩니다:
모델이 도구를 호출하려 함
↓
pre-execute → 정책 게이트: allow / deny / ask (권한, 샌드박스, 가로채기가 모두 여기)
↓
guard → 단조적 가드: 한 번 거부되면 이후 리스너가 되돌릴 수 없음 (최후의 방어선)
↓
execute → 실제로 도구 실행
↓
post-execute → 결과 변환: 반환값 재작성, 콘텐츠 추가
↓
result → 읽기 전용 관찰: 결과를 훑어보기만 하고 변경 불가
↓
결과가 모델로 반환됨"훅"은 특정 단계에 마운트된 플러그인입니다: ctx.on('tools/xxx', ...)로 해당 에피션을 구독하고, 이벤트 안에서 본인이 원하는 일을 합니다.
공식 문서에는 각 확장 지점이 무엇을 할 수 있는지 정리한 표가 명확합니다:
| 확장 지점 | 효과 | 전형적 용도 |
|---|---|---|
tools/pre-execute | 도구 실행 전 결정 계층 | allow / deny / ask, 권한 게이트 |
ctx.tools.guard() | 단조적 최종 거부 | 후속 리스너가 되돌릴 수 없는 하드 리밋 |
tools/execute | 전체 디스패치 사이클을 감쌈 | 타임아웃, 재시도, 메트릭 수집 추가 |
tools/post-execute | 결과를 명시적으로 변환 | 표시 콘텐츠 교체, 모델 가시 컨텍스트 추가 |
tools/result | 불변 결과의 읽기 전용 관찰 | 감사 로그, 통계, 변경 불가 |
pre-execute는 워터폴 이벤트입니다: 리스너는 next()(허용) 또는 { kind: 'deny', reason: '...' }(거부)를 반환할 수 있습니다.
실습: "감사 + 가로채기" 훅 플러그인 작성
기록(audit)과 거부(deny)를 동시에 수행하는 훅 플러그인을 작성합니다. 거부 목록은 설정 가능하도록 만들어, 동시에 "플러그인은 설정 가능하다"는 능력을 함께 연습합니다.
1단계: 디렉터리 생성, 의존성 설치
플러그인을 둘 워크스페이스에서:
New-Item -ItemType Directory -Path "hook-demo\src" -Force
cd "E:\software-workspace\DeepSeek harness demo\hook-demo" # 본인 디렉터리로 바꾸세요
npm init -y
npm install @deepseek-ai/schemasteryschemastery는 설정 스키마를 정의하기 위한 라이브러리입니다(플러그인을 설정 가능하게 만들 때, 설정의 형상과 기본값을 선언하는 데 사용). package.json에 "type": "module"을 추가하는 것을 잊지 마세요.
2단계: 훅 플러그인 작성
hook-demo\src\audit.js를 만듭니다:
import Schema from '@deepseek-ai/schemastery'
export const name = 'audit-hook'
export const Config = Schema.object({
denyTools: Schema.array(Schema.string()).default([]),
})
export function apply(ctx, config) {
ctx.on('tools/pre-execute', (exec, next) => {
console.log(`[audit] Tool will be called: ${exec.name}`)
if (config.denyTools.includes(exec.name)) {
return { kind: 'deny', reason: `Policy: this session is forbidden from calling ${exec.name}` }
}
return next()
})
}한 줄씩 분해합니다:
Config: 플러그인의 설정 가능 항목을 선언합니다.denyTools는 문자열 배열이며 기본값은 빈 배열입니다.apply(ctx, config)의config는 사용자 설정과 기본값을 머지한 결과입니다.- **
ctx.on('tools/pre-execute', ...): 도구 실행 전 이벤트를 구독합니다. 도구가 호출되기 직전마다 여기서 지나갑니다. console.log: 감사(audit) — 이 도구가 곧 호출될 것을 기록합니다.config.denyTools.includes(exec.name): 이 도구가 거부 목록에 있으면{ kind: 'deny', reason }을 반환해 가로챕니다.return next(): 그렇지 않으면 허용하여 파이프라인이 계속 진행되도록 합니다.
3단계: cordis.yml에서 설정 전달
hook-demo\cordis.yml을 만듭니다(경로는 본인 것으로 바꾸고, 공백은 %20으로 적어야 함을 기억):
- insert:
- id: audit
name: 'file:///E:/your-workspace/hook-demo/src/audit.js'
config:
denyTools: ['pwsh']여기서 config는 pwsh(PowerShell)를 거부 목록에 추가합니다. 플러그인 코드는 한 글자도 바꾸지 않았지만 동작이 달라졌습니다 — 이것이 바로 설정의 역할이며, "하드코딩된 조정 가능 파라미터는 없다"는 공식 설계 원칙입니다. cordis.yml에서 바꿀 수 있는 값을 코드에 박아두어서는 안 됩니다.
4단계: 실행하여 효과 확인
headless로 한 번 실행해 모델이 pwsh를 호출하도록 합니다:
cd your-workspace-directory
dsh --profile headless --patch "./hook-demo/cordis.yml" "Use pwsh to run Get-ChildItem to list the current directory"제 실제 terminal 출력:

두 줄의 노란색 [audit] 로그는 훅이 기록한 것입니다 — 모델이 먼저 skill을 호출했고, 이어서 pwsh를 호출했으며, 모든 도구 호출이 본인의 훅을 지나갔습니다. 그리고 pwsh는 거부 목록에 있으므로 deny 처리되었고, 모델이 거부를 인지해 "이 세션에서는 pwsh 호출이 금지됨"을 적극적으로 보고하면서 대안을 제시했습니다.
하나의 플러그인이 감사(보이는 것)와 가로채기(제어 가능한 것)를 동시에 수행합니다. 이것이 훅의 힘입니다.
dsh에게 맡기기: 한 줄 프롬프트로 끝내기
이 플러그인 작성은 dsh에게 맡기는 편이 빠릅니다. Web UI의 입력창에 다음을 그대로 보냅니다:
현재 워크스페이스에서 훅 플러그인을 작성해 줘: 도구 호출 전에 로그 한 줄을 출력하고, 설정을 통해 지정한 도구를 거부할 수 있어야 함. 공식 명세에 따라 구현하고, headless를 실행해 기록과 가로채기 모두 가능함을 검증한 후, 마지막에 결과를 알려 줘.dsh가 공식 문서를 직접 읽고, 플러그인을 작성하고, 기록과 가로채기 모두 동작하는지 직접 검증합니다. 그저 확인하시면 됩니다.

이는 실제로 이 프롬프트를 실행한 결과입니다: 먼저 본인이 구현 포인트를 스스로 정리했습니다 — 플러그인은 name / inject / apply의 명명된 export만 가능, inject: ['tools']로 도구 레지스트리 준비 보장, ctx.on('tools/pre-execute', ...)로 훅을 건다(공식 권한 게이트 예제와 일치), { kind: 'deny', reason }을 반환해 거부, await next()로 허용 — "거부 목록은 config를 통해, 코드 변경 없이"라는 부분까지 본인을 위해 생각해 두었습니다.

이 trajectory 다이어그램은 그 전체 작업 과정입니다: write로 플러그인 파일을 작성하고, pwsh로 자체 검증 스크립트를 실행하며(8개 검사 모두 통과), todo_write로 작업 목록을 갱신하는 등 모든 항목가 도구 호출입니다.
자주 겪는 함정
| 문제 | 현상 | 대응 |
|---|---|---|
return next()를 잊음 | next 없는 워터폴 이벤트, 파이프라인이 멈춤 | pre-execute / post-execute는 반드시 next() 또는 결정 객체를 반환 |
| 거부 후에도 모델이 계속 재시도 | 모델이 이 도구가 영구적으로 사용 불가함을 모름 | reason을 명확히 작성, 모델이 이를 보고 다른 방식으로 전환 |
| 설정이 적용되지 않음 | cordis.yml의 config가 잘못됨 | 필드명과 schema 타입이 일치하는지 확인 |
Cannot find package '@deepseek-ai/schemastery' 보고 | 플러그인 디렉터리에 의존성을 설치하지 않음 | npm install @deepseek-ai/schemastery |
이 장에서 배운 것
아래 항목들을 직접 해낼 수 있으면 합격입니다:
- [ ] 도구 호출 파이프라인의 주요 단계(pre-execute / execute / post-execute / result)를 말할 수 있다
- [ ]
ctx.on('tools/pre-execute', ...)로 훅 플러그인을 작성할 수 있다 - [ ]
{ kind: 'deny', reason }을 반환해 도구를 가로채고,next()로 허용한다 - [ ]
Config+ Schemastery로 플러그인을 설정 가능하게 만든다(거부 목록) - [ ] "하드코딩된 조정 가능 파라미터는 없다"는 설계 원칙을 말할 수 있다
