훅(Hooks) 횡단 — 코드가 AI의 행동에 끼어드는 법
한 줄 요약
훅(hook) = AI 에이전트가 일하는 도중 정해진 순간에 자동으로 끼어드는 코드. 모델을 건드리지 않고도 “이 순간엔 이걸 검사·주입·차단해라”를 강제한다. 왜 배우나 — 하네스 엔지니어링에서 훅은 가드레일·자동화·자기개선 루프가 실제로 작동하는 지점이다. 7개 프레임워크가 같은 문제를 어떻게 다르게 푸는지 보면 “훅으로 무엇을 할 수 있는가”의 지도가 통째로 그려진다.
그림
훅이 끼어드는 순간을 에이전트의 생명주기 위에 얹어 보자. 핵심은 세션의 처음과 끝, 그리고 도구 실행의 앞뒤다.
flowchart LR A(["세션 시작<br/>SessionStart"]) --> B(["프롬프트 제출<br/>UserPromptSubmit"]) B --> C{에이전트 루프} C --> D(["도구 실행 전<br/>PreToolUse"]) D -->|"허용·차단·수정"| E["(도구 실행)"] E --> F(["도구 실행 후<br/>PostToolUse"]) F --> C C --> G(["종료 시도<br/>Stop"]) G -->|"검증 미흡 → 차단"| C G -->|통과| H(["세션 종료<br/>SessionEnd"]) classDef hook fill:#fde2e4,stroke:#c9184a,color:#3a0ca3; class A,B,D,F,G hook
핵심 3지점만 기억하면 된다
PreToolUse(실행 전 검사·차단·수정), PostToolUse(실행 후 기록·검증), Stop(종료 게이트). 거의 모든 가드레일·검증·루프가 이 셋 위에서 돈다. 나머지 이벤트는 곁가지다.
쉽게 풀기
공장 컨베이어 벨트를 떠올려 보자. 제품(=AI의 작업)이 벨트를 따라 흐르는데, 중간중간 검문소를 세울 수 있다.
- 입구 검문소(세션 시작) → “오늘 작업 규칙 브리핑을 끼워 넣어라”
- 가공 직전(도구 실행 전) → “이 명령 위험하지 않은지 검사하고, 위험하면 막아라”
- 출고 직전(작업 종료) → “검증 안 끝났으면 내보내지 마라”
훅은 바로 이 검문소다. 모델(작업자)의 머리를 바꾸는 게 아니라, 벨트 위 특정 지점에 코드를 심어 흐름을 검사·수정·차단·보강한다. 그래서 훅의 본질은 늘 세 가지 질문으로 환원된다.
① 언제(어느 이벤트에서) · ② 무엇에 걸려(matcher) · ③ 무엇을 실행할까(handler)
이 “언제·무엇에·무엇을” 세 축이 그대로 형식이 된다. 거의 모든 프레임워크가 훅을 이벤트 → matcher 그룹 → 핸들러 배열의 3단 중첩으로 표현한다. Claude Code가 정의한 이 형식을 Codex(Rust로 새로 구현하면서도), OMC, ouroboros, fable-ish가 그대로 채택했다 — 사실상 업계 표준이 된 셈이다.
여기서 가장 중요한 통찰 하나. 7개를 외우려 하지 말고 두 진영으로 갈라서 보면 전부 정리된다.
모든 차이의 뿌리 — "정의하는 쪽"과 "소비하는 쪽"
- 호스트(계약 정의) = Claude Code · Codex · gajae-code. 훅의 형식과 이벤트 자체를 만든다. “어떤 순간에 어떤 데이터를 줄지”의 원천.
- 플러그인(계약 소비) = OMC · ouroboros · fable-ish · 내 패턴. 호스트가 정한 형식 위에 훅 스크립트를 얹어 자기 기능을 구현.
이 비대칭이 모든 차이의 뿌리다. 호스트는 “무엇이 가능한가”를, 플러그인은 “그 안에서 무엇을 할까”를 정한다. OMC의 화려한 랄프 루프도 결국 Claude Code가 준
Stop이벤트가 없으면 불가능하다.
핵심 정리
모두가 수렴한 공통 패턴 (왜 그렇게 됐나)
- 3중 중첩 스키마 — “언제·무엇에·무엇을”의 3차원이 본질. Codex가 Rust로 새로 짜면서도 같은 형태를 복제한 게 증거.
- stdin JSON → stdout JSON 단방향 계약(외부 프로세스형) — 언어 중립(셸·파이썬·노드 무관), 프로세스 격리(훅 크래시가 호스트를 안 죽임), 테스트 용이.
- 차단은 두 채널 —
exit 2(간단) 또는 JSONdecision:block(구조화된 사유 첨부). - 컨텍스트 주입은
additionalContext— 모델은 system-reminder·developer 메시지를 “지금 따를 지시”로 신뢰하므로, 코드가 모델 행동을 조종하는 가장 효율적 통로. - fail-open이 조언형 훅의 기본값 — 로그·추천·주입 훅은 실패해도 작업을 막지 않는다. 단, 권한·검증 훅은 fail-closed여야 한다.
갈림길 (누가 왜 다르게 했나)
4가지 분기점
- (A) 외부 프로세스 vs in-process 콜백 — 6개는 외부 프로세스, gajae만 TS 콜백. 표현력↑(LLM 전송 직전 메시지 통째 교체) 대신 격리성↓(언어 종속)를 택하고, 훅 권한을 좁혀 보완.
- (B) 신뢰 게이트 — Codex만 “비관리 훅은 승인 필요”. 공급망 위험 차단. 안전↑ vs 첫 사용 마찰↑.
- (C) 이벤트 표면 — 넓게 vs 좁게 — fable-ish는 단 3종으로 “분류→증거→완료게이트” 한 사이클을 완성. 넓은 표면(Claude·OMC)은 표현력이지만 유지·디버깅 비용. 목적이 하나면 표면도 최소로.
- (D) Stop 훅의 철학 — fable-ish의 Stop = “한 번 상기시키고 양보”(최대 2회). OMC·내 패턴의 Stop = “끝낼 때까지 다시 굴림”(랄프). 자율 존중 vs 하네스 강제 — 둘 다 무한루프 방지장치는 필수.
7개 한눈 비교 (위 두 진영으로 나눠 보기 — 빽빽하면 한 열씩 따로 훑어라)
| 프레임워크 | 형식 | 주요 트리거 | 한 줄 특이점 |
|---|---|---|---|
| Claude Code | hooks.json 3단 중첩, 핸들러 5종 | 세션·턴·도구 단위로 가장 많은 이벤트(계속 증가) | 계약의 원천. PreToolUse 우선순위 deny>ask>allow, updatedInput으로 입력 재작성 |
| Codex | config.toml 인라인 또는 hooks.json | 10종, 이벤트별 scope=Turn/Thread | 신뢰(trust) 모델 — 비관리 훅은 승인 필요, 기업 requirements.toml=무조건 실행 |
| gajae-code | 유일한 in-process TS 콜백(pi.on()) | 세션·턴 내부 이벤트 | context 훅이 LLM 전송 직전 messages[] 교체. 훅 권한을 의도적으로 좁힘 |
| OMC | CC 계약 소비, command가 run.cjs→.mjs 2단 | CC 이벤트 다수(약 11종) | additionalContext로 [MAGIC KEYWORD] 주입, Stop block으로 랄프 루프. 킬스위치 DISABLE_OMC |
| ouroboros | CC + Codex 이중 hooks.json | 관측 3종(SessionStart·UserPromptSubmit·PostToolUse) | 두 호스트 동시 타깃, keyword-detector가 <skill-suggestion> 주입, drift-monitor |
| fable-ish | CC 3종만, 셋 다 timeout:10 | 딱 3종(UserPromptSubmit·PostToolUse·Stop) | 공유 ledger로 PostToolUse↔Stop 통신, Stop 차단 최대 2회 |
| 내 패턴 | 별도 커스텀 훅 없음 — OMC 훅에 의존 | (OMC 제공 이벤트) | 하네스 레벨 자동강제가 비어 있음 = 개선 1순위 → 1_내패턴_개선백로그 |
정확도 노트 (Codex가 잡아준 정정)
초안엔 ① Claude 이벤트를 “13종”으로 못 박고 ② “내 패턴”에 풍부한 커스텀 훅이 있는 듯 적혀 있었다. 실제로는 ① 이벤트는 그보다 많고 계속 늘어나며 ② 내
~/.claude/settings.json엔 커스텀 훅이 없다(OMC 플러그인 훅에 의존). 이 약점이 오히려 개선의 출발점이다.
실제 예시
훅 형식은 어디서나 “이벤트 → matcher → 핸들러[]“의 3단 중첩이다. 같은 구조가 JSON과 TOML로 어떻게 나타나는지 비교해 보자.
// Claude Code: 플러그인 hooks.json (이벤트 → matcher → 핸들러[])
{
"PreToolUse": [
{
"matcher": "Bash", // ② 무엇에 걸려
"hooks": [
{ "type": "command", // ③ 무엇을 실행
"command": "${CLAUDE_PLUGIN_ROOT}/check.sh",
"timeout": 5 }
]
}
]
}# Codex: ~/.codex/config.toml — 같은 3단 구조를 TOML로
[[hooks.PreToolUse]]
matcher = "Bash"
command = "./check.sh"
timeout = 30 # (사용자 설정 필드명은 timeout, Rust 내부명은 timeout_sec)핸들러 타입은 여러 개지만 "실제로 실행되는 건 대부분 command"
Claude Code는
command·http·mcp_tool·prompt·agent5종을 정의하지만, Codex는command만 실제 실행하고prompt·agent는 파싱만 한다. 셸 명령(command)이 사실상의 공통 분모다.
내 스택(Claude+Codex+OMC)에 차용할 것
1) 검증 게이트를 Stop훅에 이식 (출처: fable-ish)
내 Stop엔 랄프 루프(강제 지속)만 있음 → "끝내려 하면 검증 증거부터 확인" 모드 추가.
PostToolUse가 ledger에 증거를 쌓고 Stop이 읽는 구조.
ledger는 /tmp 고정이 아니라 plugin data dir 기준으로.
2) fail-open / fail-closed를 의식적으로 분리 (출처: fable-ish + Codex 정정)
로그·추천·주입 = fail-open
권한 차단·산출물 검증·모델 라우팅 = fail-closed (또는 명시적 degraded)
"모든 훅 fail-open"은 위험.
3) PreToolUse updatedInput으로 "막기"보다 "고쳐 통과" (출처: Claude Code)
예: grep→rg 자동치환, Task 라우팅. 마찰 줄이며 정책 적용.
4) 에코 재주입 방지를 기본 위생수칙으로
[MAGIC KEYWORD] 같은 강한 주입 태그는 항상 sanitize (사용자 복붙해도 재발동 금지).
5) 무거운/가벼운 훅 timeout 차등 + async
매 프롬프트 블록되는 훅은 3~5초, setup은 길게, 정리성 훅은 비동기.요약 & 셀프체크
3줄 요약
- 훅 = “언제·무엇에·무엇을”의 3중 스키마로 에이전트 생명주기에 끼어드는 코드.
- 호스트는 계약을 정의(Claude·Codex·gajae), 플러그인은 소비(OMC·ouroboros·fable·나) — 이 비대칭이 모든 차이의 뿌리.
- PreToolUse·PostToolUse·Stop 셋이 가드레일·검증·루프의 핵심 무대.
스스로 답해보기
- 어떤 훅 이벤트에서 “위험한 셸 명령”을 막아야 할까? 그 훅은 fail-open이어야 할까 fail-closed여야 할까?
- fable-ish가 단 3종 이벤트만 쓰는데도 검증 규율을 완성할 수 있는 이유는?
- 내 패턴의 “커스텀 훅 부재”는 왜 약점이고, fable의 무엇을 빌려오면 메울 수 있나?
연결
근거 노트 CC_30_hooks · CX_40_hooks · OMC_10_entrypoint-hooks-loop · GJ_30_extension-points-hooks-skills-commands · OB_70_extension-points-hooks-skills-commands · FB_20_hook-event-loop · MINE_30_hooks-lifecycle
관련 횡단 _분석축_루브릭 · HOME · 검증 루프 횡단 · 1_내패턴_개선백로그
Codex 교차검증 메모
이 노트는 Codex 메타검증을 거쳐 ① Claude 훅 이벤트 개수를 “13종 고정”에서 “그보다 많고 계속 증가”로 정정, ② “내 패턴”의 커스텀 훅 존재 주장을 “커스텀 훅 없음(OMC 의존)“으로 정정했다. 두 정정 모두 본문에 반영되어 있다.