훅(Hook)
부탁으로는 안 지켜지는 것을 실행 직전에 기계로 막는다.
무엇인가
정해진 순간이 오면 자동으로 실행되는 스크립트다. 사람이나 AI가 부르는 것이 아니라, 사건이 일어나면 도구가 알아서 부른다.
이 저장소에서 「훅」이라고 부르는 것은 두 종류다. 이름은 같은데 거는 자리가 다르다.
| Claude Code 훅 | git 훅 | |
|---|---|---|
| 거는 자리 | .claude/settings.json 의 hooks | scripts/hooks/pre-commit (core.hooksPath) |
| 언제 도나 | 에이전트가 도구를 쓰기 직전·직후, 프롬프트 제출, 응답 종료 등 | 커밋 직전 |
| 누구를 막나 | AI 에이전트의 행동 | 사람이든 AI든 커밋하는 쪽 전부 |
| 여기서 쓰는 것 | 파괴적 git 명령 차단 | 한글 파일명 NFC 검사, noon 번역 누락 검사 |
왜 필요한가
규칙을 문서에 적는 것은 읽히기를 기대하는 부탁이다. 에이전트 정의나 CLAUDE.md 에 「reset --hard 금지」라고 아무리 굵게 써도, 모델이 그 줄을 놓치는 날이 온다. 그날 다른 세션의 커밋 안 된 작업이 날아간다.
훅은 부탁이 아니라 결정론적인 거부다. 모델이 무엇을 기억하든 상관없이, 명령이 실행되기 직전에 스크립트가 보고 막는다.
Claude Code 훅이 도는 방식
자주 쓰는 사건은 이렇다.
- PreToolUse 도구를 쓰기 직전. 막을 수 있는 유일한 자리라 안전장치는 대부분 여기 건다
- PostToolUse 도구를 쓴 직후. 포매터를 돌리거나 결과를 검사할 때
- UserPromptSubmit 사용자가 프롬프트를 보낸 직후. 맥락을 덧붙일 때
- Stop 에이전트가 응답을 끝내려 할 때. 「정말 끝났나」를 대조하게 할 때
설정은 사건 이름, 어떤 도구에 걸지(matcher), 실행할 명령으로 이뤄진다. 이 저장소의 배선은 이렇게 생겼다.
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"if": "Bash(git *)",
"command": "bash \"$CLAUDE_PROJECT_DIR/scripts/hooks/guard-destructive-git.sh\"",
"timeout": 10
}]
}]
스크립트는 표준 입력으로 받은 JSON 에서 .tool_input.command 를 꺼내 검사하고, 막을 때는 permissionDecision: "deny" 와 사유를 돌려준다. 통과면 아무것도 출력하지 않는다.
무엇을 훅으로 막나
하루에 훅 셋을 얹었다가 다음 날 전부 걷어낸 적이 있다. 할 일이 전부 완료로 바뀌면 요청과 대조하게 하는 것, 위임 프롬프트가 비대해지는 걸 막는 것, 구현 전에 계획을 세우게 하는 것이었다. 훅 셋과 측정 스크립트를 만든 그날, 정작 쓸 화면은 하나도 안 나왔다.
남긴 것은 파괴적 git 을 막는 훅 하나다. 기준은 틀렸을 때 그 자리에서 고칠 수 있는가다. 다른 세션의 커밋 안 된 작업이 날아가는 것은 되돌릴 수 없으니 기계로 막는다. 나머지는 틀렸을 때 고치면 된다. 훅은 매 호출마다 돌기 때문에, 막을 가치가 없는 것까지 걸면 그 비용을 매번 낸다.
지금 막는 것은 reset --hard, checkout ., clean -fd 같은 명령이다. 경로를 한정해도 막는다. git checkout HEAD <파일> 은 그 파일에 대한 reset --hard 와 같다. 경로 한정은 안전장치가 아니라 피해 범위를 줄일 뿐이다.
걸리기 쉬운 자리
조용히 멈춘 훅이 제일 나쁘다. 훅은 실패해도 오류를 크게 내지 않는다. 막는 장치가 없어진 것을 아무도 모르는 상태가 된다. 실제로 겪은 경로가 셋이다.
- 설정 파일을 git 에서 빼 두면 다른 기계나 다른 세션에서 훅이 통째로 사라진다. 에이전트 정의와 스킬만 추적하던 때가 그랬다. 지금은 설정까지 추적하고, 기계마다 다른 로컬 설정만 뺀다
- 훅이 기대는 도구가 없으면 입력을 못 읽어 값이 비고, 빈 값은 「검사할 것 없음」으로 통과된다. JSON 을
jq로 읽는 훅을 jq 없는 기계에 두면 배선은 돼 있는데 아무것도 안 막는다. 의존을 줄이거나, 입력을 못 읽으면 통과가 아니라 거부하게 짠다 - 다른 실행 경로를 안 보면 우회된다.
Bash도구에만 걸어 두면 다른 셸 도구로 낸 같은 명령은 그냥 지나간다. 명령 문자열을 정규식 하나로 보면 따옴표, 줄 이음, 감싸기로 빠져나가는 길이 여럿 생긴다. 단어를 먼저 나누고 명령별로 옵션을 해석해야 한다
훅을 만들었으면 막혀야 할 명령을 한 번 실제로 넣어 보고 막히는 것까지 본다. 배선했다는 것과 막는다는 것은 다르다.
git 훅은 따로 하나
커밋 직전에 도는 pre-commit 은 사람과 AI 를 가리지 않는다. 여기서는 한글 파일명이 NFC 인지 본다. 맥은 한글 파일명을 NFD 로 저장하는데, 리눅스 빌드 서버는 그 경로를 못 찾아 배포가 깨진다. noon 변경이 담긴 커밋에만 번역 누락 검사가 추가로 걸린다. 다른 프로젝트 커밋은 그대로 통과한다.
급할 때 git commit --no-verify 로 건너뛸 수 있다. 건너뛸 수 있다는 것까지 알고 쓰는 장치다.