Claude Code 세션에서는 hook 이벤트 30개가 발생합니다. 응답할 수 있는 것은 3개뿐입니다.
Claude Code hook 이벤트 전체 목록, 각 이벤트가 발생하는 시점, 차단할 수 있는 15개, 그리고 대부분의 hook 출력을 소리 없이 삼켜버리는 stdout 규칙. 수천 건의 에이전트 세션에서 hook을 실제로 운영하며 정리한 현장 레퍼런스입니다.
Claude Code에 hook을 붙일 때 반복해서 나타나는 실패가 두 가지 있는데, 둘은 겉보기에 전혀 닮지 않았습니다.
첫 번째: hook을 추가했는데 아무 일도 일어나지 않습니다. 오류도, 경고도, 로그 한 줄도 없습니다. hook이 그냥 한 번도 실행되지 않습니다.
두 번째: hook은 분명히 실행되고 디스크에 남은 부수 효과도 보이는데, 에이전트에게 보내려고 출력한 메시지가 끝내 도착하지 않습니다. 에이전트는 hook이 아무 말도 하지 않은 것처럼 행동합니다.
둘 다 원인은 같습니다. hook 시스템은 대부분의 글이 다루는 몇 개의 이벤트보다 훨씬 크고 훨씬 덜 균일하며, 누가 에이전트에게 말을 걸 수 있는지에 대한 규칙은 짐작과 다릅니다. 이 글은 우리에게 처음부터 있었으면 했던 레퍼런스입니다. 우리는 이 hook들 위에 AgentsRoom을 만들고 있고, 아래 내용은 모두 공식 레퍼런스에서 인용했거나 프로덕션에서 측정한 것입니다.
이벤트는 여섯 개가 아니라 30개입니다
대부분의 가이드는 PreToolUse, PostToolUse, UserPromptSubmit, Stop, Notification, SubagentStop을 다룹니다. 이 여섯 개는 실제로 존재하고, 쓸모 있는 작업의 대부분을 담당합니다. 동시에 존재하는 전체의 5분의 1이기도 합니다.
각 이벤트가 무엇을 관찰하는지에 따라 묶은 전체 목록입니다.
| 그룹 | 이벤트 |
|---|---|
| 세션 | SessionStart, SessionEnd, Setup |
| 프롬프트 | UserPromptSubmit, UserPromptExpansion |
| 도구 | PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch |
| 권한 | PermissionRequest, PermissionDenied |
| 턴 | Stop, StopFailure |
| 서브에이전트와 태스크 | SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle |
| 컨텍스트 | PreCompact, PostCompact, InstructionsLoaded |
| 환경 | FileChanged, CwdChanged, ConfigChange |
| Worktree | WorktreeCreate, WorktreeRemove |
| 인터페이스 | Notification, MessageDisplay |
| MCP elicitation | Elicitation, ElicitationResult |

한 세션 안에서 이벤트가 발생하는 순서. 도구 블록은 도구 호출마다 한 번씩 반복되고, 프롬프트 블록 전체는 턴마다 한 번씩 반복됩니다.
이 중 몇 개는 시스템을 바라보는 방식을 바꿉니다. PostToolUseFailure가 존재하므로 "도구가 동작했는가"라는 분기는 페이로드에서 추론하는 것이 아니라 하나의 이벤트입니다. PostToolBatch는 병렬 도구 호출 묶음이 모두 끝난 뒤 한 번 발생하므로, 수정할 때마다가 아니라 한 번만 린터를 돌리기에 알맞은 자리입니다. InstructionsLoaded는 CLAUDE.md를 읽을 때 발생하므로, 에이전트가 읽었으리라고 생각하는 그 규칙을 실제로 읽었는지 확인할 hook 지점이 생깁니다.
대부분의 hook 출력을 삼켜버리는 stdout 규칙
이 페이지에서 가장 쓸모 있는 한 가지입니다.
종료 코드가 0이면 Claude Code는 stdout을 파싱해 JSON 출력 필드를 찾습니다. 하지만 그 stdout이 에이전트에게 보이는지는 이벤트에 따라 다르고, 예외는 짧은 목록에 그칩니다. 레퍼런스의 설명입니다.
대부분의 이벤트에서 stdout은 디버그 로그에 기록되지만 트랜스크립트에는 표시되지 않습니다. 예외는
UserPromptSubmit,UserPromptExpansion,SessionStart이며, 이 이벤트들에서는 stdout이 Claude가 보고 활용할 수 있는 컨텍스트로 추가됩니다.
30개 중 3개입니다. PostToolUse hook에서 echo "warning: this migration is destructive"를 실행하고 에이전트가 읽어주기를 기대한다면, 에이전트는 절대 읽지 않습니다. 그 텍스트는 디버그 로그로 갔습니다.
다른 이벤트에서 에이전트 앞에 텍스트를 놓는 방법은 정확히 두 가지뿐입니다.
- 종료 코드 2로 끝내고 stderr에 쓰기. 종료 코드가 2이면 Claude Code는 stdout과 그 안의 JSON을 모두 무시하고, stderr를 오류 메시지로 에이전트에게 돌려줍니다.
- 종료 코드 0으로 끝내고 JSON 객체 출력하기. 그 객체에
hookSpecificOutput.additionalContext를 담습니다.
첫 번째 방법의 비대칭에 주의하세요. 종료 코드 0에서는 stdout이 중요하고 stderr는 중요하지 않습니다. 종료 코드 2에서는 stderr가 중요하고 stdout은 통째로 버려집니다. 이 둘을 반대로 알고 있는 것이, hook이 완벽하게 맞아 보이는데도 아무 말이 없는 이유입니다.
그 외의 종료 코드는 모두 차단하지 않는 오류입니다. 트랜스크립트에는 stderr의 첫 줄과 함께 <hook name> hook error 알림이 표시되고, 실행은 계속되며, stderr 전문은 디버그 로그에 남습니다.

종료 코드별로 어느 채널이 에이전트에 도달하는지. 점선 경로는 다들 있다고 생각하지만 실제로는 없는 경로입니다.
정확히 절반이 차단할 수 있습니다
15개 이벤트는 종료 코드 2에서 동작을 멈춥니다. 나머지 15개는 그것을 무시하고 계속 진행합니다.
차단할 수 있는 이벤트: PreToolUse, PermissionRequest, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, ConfigChange, PostToolBatch, PreCompact, Elicitation, ElicitationResult, WorktreeCreate.
차단할 수 없는 이벤트: PostToolUse, PostToolUseFailure, PermissionDenied, StopFailure, Notification, SubagentStart, SessionStart, Setup, SessionEnd, CwdChanged, FileChanged, PostCompact, WorktreeRemove, InstructionsLoaded, MessageDisplay.
실무적인 결론: 가드레일은 PreToolUse에 두어야 하고, PostToolUse에는 절대 두면 안 됩니다. PostToolUse는 도구가 성공한 뒤에 발생합니다. 거기서 종료 코드 2를 내도 쓰기 작업은 취소되지 않고, 피해가 디스크에 남은 채 오류만 출력됩니다. rm -rf를 막고 싶다면 그렇게 할 수 있는 자리는 정확히 한 곳뿐입니다.
PostToolUse는 차단하지 못하는데 PostToolBatch는 차단한다는 점은 한 번 더 볼 만합니다. 병렬 수정이 모두 반영된 뒤에도 배치 단위 검사로 턴을 멈출 수 있다는 뜻이고, 이것이 이 시스템이 제공하는 쓰기 이후의 거부권에 가장 가까운 수단입니다.
matcher는 정확 일치입니다. 갑자기 아니게 되기 전까지는
matcher 필드는 자기 안에 든 문자에 따라 평가 방식을 바꾸는데, 어느 쪽으로 갔는지 알려주는 것은 아무것도 없습니다.
| matcher | 평가 방식 |
|---|---|
"*", "", 또는 생략 | 모든 것에 일치 |
영문자, 숫자, _, -, 공백, ,, |만 사용 | 정확한 문자열, 또는 |나 ,로 나눈 정확한 문자열의 목록 |
| 그 외의 문자가 있는 경우 | 앵커 없는 JavaScript 정규식 |
함정은 앵커가 없다는 점입니다. 레퍼런스는 정규식이 RegExp.prototype.test로 검사된다고 명시하는데, 이 메서드는 값의 어느 위치에서든 일치하면 성공합니다. 그래서 Edit.*는 Edit에도 NotebookEdit에도 모두 일치합니다. 도구 하나만 노렸다면 ^Edit$라고 쓰세요.
엉뚱한 곳을 디버깅하기 전에 알아둘 만한, 버전에 따라 달라지는 동작이 두 가지 있습니다.
- 쉼표 구분자와 공백 허용은 Claude Code v2.1.191 이상이 필요합니다.
- 하이픈은 v2.1.195에서 정확 일치 문자 집합에 합류했습니다. 그전까지는
code-reviewer같은 matcher가 앵커 없는 정규식으로 취급되어senior-code-reviewer에서도 함께 발생했습니다.
AgentsRoom에서는 파일 귀속용 hook의 범위를 Write|Edit|MultiEdit|NotebookEdit로 좁힙니다. 이렇게 하면 정확한 문자열 경로에 머물러 이 네 개 도구에만 일치하고 그 밖의 것에는 일치하지 않습니다. 우리가 설치하는 라이프사이클 hook에는 matcher가 아예 없는데, 어느 경우든 우리에게 해당되는 이벤트이기 때문입니다.
hook을 정의할 수 있는 곳은 여섯 군데이고, 모두 병합됩니다
본능적으로 우선순위를 찾게 됩니다. 그런 것은 없고, 흥미로운 지점이 바로 거기입니다.
| 위치 | 범위 |
|---|---|
~/.claude/settings.json | 내 모든 프로젝트, 이 머신에만 적용 |
.claude/settings.json | 프로젝트 하나, 커밋 가능 |
.claude/settings.local.json | 프로젝트 하나, Claude Code가 gitignore 처리 |
| Managed policy settings | 조직 전체, 관리자가 통제 |
플러그인의 hooks/hooks.json | 플러그인이 활성화된 동안 |
| skill 또는 agent의 frontmatter | 해당 구성 요소가 활성화된 동안 |
레퍼런스의 설명입니다.
hook 항목은 settings 레벨끼리 서로를 대체하지 않고 병합됩니다. 사용자, 프로젝트, 로컬 settings는 관리형 hook을 제거하지 않고 각자의 hook을 더하며,
disableAllHooks설정은 관리형 settings 바깥에서 관리형 hook을 비활성화할 수 없습니다.
그래서 프로젝트 hook은 전역 hook을 덮어쓰지 않고 그 위에 쌓입니다. 여섯 개의 소스가 모두 더해집니다. 사용자 settings에 정의하고 프로젝트에도 다시 정의한 PostToolUse 포매터는 수정 한 번마다 두 번 실행되며, 유일한 증상은 뭔가 느려진 것 같다는 느낌뿐입니다.

여섯 개의 소스, 하나의 병합된 집합. 여기서는 무엇도 다른 것을 덮어쓰지 않습니다.
이 점은 어떤 도구가 다른 사람의 프로젝트에 hook을 설치할 때 .claude/settings.local.json이 왜 알맞은 자리인지도 설명해 줍니다. 프로젝트 범위로 한정되고, Claude Code가 gitignore 처리하며, CLI 플래그 없이 로드됩니다. AgentsRoom이 자기 항목을 기록하는 곳도 여기입니다. 그래야 사용자가 커밋한 .claude/settings.json은 절대 건드리지 않고, 동료들이 특정 머신에만 맞는 경로를 물려받는 일도 없습니다.
프로덕션에서 hook을 운영하며 배운 것
AgentsRoom은 여는 프로젝트마다 hook을 설치합니다. 에이전트 상태를 결정론적으로 추적하고 수정된 파일을 올바른 에이전트에 귀속시키기 위해서입니다. 그 규모에서만 드러나는 것들이 있습니다.
모르는 이벤트 이름은 조용히 무시됩니다. 문서에는 없는 동작이고, 우리는 여기에 의존하고 있습니다. 인스톨러에 새 라이프사이클 이벤트를 추가하면, 예전 CLI를 쓰는 사용자에게는 자기 바이너리가 한 번도 들어본 적 없는 이벤트 이름이 담긴 settings.local.json이 생깁니다. 아무것도 깨지지 않고, 아무 경고도 없이 그 항목은 건너뜁니다. 그래서 CLI 릴리스보다 먼저 인스톨러를 내보내도 안전합니다. 그리고 필연적으로, 오타 하나가 오류 대신 완전한 침묵을 만드는 이유이기도 합니다.
서브에이전트 안에 있는지는 agent_id로 알 수 있습니다. 이 필드는 hook이 서브에이전트 호출 안에서 발생할 때만 존재합니다. 들리는 것보다 중요한 이야기입니다. Stop은 메인 에이전트뿐 아니라 서브에이전트가 자기 턴을 끝낼 때도 발생합니다. "Stop이 오면 세션을 완료로 표시한다"는 순진한 규칙은, 아무 서브에이전트나 처음 반환하는 순간 세션 전체를 끝난 것으로 표시해 버립니다. 우리가 agent_id를 실은 턴 이벤트를 건너뛰는 이유가 정확히 이것입니다.
현재 턴을 알아내려고 transcript_path를 읽지 마세요. 레퍼런스는 트랜스크립트가 비동기로 기록되어 메모리 안의 대화보다 뒤처질 수 있다고 경고합니다. hook이 발생하는 시점에 가장 최근 메시지가 아직 거기 없을 수 있다는 뜻입니다. Stop과 SubagentStop이 last_assistant_message를 받는 것은 그 파일과 경쟁하지 않아도 되게 하기 위해서입니다.
hook은 유일하게 믿을 수 있는 상태 신호입니다. hook이 있기 전에는 에이전트가 생각 중인지, 대기 중인지, 끝났는지 알아내려고 PTY를 긁었습니다. 그 방식은 CLI가 터미널의 대체 화면 버퍼로 렌더링하는 순간 무너지는데, /tui fullscreen이 바로 그렇게 합니다. hook은 어떤 렌더러에서도 똑같이 발생합니다. 에이전트를 바깥에서 관찰하는 무언가를 만들고 있다면 바로 이 계층 위에 만들어야 하고, 스크래핑은 잘해야 대비책으로 남습니다.
async: true는 비용이 없습니다. hook의 command는 async: true를 선언할 수 있고, 그러면 에이전트는 그 hook을 기다리지 않습니다. 우리 hook은 2초 상한을 두고 로컬 엔드포인트에 POST를 보낸 뒤 곧바로 반환합니다. 수신하는 앱이 닫혀 있어도 에이전트의 턴 지연 시간은 영향을 받지 않습니다. hook이 관찰만 하고 아무것도 결정하지 않는다면, async로 돌리고 그 비용을 그만 치르세요.
hook이 터미널에 쓰레기를 쓰게 두지 마세요. 우리 스크립트는 최상위 레벨을 포함해 모든 예외를 삼킵니다. hook에서 처리되지 않은 트레이스백은 조용히 실패하는 데서 그치지 않고, 사용자가 한창 작업하는 도중에 그 터미널 세션에 Python 스택 트레이스를 출력합니다.
타임아웃
기본값은 넉넉합니다. 넉넉하지 않은 예외가 세 개 있습니다.
| hook 종류 | 기본 타임아웃 |
|---|---|
command, http, mcp_tool | 600초 |
prompt | 30초 |
agent | 60초 |
UserPromptSubmit (command, http, mcp_tool) | 30초 |
MessageDisplay (command, http, mcp_tool) | 10초 |
SessionEnd | 모든 hook이 함께 나눠 쓰는 1.5초, hook별 timeout이 더 길면 거기에 맞춰 최대 60초까지 상향 |
사람들이 놀라는 것은 SessionEnd의 예산입니다. hook마다 주어지는 할당량이 아니라 공유 예산이라서, 명시적으로 올리지 않으면 정리용 hook 세 개가 1.5초를 나눠 씁니다.
짧은 요약
- 이벤트는 30개 있습니다. 유명한 것은 여섯 개입니다.
- stdout이 에이전트에 도달하는 것은
UserPromptSubmit,UserPromptExpansion,SessionStart뿐입니다. 그 밖의 모든 곳에서는 종료 코드 2와 stderr를 쓰거나, JSON의additionalContext를 쓰세요. - 15개 이벤트는 종료 코드 2에서 차단하고, 15개는 무시합니다. 가드레일은
PreToolUse에 둡니다. - matcher는 특수 문자 하나가 앵커 없는 정규식으로 바꿔놓기 전까지는 정확한 문자열입니다.
- 여섯 개의 settings 소스가 더해지며 병합됩니다. 무엇도 다른 것을 덮어쓰지 않습니다.
- 이벤트 이름에 오타가 있으면 완전한 침묵 속에서 실패합니다.
이 이벤트들을 머리로 따져보는 대신 실제로 발생하는 모습을 보고 싶다면, 그게 바로 우리가 만든 것입니다. AgentsRoom은 에이전트별, 프로젝트별, 실행별로 모든 hook 트리거를 수십 개의 병렬 에이전트와 서브에이전트 세션에 걸쳐 보여줍니다. 여러분이 자기 settings에 설정한 hook은 적힌 그대로 계속 동작합니다. 진짜 CLI를 실행하기 때문입니다.
계속 읽기
개발 팀에서 AI 코딩 에이전트를 확장하는 방법
코딩 에이전트를 가진 한 명의 개발자는 생산성 이야기입니다. 20명의 에이전트를 가진 5명의 개발자는 조정 문제입니다. 팀이 확장할 때 가장 먼저 무너지는 것과 유지되는 설정: 커밋된 컨텍스트 파일, 명확한 파일 소유권, 폭발 반경에 의한 검토, 실제로 볼 수 있는 비용이 무엇인지입니다.
기사 읽기AgentsRoom, 이제 Kimi Code를 지원합니다
Moonshot AI의 터미널 코딩 에이전트 Kimi Code가 AgentsRoom의 정식 프로바이더가 되었습니다. Claude, Codex, Antigravity CLI, OpenCode, Aider, Grok Build, Mistral Vibe와 나란히 실행하고 대화 도중에 전환하세요.
기사 읽기여전히 AI 에이전트의 코드를 검토해야 할까요?
당신의 에이전트는 당신이 병합하던 풀 리퀘스트의 절반보다 더 나은 코드를 작성합니다. 그렇다면 여전히 모든 줄을 읽어야 할까요? 양측의 솔직한 주장, 에이전트가 실수했다는 10가지 신호, 그리고 각 변경 사항이 실제로 얼마나 많은 검토를 받아야 하는지에 대해 알아보세요.
기사 읽기
AgentsRoom 다운로드
모든 프로젝트에서 AI 에이전트(Claude, Codex, Antigravity CLI, OpenCode, Aider, Grok Build, Mistral Vibe, Kimi Code)를 하나의 창에서 실행하세요.
컴패니언 앱: 이동 중에도 에이전트를 모니터링
Claude, Codex, Antigravity CLI 또는 다른 AI 공급자를 사용하세요.
버그와 요청을 공개 백로그로 바로 보내세요.
AgentsRoom의 실제 모습.