Hooks는 Claude Code 라이프사이클의 특정 이벤트 시점에 자동으로 실행되는 쉘 커맨드다. CLAUDE.md가 AI 모델에게 하는 부탁이라면, Hooks는 AI를 감싸고 있는 엔진에 하는 설정이다. 그래서 모델이 어길 수 없다.
CLAUDE.md → Claude 모델에게 부탁 → 모델이 어길 수 있음
Hooks → Claude Code 엔진이 강제 → 모델이 어길 수 없음
Hooks 호출 판단은 Claude Code 엔진(프로그램)이 한다. Claude 모델(AI)이 아니다.
1. Claude 모델이 "Bash로 rm -rf / 실행할게요" 결정
2. Claude Code 엔진이 가로챔 → settings.json 확인
3. security-gate.sh 호출
4. exit 2 반환
5. 엔진이 실행 차단
6. Claude 모델에게 결과만 전달
Claude 모델은 3~5번 과정을 모른다. 엔진이 알아서 처리하고 결과만 알려준다.
~/.claude/settings.json # 전역 (모든 프로젝트)
.claude/settings.json # 프로젝트 전용 (팀 공유 가능)
| 이벤트 | 발동 시점 | 차단 가능 여부 |
|---|---|---|
PreToolUse | 툴 실행 직전 | 가능 (exit 2) |
PostToolUse | 툴 실행 직후 | 불가 (후처리 전용) |
UserPromptSubmit | 프롬프트 제출 시 | - |
Stop | Claude 응답 완료 시 | - |
SubagentStop | 서브에이전트 완료 시 | - |
SessionStart/End | 세션 시작/종료 시 | - |
exit 0 → 허용, 계속 진행
exit 1 → 경고만 출력, 진행은 됨
exit 2 → 차단, Claude에게 에러 메시지 전달
보안이 목적이라면 반드시 exit 2를 써야 한다. exit 1은 경고만 하고 통과시킨다.
settings.json은 "어떤 이벤트에, 어떤 명령어를 실행할지" 연결하는 허브 역할이다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
}
],
"PreToolUse": [
{
"matcher": "Bash",
"command": "~/.claude/hooks/security-gate.sh"
}
]
}
}
// 단순한 경우 — 바로 씀
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
// 복잡한 경우 — 스크립트 파일로 분리
"command": "~/.claude/hooks/security-gate.sh"
#!/bin/bash
INPUT=$(cat) # Claude가 stdin으로 보내는 JSON 읽기
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# rm -rf 패턴 차단
if echo "$CMD" | grep -qE 'rm\s+-rf\s+/'; then
echo "위험한 명령어 감지: $CMD" >&2 # >&2 = stderr로 출력 (차단과 무관)
exit 2 # 실제 차단 신호
fi
# main 브랜치 직접 커밋 차단
if echo "$CMD" | grep -q "git commit" && \
[ "$(git branch --show-current 2>/dev/null)" = "main" ]; then
echo "main 브랜치 직접 커밋 금지" >&2
exit 2
fi
exit 0
>&2와 exit 2는 별개다. >&2는 메시지 출력 방향이고, 실제 차단은 exit 2가 결정한다.
settings.json (허브)
├── PreToolUse → security-gate.sh 실행 (차단 가능)
└── PostToolUse → prettier 실행 (후처리)
각 스크립트는 서로 모른다. Claude Code 엔진이 이벤트 발생 시 settings.json을 보고 각각 독립적으로 호출한다.
PreToolUse가 가장 강력 — 실행 전에 차단 가능exit 2 = 차단, exit 1 = 경고만, exit 0 = 허용>&2는 메시지 출력 방향, 차단은 exit 2가 결정async: true 옵션으로 Hook을 백그라운드에서 실행하는 패턴Hooks are shell commands that execute automatically at specific lifecycle events in Claude Code. If CLAUDE.md is a request made to the AI model, Hooks are settings applied to the engine that wraps the AI. That is why the model cannot override them.
CLAUDE.md → Request to Claude model → Model can ignore it
Hooks → Enforced by Claude Code engine → Model cannot override it
Hooks are triggered by the Claude Code engine (the program), not the Claude model (the AI).
1. Claude model decides: "I'll run rm -rf / via Bash"
2. Claude Code engine intercepts → checks settings.json
3. Calls security-gate.sh
4. Script returns exit 2
5. Engine blocks the action
6. Only the result is sent back to the Claude model
The Claude model is unaware of steps 3–5. The engine handles it and only reports the outcome.
~/.claude/settings.json # Global (all projects)
.claude/settings.json # Project-specific (team-shareable)
| Event | When it fires | Can block? |
|---|---|---|
PreToolUse | Before a tool executes | Yes (exit 2) |
PostToolUse | After a tool completes | No (post-processing only) |
UserPromptSubmit | When user submits a prompt | - |
Stop | When Claude finishes responding | - |
SubagentStop | When a subagent completes | - |
SessionStart/End | Session start or end | - |
exit 0 → Allow, continue
exit 1 → Warning only, continues anyway
exit 2 → Block, send error message to Claude
For security purposes, always use exit 2. exit 1 only warns — it does not block.
settings.json acts as the hub that connects events to commands.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
}
],
"PreToolUse": [
{
"matcher": "Bash",
"command": "~/.claude/hooks/security-gate.sh"
}
]
}
}
// Simple — write the command inline
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
// Complex — separate into a script file
"command": "~/.claude/hooks/security-gate.sh"
#!/bin/bash
INPUT=$(cat) # Read JSON sent via stdin from Claude Code
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Block rm -rf pattern
if echo "$CMD" | grep -qE 'rm\s+-rf\s+/'; then
echo "Dangerous command detected: $CMD" >&2 # >&2 = output to stderr (unrelated to blocking)
exit 2 # Actual blocking signal
fi
# Block direct commits to main branch
if echo "$CMD" | grep -q "git commit" && \
[ "$(git branch --show-current 2>/dev/null)" = "main" ]; then
echo "Direct commits to main branch are not allowed" >&2
exit 2
fi
exit 0
>&2 and exit 2 are separate concerns. >&2 controls where the message is sent. exit 2 is what actually blocks the action.
settings.json (hub)
├── PreToolUse → security-gate.sh (can block)
└── PostToolUse → prettier (post-processing)
Each script is independent — they do not call each other. The Claude Code engine reads settings.json at each event and calls them separately.
PreToolUse is the most powerful — can block before executionexit 2 = block, exit 1 = warning only, exit 0 = allow>&2 controls message output direction; blocking is decided by exit 2