최근 claude의 이슈들중 사용자 승인 없이 파일을 맘대로 수정하는 걸 해결하기 위해 hook을 만들어서 사용하고 있습니다
그런데 이런 생각이들더군요 "서브에이전트에게 지시한 작업의 마지막이 수정이면 어떻게 되는거지?"
확인해보니 서브에이전트의 작업중 hook으로 블락을 당하면 서브에이전트의 작업 내역은 블락되어 실패 되었다는 보고와 함께 종료가되더군요
토큰의 낭비가 되는 샘이죠
다행이 서브에이전트를 구분할 수 있는 분기를 발견하고, 서브에이전트의 경우 hook에서 분기를 태워 수정 전 ask를 발동하게 해서 해결 했습니다
yolo 모드에서 ask가 무시될걸 우려해 테스트해봤으나 다행이 hook에서 ask를 설정하면 오버라이드되서 정상작동을 확인했죠
기왕 파보는 김에 과거에 설치한 read_once훅도 파보자고 생각해서 read_once훅의 경우 파일을 두번째 읽으면 llm에게 경고가 가는 구조인데 이게 실제로 동작을 하는지가 의문이었어요
테스트 해보니 실제로 llm에게 경고가 가지 않더군요
이것도 까보다 보니 해당 훅의 필드가 잘못되어있었고, claude한테 이거 테스트 해보자 하니깐 갑자기 이녀석이 claude 바이너리를 까기 시작하더라구요
그래서 어찌어찌 해결하고, 갑자기 드는 생각 "Claude Code 유출 소스도 있으니 이걸로 역공학도 가능하겠는데?"
claude한테 소스코드 위치 알려주고 우리 훅에 적용 할만한것 확인하라고 했더니 매뉴얼에 없는 훅의 옵션들을 다량 발견해오더군요. 일부는 테스트도 해봤구
테스트한 내용 공유 드리니 심심하면 한번 봐주세요
🔌 Claude Code Hook 출력 필드 완전 정리
소스 코드(src/types/hooks.ts v2.1.88) + 바이너리 분석(v2.1.123) + 라이브 실험으로 검증한 hook 출력 필드 레퍼런스.
필드 역할 비교
필드 | 위치 | 대상 | 동작 | 검증 |
|---|
systemMessage
| 최상위 | 사람 (TUI 표시) | UI에만 표시, AI context 미주입 | ✅ |
hookSpecificOutput.additionalContext
| hookSpecificOutput 내부 | AI 모델 | 로 AI context 주입
| ✅ |
hookSpecificOutput.permissionDecisionReason
| hookSpecificOutput 내부 | 로그/UI | allow 시 AI context 미주입, deny 시 AI에 전달 | ✅ |
hookSpecificOutput.updatedInput
| hookSpecificOutput 내부 | 도구 입력 | 도구 실행 전 입력값 교체 | ✅ |
hookSpecificOutput.updatedMCPToolOutput
| hookSpecificOutput 내부 | MCP 도구 출력 | MCP 도구 결과값 교체 | 미검증 |
소스 코드 근거 (src/types/hooks.ts)
// 최상위 필드
systemMessage: z.string().describe('Warning message shown to the user').optional()
// hookSpecificOutput 내부 — PreToolUse
z.object({
hookEventName: z.literal('PreToolUse'),
permissionDecision: permissionBehaviorSchema().optional(),
permissionDecisionReason: z.string().optional(),
updatedInput: z.record(z.string(), z.unknown()).optional(), // 도구 입력 변조
additionalContext: z.string().optional(), // AI context 주입
})
// hookSpecificOutput 내부 — PostToolUse
z.object({
hookEventName: z.literal('PostToolUse'),
additionalContext: z.string().optional(),
updatedMCPToolOutput: z.unknown().optional(), // MCP 출력 변조
})
내부 처리 흐름 (src/utils/hooks.ts):
// systemMessage → hook_system_message (TUI 표시 전용)
if (result.systemMessage) {
yield { message: createAttachmentMessage({ type: 'hook_system_message', ... }) }
}
// additionalContext → hook_additional_context (AI context 주입)
if (result.additionalContext) {
yield { additionalContexts: [result.additionalContext] }
}
// → createAttachmentMessage({ type: 'hook_additional_context', content: [...] })
검증된 출력 형식
PreToolUse warn 모드 (allow + AI context 주입)
jq -cn --arg r "$REASON" \
'{"systemMessage":$r,"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":$r}}'
PreToolUse deny 모드
{
"hookSpecificOutput": {
"permissionDecision": "deny",
"permissionDecisionReason": "차단 이유"
}
}
PreToolUse updatedInput — 도구 입력 변조
# Bash 명령어 가로채서 변조
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "echo OVERRIDDEN" # Bash tool
# "file_path": "/new/path" # Read tool
}
}
}))
PostToolUse additionalContext
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "도구 실행 후 AI context에 주입할 내용"
}
}
라이브 실험 결과 (2026-05-01)
실험 1: additionalContext 주입 검증
additionalContext에 ACKNOWLEDGE: 응답 첫 줄에 X를 적어라 지시 삽입 → AI가 지시 수행 확인.
실험 2: updatedInput — Bash 명령어 변조 ✅
# 실제 실행한 명령어
echo HOOK_TEST_ORIGINAL
# 출력
HOOK_TEST_OVERRIDDEN ← hook이 교체
실험 3: updatedInput — Read 파일 경로 리다이렉트 ✅
Read("/tmp/REDIRECT_SOURCE.txt") 요청
→ hook이 file_path를 "/tmp/REDIRECT_TARGET.txt"로 교체
→ TARGET 파일 내용 반환
실험 4: PostToolUse additionalContext ✅
Bash 도구 실행 후 hook의 additionalContext가 로 AI context 주입 확인.
additionalContext가 지원되는 hook 이벤트
소스 코드 기준 additionalContext 필드가 있는 이벤트:
hookEventName | additionalContext |
|---|
PreToolUse | ✅ |
PostToolUse | ✅ |
PostToolUseFailure | ✅ |
UserPromptSubmit | ✅ |
SessionStart | ✅ |
Setup | ✅ |
SubagentStart | ✅ |
Notification | ✅ |
그 외 주목할 필드
필드 | hook | 설명 |
|---|
updatedInput
| PreToolUse | 도구 입력 변조 (Bash command, Read file_path 등) |
updatedMCPToolOutput
| PostToolUse | MCP 도구 결과값 교체 |
initialUserMessage
| SessionStart | 세션 시작 시 첫 메시지 주입 |
watchPaths
| SessionStart / CwdChanged | 파일 감시 경로 등록 |
retry
| PermissionDenied | 권한 거부 후 재시도 여부 |
updatedPermissions
| PermissionRequest | 권한 동적 변경 |
read-once hook 적용 내역
~/.claude/read-once/hook.sh warn 모드에 additionalContext 추가 (2026-05-01):
# 변경 전 (AI context 미주입)
jq -cn --arg r "$REASON" \
'{"systemMessage":$r,"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
# 변경 후 (AI context 주입)
jq -cn --arg r "$REASON" \
'{"systemMessage":$r,"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":$r}}'
diff 경로(warn 모드)도 동일하게 수정 + heredoc → jq 교체(특수문자 안전).
🤖 Claude Code Sub-agent Hook 감지 및 제어 패턴
Claude Code에서 sub-agent가 파일 수정(Edit/Write/Bash 쓰기)을 시도할 때 PreToolUse 훅으로 감지하고 제어하는 패턴. 실험으로 확인한 동작과 공식 문서 기반.
핵심 발견
1. Session ID 공유
Sub-agent는 parent와 동일한 session_id를 공유한다. UserPromptSubmit 훅은 유저 메시지가 있을 때만 실행되므로, sub-agent는 별도의 프롬프트 파일을 생성하지 않는다. 결과적으로 parent의 유저 메시지 파일을 그대로 읽게 된다.
2. agent_id 필드로 감지
PreToolUse hook이 받는 JSON에서 sub-agent 여부를 구분할 수 있다:
필드 | 메인 에이전트 | Sub-agent | 로직 사용 |
|---|
agent_id
| 없음 | 있음 (예: aaa6abfab...) | ✅ 사용 |
agent_type
| 없음 | 있음 (예: general-purpose) | ❌ 미사용 |
공식 문서 확인: "agent_id and agent_type are populated when the hook fires inside a subagent."
agent_type은 임의로 호출된 sub-agent에서 general-purpose로 관찰됐으나, 스킬 기반 호출과의 구분이 불확실하여 로직에는 사용하지 않는다. agent_id 유무만으로 감지하는 것이 충분하고 신뢰성이 높다.
3. Sub-agent는 parent 권한을 자동 상속하지 않음
공식 문서: "Subagents do not automatically inherit parent agent permissions."
스킬 호출의 경우 유저 메시지에 스킬명이 포함되어 화이트리스트를 통과하지만, Claude가 자율 판단으로 띄운 sub-agent는 parent 유저 메시지에 따라 우연히 통과되거나 블락된다.
permissionDecision 동작 비교
값 | 동작 | AI 컨텍스트 주입 |
|---|
"allow"
| 그냥 통과 | — |
"deny"
| 완전 차단 | Sub-agent에게 reason 전달됨 |
"ask"
| 사용자 UI 승인 다이얼로그 | AI에게는 전달 안 됨 |
"defer"
| 나중으로 미룸 (TypeScript 전용) | — |
우선순위: deny > defer > ask > allow
Note
ask는 사용자 UI로 직접 올라가며 AI 컨텍스트에 주입되지 않는다. deny는 sub-agent 컨텍스트에 reason이 전달되어 sub-agent가 이유를 파악하고 보고할 수 있다.
적용 패턴
Sub-agent를 감지해 ask로 분기하는 코드 (shell command hook):
raw = sys.argv[1]
try:
d = json.loads(raw)
# sub-agent 감지 → ask로 분기
agent_id = d.get("agent_id", "")
if agent_id:
file_path = d.get("tool_input", {}).get("file_path", "?")
reason = f"Sub-agent({agent_id[:8]})가 파일 수정을 요청합니다: {file_path}"
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": reason
}
}))
sys.exit(0)
# 이하 기존 로직 (메인 에이전트용)
...
Warning
ask는 permission_mode가 acceptEdits일 때 자동 승인될 수 있다. 실제 사용자 확인이 필요하면 환경의 permission_mode를 확인할 것.
Important
yolo 모드에서도 hook ask는 우회되지 않는다. yolo 모드는 Claude Code의 일반 permission prompt를 자동 승인하지만, 훅은 그보다 상위 레이어에서 개입한다. 훅의 ask는 yolo 모드 활성 여부와 무관하게 항상 사용자 다이얼로그를 표시한다. → sub-agent 수정 제어용으로 신뢰할 수 있는 안전장치.
이 볼트에 적용된 훅
파일 | 대상 | 적용 내용 |
|---|
~/.claude/scripts/hooks/pre-edit-question-block.sh
| Edit / Write | sub-agent → ask |
~/.claude/scripts/hooks/pre-bash-write-block.sh
| Bash 쓰기 명령 | sub-agent → ask |