훅
훅 (Hooks)
에이전트가 셸 명령을 실행하기 전에 "이건 막아야겠다" 싶은 순간이 있어요. Deep Agents Code의 **훅(hook)**은 외부 프로그램이 에이전트의 수명주기 이벤트를 관찰하고 제어할 수 있게 해줘요. 이벤트가 발생하면 일치하는 핸들러를 찾아 JSON 페이로드를 stdin으로 보내고, 그 응답으로 허용·거부·컨텍스트 주입·턴 계속을 결정할 수 있어요. 여기서 훅을 어떻게 구성하고 어떤 이벤트가 있는지 살펴볼게요.
출처: 공식문서
훅이란
훅은 외부 프로그램이 Deep Agents Code 수명주기 이벤트를 관찰하고 제어하게 해줘요.
이벤트가 발생하면 Deep Agents Code는 일치하는 핸들러를 찾아 각각에 JSON 페이로드를 stdin으로 보내고, 그 종료 코드와 stdout을 결합해요. 그 응답으로 허용, 거부, 컨텍스트 주입, 또는 턴 계속을 할 수 있어요. 아래 섹션들에서 구성, Events, Input payload, Handler output을 다룹니다.
훅은 사용자 권한으로 실행되며, 구성에서 임의 코드를 실행해요. 훅 구성을 실행 코드로 취급하고, 신뢰하는 소스에서만 훅을 설치하세요.
설정 (Setup)
모든 프로젝트에 적용되는 훅은 ~/.deepagents/hooks.json을 만들고, 프로젝트 스코프 훅은 {project_root}/.deepagents/hooks.json을 만들어요 (워크스페이스 신뢰를 부여한 후). 핸들러는 이벤트 이름 → 매처 그룹 → 해당 그룹에서 실행되는 핸들러의 세 수준으로 중첩돼요.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.deepagents/hooks/block-rm.sh",
"timeout": 600
}
]
}
],
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "~/.deepagents/hooks/load-context.sh"
}
]
}
]
}
}
Deep Agents Code는 우선순위 순서로 훅 구성을 로드해요:
- 워크스페이스 신뢰가 부여된 후
{project_root}/.deepagents/hooks.json의 프로젝트 훅. ~/.deepagents/hooks.json의 사용자 훅.- 활성화된 플러그인이 제공하는 훅.
일치하는 모든 핸들러는 동시에 실행되고, 결과는 우선순위 순서로 결합돼요. 우선순위는 어느 답이 이기는지를 결정하지, 어느 핸들러가 실행되는지는 결정하지 않아요. 더 낮은 우선순위 핸들러도, 더 높은 우선순위 핸들러가 이벤트를 멈추더라도 실행되고 그 부작용도 여전히 발생해요.
dcode config path를 실행해 별도의 프로젝트·사용자 훅 위치와 워크스페이스 신뢰 저장소를 확인해요.
훅 구성은 /reload나 새 세션까지 스냅샷돼요. 턴 중에 hooks.json을 편집해도 활성 스냅샷은 바뀌지 않아요. 플러그인을 활성화·비활성화해도 스냅샷이 바뀌므로, 그 훅을 반영하려면 /reload를 실행하세요.
프로젝트 훅 신뢰 (Trust project hooks)
프로젝트 훅은 저장소에서 오므로, 워크스페이스가 신뢰된 후에만 로드돼요:
- 대화형 세션은 신뢰되지 않은 워크스페이스에
.deepagents/hooks.json이 있으면 프롬프트해요. 워크스페이스를 신뢰하면 그 결정이~/.deepagents/.state/hooks_trust.json아래 해당 프로젝트 루트에 저장돼요. - 거부하면 그 세션의 프로젝트 훅을 건너뛰고 사용자·플러그인 훅으로 계속돼요.
Esc또는Ctrl+D로 취소하면 시작이 중단돼요.- headless와 CI 실행은 절대 프롬프트하지 않아요. 그 실행에 옵트인하려면
--trust-project-hooks를 전달하세요.
플러그인 훅
활성화된 플러그인은 hooks/hooks.json, 매니페스트 hooks 경로, 또는 인라인 매니페스트 객체에서 같은 구성 형태를 제공해요. 플러그인을 설치·활성화하는 것이 동의 게이트예요. 워크스페이스 신뢰는 프로젝트 훅만 다루므로, 플러그인의 훅을 부여하거나 부여하지 않지 않아요. 활성화 전에 플러그인을 검토하고, 플러그인 매니저에서 선언한 이벤트를 확인하세요. Plugins and marketplaces 참고.
서버 소유 이벤트는 세션 시작 시 고정되므로, 새로 활성화된 플러그인 훅은 다음 시작 또는 /reload에 활성화돼요.
플러그인 핸들러는 이 변수들로 자체 설치 경로를 참조할 수 있어요:
| 변수 | 값 |
|---|---|
${CLAUDE_PLUGIN_ROOT}, ${PLUGIN_ROOT} |
설치된 플러그인 디렉토리 |
${CLAUDE_PLUGIN_DATA}, ${PLUGIN_DATA} |
플러그인의 쓰기 가능한 데이터 디렉토리 |
${CLAUDE_PROJECT_DIR} |
프로젝트 루트 |
설치 경로에 공백이 포함될 수 있으므로 command 문자열에서 이 변수를 인용하세요: "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/format.sh\"". 가능하면 선택적 argv 필드를 선호해요. Deep Agents Code가 시작 전에 변수를 해석하고 셸을 건너뛰므로 인용이 필요하지 않아요.
유효하지 않은 플러그인 훅 문서는 자체적으로 건너뛰고 구성 진단으로 보고돼요. 다른 플러그인, 프로젝트 훅, 사용자 훅은 계속 동작해요.
핸들러 필드
매처 그룹 hooks 배열의 각 항목은 명령 핸들러예요:
type(string, 필수): 핸들러 유형.command만 지원. 명령 핸들러는 이벤트 JSON을 stdin으로 받는 서브프로세스를 실행한다.command(string, 필수): 실행할 셸 명령. 항상 필수. 파이프, 리다이렉트, glob, 환경 변수 확장이 지원된다. 이벤트 페이로드는 stdin에 JSON으로 쓰여지며, 인자로 보간되지 않는다.argv도 설정되면 이 문자열은 셸을 거치지 않는다.argv(list[string], 선택):command를 셸로 해석하는 대신 인자 목록을 직접 실행한다. 명시적 실행 파일 경로와 인자에 쓰세요.timeout(number, 선택): 핸들러당 타임아웃(초). 기본 600초이며,UserPromptSubmit는 기본 30초. 타임아웃은 비차단 실패다.statusMessage(string, 선택): 핸들러 실행 중 UI에 표시되는 일시 메시지.
지원되지 않는 핸들러 유형이나 "async": true를 설정하면 눈에 띄는 구성 오류가 발생해요.
핸들러 환경
핸들러는 페이로드에서 cwd로 보고된 작업 디렉토리에서 시작하고, 세션 환경을 상속하되 자격 증명처럼 보이는 변수는 제거돼요: 이름에 KEY, TOKEN, SECRET, PASSWORD, APIKEY 중 어느 하나를 포함하는 변수는 시작 전에 제거돼요. 자격 증명이 필요한 핸들러는 상속된 환경이 아니라 파일이나 시크릿 매니저에서 읽어야 해요. 플러그인 핸들러는 추가로 자체 플러그인 경로 변수를 받아요.
매처 (Matchers)
매처는 주어진 이벤트에 핸들러 그룹이 실행되는지 필터링해요. 각 이벤트는 한 필드에 대해 매치돼요 (Events 참고):
- 생략, 빈 값,
*는 해당 이벤트의 모든 값과 매치. - 단순 이름은 정확히 매치 (
Bash). |또는,는 정확한 대안을 구분 (Edit|Write).- 다른 값은 앵커되지 않은 정규식으로 취급 (
mcp__.*).
UserPromptSubmit와 Stop은 매처 필드가 없어요. 그 이벤트에는 matcher를 생략하거나 *로 설정하세요. 다른 값은 구성 로드 시 거부돼요.
컴파일 오류는 그 그룹을 무효화하고 세션이 실행되기 전에 사용자에게 보이는 구성 진단을 만들어요.
이벤트 (Events)
Deep Agents Code는 다음 이벤트를 방출해요. 클라이언트 소유 이벤트는 CLI 프로세스에서 실행돼요. 서버 소유 이벤트는 에이전트 실행 경로에서 시작되어 클라이언트로 왕복해, 구성이 있는 곳에서 명령 핸들러가 실행돼요.
| 이벤트 | 소유자 | 종료 코드 2 효과 | 매치 대상 |
|---|---|---|---|
SessionStart |
Client | Diagnostic | source |
UserPromptSubmit |
Client | Block prompt | none |
SessionEnd |
Client | Diagnostic | reason |
PermissionRequest |
Client | Deny | tool_name |
Notification |
Client | Diagnostic | notification_type |
PreToolUse |
Server | Deny | tool_name |
PostToolUse |
Server | Feedback | tool_name |
PreCompact |
Server | Block compaction | trigger |
Stop |
Server | Continue turn | none |
SubagentStart |
Server | Diagnostic | agent_type |
SubagentStop |
Server | Add context | agent_type |
PreToolUse는 권한 프롬프트 전, 도구 실행 전에 실행돼서 도구를 허용·거부할 수 있는 주요 지점이에요. Stop은 터미널 모델 응답이 커밋되기 전에 실행돼요.
flowchart LR
A["Agent requests tool"] --> P["PreToolUse"]
P -->|allow| T["Tool runs"]
P -->|ask| H["Permission prompt"]
P -->|deny| X["Tool blocked"]
H --> T
T --> PT["PostToolUse"]
다이어그램은 도구 호출 경로만 다뤄요. PermissionRequest는 Deep Agents Code가 권한 프롬프트를 보이려 할 때의 별도 클라이언트 소유 이벤트예요.
입력 페이로드 (Input payload)
모든 핸들러는 stdin에서 JSON 객체를 받아요. 모든 이벤트는 공통 엔벨로프와 이벤트별 필드를 공유해요.
공통 필드
| 필드 | 설명 |
|---|---|
session_id |
세션 식별자 |
transcript_path |
사용 가능할 때 대화 트랜스크립트 경로 |
cwd |
훅이 호출될 때 작업 디렉토리 |
hook_event_name |
발생한 이벤트 이름 |
prompt_id |
사용 가능할 때 현재 사용자 프롬프트의 UUID |
permission_mode |
의미 있을 때 권한 모드 (default, plan, acceptEdits, auto, dontAsk, bypassPermissions) |
effort |
{ "level": "medium" } 같은 객체. level은 none, low, medium, high, xhigh, max 중 하나. 사용 가능할 때 |
agent_id, agent_type |
서브에이전트 정체성, 사용 가능할 때 |
transcript_path는 ~/.deepagents/transcripts 아래에 기록되는 대화의 JSONL 프로젝션을 가리켜요. 서브에이전트 이벤트는 서브에이전트 자체 트랜스크립트용 agent_transcript_path도 운반해요. 두 파일 모두 일치하는 핸들러가 실행되기 전에 갱신되므로, 핸들러는 현재 이벤트까지의 대화를 읽을 수 있어요.
이벤트별 필드
| 이벤트 | 필드 |
|---|---|
SessionStart |
source (startup, resume, clear, compact) 및, 가능할 때 model |
UserPromptSubmit |
prompt |
SessionEnd |
reason (clear, resume, prompt_input_exit, other) |
PermissionRequest |
tool_name, tool_input, permission_suggestions (현재 비어 있음) |
Notification |
message, notification_type, 가능할 때 title |
PreToolUse |
tool_name, tool_input, tool_use_id |
PostToolUse |
tool_name, tool_input, tool_response, tool_use_id, 가능할 때 duration_ms |
PreCompact |
trigger (manual, auto), custom_instructions |
Stop |
stop_hook_active, last_assistant_message, background_tasks, session_crons |
SubagentStart |
agent_id, agent_type |
SubagentStop |
stop_hook_active, agent_id, agent_type, agent_transcript_path, last_assistant_message, background_tasks, session_crons |
PreToolUse 페이로드 예:
{
"session_id": "abc123",
"transcript_path": "/Users/you/.deepagents/.../transcript.jsonl",
"cwd": "/Users/you/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build"
},
"tool_use_id": "toolu_01ABC"
}
도구 이름
훅 스크립트는 내부 Deep Agents Code 도구 이름이 아니라 안정적인 공개 도구 이름과 인자 형태를 봐요. PreToolUse, PostToolUse, PermissionRequest에서 이 이름들을 매치하고 읽으세요:
| 공개 도구 이름 | 주목할 입력 필드 |
|---|---|
Bash |
command, 선택적 timeout(밀리초) |
Write |
file_path, content |
Edit |
file_path, old_string, new_string, replace_all |
Read |
file_path, limit, offset |
Glob |
pattern, path |
Grep |
pattern, path, glob, output_mode, head_limit |
LS |
path |
mcp__<server>__<tool> |
도구별 JSON |
핸들러 출력 (Handler output)
명령 핸들러는 종료 코드, stdout, stderr로 결과를 통신해요.
| 종료 코드 | 의미 |
|---|---|
0 |
성공. stdout에 JSON이 있으면 파싱되어 적용. |
2 |
해당 이벤트의 차단 또는 피드백 경로. Events 표의 종료 코드 2 효과 열 참고. stdout JSON은 무시되고, stderr가 기본 피드백 채널. |
| 다른 0이 아닌 값 | 비차단 오류. Deep Agents Code가 진단을 기록하고 계속. |
JSON 출력은 종료 0에서만 처리되며 stdout의 유일한 내용이어야 해요. 성공적인 비-JSON stdout은 SessionStart와 UserPromptSubmit의 추가 컨텍스트가 되고, 다른 이벤트에서는 진단을 생성해요. stdout과 stderr는 각각 최대 100,000 바이트까지 유지돼요.
공통 출력 필드
모든 핸들러는 다음 최상위 필드를 반환할 수 있어요:
{
"continue": true,
"stopReason": "optional user-facing reason when continue is false",
"suppressOutput": false,
"systemMessage": "optional message shown to the user",
"terminalSequence": "optional restricted terminal control sequence",
"hookSpecificOutput": {
"hookEventName": "PreToolUse"
}
}
일치하는 모든 핸들러가 결과가 결합되기 전에 끝나요. "continue": false를 반환하면 축소된 결정이 멈췄다고 표시하지만, 다른 일치 핸들러가 실행되는 것을 막지는 않아요. 첫 번째 stopReason이 구성 순서로 이겨요. suppressOutput은 그 핸들러의 systemMessage만 억제해요.
이벤트별 제어는 hookSpecificOutput(도구·권한 이벤트) 또는 최상위 decision과 reason(Stop)에 있어요.
PreToolUse로 도구 실행 제어
도구가 실행되기 전에 허용·거부·프롬프트 강제 결정을 반환해요:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by hook"
}
}
여러 훅이 일치하면 결정은 우선순위 deny > ask > allow로 결합돼요. deny는 권한 프롬프트 전에 실행을 단락시키고 그 이유를 모델에 전달해요. ask는 권한 프롬프트를 강제해요. allow는 일반 프롬프트를 억제하지만 별도의 deny나 ask를 덮어쓰지 않아요. additionalContext 값은 구성 순서로 전달돼요.
종료 코드 2와 stderr에 이유를 적어 차단할 수도 있어요.
PermissionRequest로 허용·거부
사용자를 대신해 권한 프롬프트에 답하는 결정을 반환해요:
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "deny",
"message": "Not allowed in this environment"
}
}
}
어떤 deny든 이겨요. 거부하는 훅이 없고 최소 하나가 허용하면 작업이 허용돼요. 아무 훅도 결정하지 않으면 일반 권한 프롬프트가 표시돼요.
Stop로 턴 계속
턴을 끝내는 대신 에이전트가 계속 일하도록 차단 결정을 반환해요:
{
"decision": "block",
"reason": "Tests are still failing; keep working"
}
block은 피드백과 함께 에이전트 턴을 계속해요. Stop.hookSpecificOutput.additionalContext도 같은 계속 효과가 있어요. 무한 루프를 피하려면 페이로드에서 stop_hook_active를 확인하고 조건이 충족되면 차단을 멈추세요. Deep Agents Code는 연속 8회 연속 계속을 하드 캡으로 강제해요.
컨텍스트 주입
SessionStart, UserPromptSubmit, SubagentStart는 모델에 컨텍스트를 추가할 수 있어요:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Current sprint: ENG-1421. Prefer the staging database."
}
}
UserPromptSubmit는 suppressOriginalPrompt도 지원해요. PostToolUse와 SubagentStop는 모델에 additionalContext를 추가할 수 있지만, 이미 실행된 작업을 되돌릴 수는 없어요.
지원되지 않는 출력 필드
다음 호환성 필드는 인식되지만 적용되지 않아요. Deep Agents Code는 진단을 방출하고 Result 열의 폴백으로 계속돼요. 도구·권한 행의 경우, 도구 입력을 변형하거나 연기하지 않고 일반 PreToolUse 또는 PermissionRequest 결정 경로를 의미해요.
| 필드 또는 동작 | 결과 |
|---|---|
SessionStart.initialUserMessage, sessionTitle, watchPaths, reloadSkills |
파싱만, 적용 안 함 |
UserPromptSubmit.sessionTitle |
파싱만, 적용 안 함 |
PreToolUse.updatedInput |
변형 무시; allow·ask는 일반 PreToolUse 결정 사용 |
PreToolUse.defer |
일반 PreToolUse 결정 사용; allow로 취급되지 않음 |
PostToolUse.updatedToolOutput, updatedMCPToolOutput |
파싱만, 적용 안 함 |
PermissionRequest.updatedInput |
변형 무시; allow는 일반 PermissionRequest 결정 사용 |
PermissionRequest.updatedPermissions |
파싱만, 적용 안 함 (권한 규칙 저장소 없음) |
SubagentStop block |
컨텍스트만; 완료된 서브에이전트는 재개 불가 |
예시
파괴적인 Bash 명령 차단 (PreToolUse)
#!/usr/bin/env bash
command=$(jq -r '.tool_input.command // ""')
if printf '%s' "$command" | grep -Eq 'rm[[:space:]]+.*-[a-zA-Z]*[rf]'; then
cat <<'JSON'
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Recursive or forced rm is blocked by policy"
}
}
JSON
fi
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.deepagents/hooks/block-rm.sh" }
]
}
]
}
}
세션 시작 시 프로젝트 컨텍스트 로드 (SessionStart)
#!/usr/bin/env bash
context=$(git -C "$(jq -r '.cwd')" log --oneline -5 2>/dev/null)
jq -n --arg ctx "$context" '{
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: ("Recent commits:\n" + $ctx)
}
}'
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{ "type": "command", "command": "~/.deepagents/hooks/load-context.sh" }
]
}
]
}
}
macOS에서 턴이 끝날 때 데스크톱 알림 (Stop)
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Agent finished\" with title \"Deep Agents Code\"'"
}
]
}
]
}
}
페이로드를 읽는 Python 핸들러
import json
import sys
def handle(payload: dict[str, object]) -> None:
event = payload["hook_event_name"]
if event == "PreToolUse":
tool_name = payload["tool_name"]
print(f"About to run {tool_name}", file=sys.stderr)
if __name__ == "__main__":
handle(json.load(sys.stdin))
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 ~/.deepagents/hooks/handler.py"
}
]
}
]
}
}
훅 문제 해결
훅 활동은 로그뿐 아니라 세션에서도 보여요:
- 실행 중인 핸들러는
statusMessage, 또는 설정하지 않으면Running <event> hook을 보여줘요. 동시 핸들러는 상태 슬롯 하나를 공유하므로, 완료될 때까지 가장 최근 것이 표시돼요. - 핸들러의
systemMessage는 정보성 알림으로 나타나요. - 구성 오류, 0이 아닌 종료, 타임아웃, 미지원 출력 필드는 호출마다 한 번
Hook warning또는Hook error알림으로 나타나요. - 훅의 권한 답변은 훅에 귀속돼요, 예:
PermissionRequest hook denied Bash. DEEPAGENTS_CODE_DEBUG=1을 설정하면 알림으로 표시되지 않는 디버그 수준 항목을 포함해 모든 진단을 포착해요.
레거시 구성
오래된 list 형태의 hooks.json 파일은 폐기됐지만 여전히 지원돼요. Deep Agents Code가 동등한 이벤트를 자동 마이그레이션하고, 안전한 매핑이 없는 이벤트는 진단과 함께 건너뛰어요.
보안
훅은 Git 훅이나 셸 별칭과 같은 신뢰 모델을 따라요. hooks.json에 쓸 수 있는 어떤 프로세스든 사용자 권한으로 임의 명령을 실행할 수 있어요.
- 페이로드 데이터는 stdin으로 JSON으로 흐르며, 명령 인자로 보간되지 않아요.
- 자격 증명처럼 보이는 환경 변수는 핸들러 환경에서 제거돼요.
- 훅 구성은
/reload나 새 세션까지 고정돼요. - 셸 래퍼보다는 직접 제어하는 명시적 셸 실행 파일을 선호해요.
- 신뢰하는 소스에서만 훅을 설치해요.
훅은 사용자 권한으로 실행됩니다. 훅 구성을 실행 코드로 취급하세요.
더 알아보기 (Learn more)
- Configuration — 훅과 환경 변수 설정 전반.
- Plugins and marketplaces — 플러그인이 제공하는 훅 구성.
- Data locations — 훅 구성이 저장되는 위치.
- CLI reference — 훅 관련 명령 참조.