Codex · 확장점: Hooks (이벤트 기반 외부 명령 실행)
Codex · 확장점: Hooks (이벤트 기반 외부 명령 실행)
한 줄 요약
Hooks는 Codex가 일하다 특정 “사건”을 만나는 순간(도구 쓰기 직전·직후, 세션 시작 등)에 내가 미리 등록한 외부 프로그램을 자동 실행해 흐름을 막거나 바꾸는 장치다. 왜 배우나: 코드를 고치지 않고 설정 파일만으로 “위험 명령 차단·회사 규칙 주입” 같은 안전장치와 자동화를 붙일 수 있기 때문이다.
그림
flowchart TD A["턴 생명주기 이벤트<br/>예: 도구 쓰기 직전 PreToolUse"] --> B["핸들러 고르기<br/>이벤트명 + matcher 매칭"] B --> C["동시 실행<br/>고른 핸들러들 한꺼번에"] C --> D["외부 명령 실행<br/>쉘로 실행, 사건정보 JSON을 stdin에 넣음"] D --> E["결과 읽기<br/>stdout JSON 파싱"] E -->|추가 맥락 additionalContext| F[개발자 메시지로 포장] F --> G[다음 모델 입력에 끼워넣음] E -->|"거부 deny/block 또는 exit 2"| H["도구 호출 차단<br/>차단 사유 반환"]
쉽게 풀기
Hooks를 “현관 CCTV + 자동문”이라고 생각해 보자. Codex라는 일꾼이 집안일을 하다가 문을 지날 때마다 센서가 작동하고, 미리 정해 둔 규칙대로 문을 열어주거나 막아 선다.
-
사건이 발생한다. Codex가 일하다 정해진 순간에 도달한다. 예를 들어 “Bash 명령을 실행하기 직전”, “도구 작업을 끝낸 직후”, “세션이 막 시작될 때” 같은 시점이다. 이런 시점을 이벤트라고 부른다.
-
누구한테 반응할지 고른다(matcher). 모든 도구에 다 반응하면 시끄럽다. 그래서 “Bash 도구일 때만”, “Edit이나 Write일 때만”처럼 matcher라는 필터로 대상을 고른다. CCTV를 현관에만 달지, 모든 방에 달지를 정하는 것과 같다.
-
내가 등록한 프로그램이 실행된다(command). 조건이 맞으면 Codex가 내 스크립트(쉘 명령, 파이썬 파일 등)를 자동으로 띄운다. 이때 Codex는 “지금 무슨 일이 벌어졌는지”를 JSON 쪽지에 적어 stdin(표준입력)으로 건네준다. 어떤 도구인지, 어떤 명령을 쓰려는지 등이 그 쪽지에 담긴다.
-
내 프로그램이 판정을 돌려준다(stdout). 스크립트는 쪽지를 읽고 판단한 뒤, 답을 다시 JSON으로 stdout(표준출력)에 적어 돌려준다. 답은 크게 세 종류다.
- 막아라(차단): “이 명령은 위험하니 실행하지 마” → 도구 호출이 중단되고 모델에게 “차단됨 + 사유”가 전달된다.
- 맥락을 더해라(주입): “실행은 허용하되, 모델에게 ‘이건 회사 규칙상 주의 대상’이라고 알려줘” → 그 문장이 모델의 다음 입력에 개발자 메시지로 끼워진다.
- 그냥 통과: 아무 출력 없이 정상 종료하면 “이상 없음, 계속”이라는 뜻이다.
비유로 정리하면, matcher는 “어느 문에 센서를 다느냐”, command는 “센서가 울릴 때 부르는 경비원”, stdout JSON은 “경비원의 판정문(통과/차단/메모)“이다.
핵심 정리
| 개념 | 한마디 | 비유 |
|---|---|---|
| 이벤트 | 훅이 작동하는 시점 | 센서가 달린 문 |
| matcher | 어떤 도구에 반응할지 거르는 필터 | 센서를 단 위치 |
| command | 실행되는 외부 프로그램 | 출동하는 경비원 |
| stdout JSON | 훅이 돌려주는 판정 | 경비원 판정문 |
[!note] 지원하는 이벤트 10종 (TOML 키 → 작동 시점)
config/src/hook_config.rs가 TOML 키를 내부 이벤트 이름으로 매핑한다.
- PreToolUse — 도구 쓰기 직전 (matcher: 도구 이름)
- PermissionRequest — 승인 요청 시 (matcher: 도구 이름)
- PostToolUse — 도구 쓴 직후 (matcher: 도구 이름)
- PreCompact / PostCompact — 대화 압축 전 / 후 (matcher:
manual/auto)- SessionStart — 세션 시작 시 (matcher:
startup/resume/clear/compact)- UserPromptSubmit — 사용자가 프롬프트 제출 시 (matcher 무시, 항상 매치)
- SubagentStart / SubagentStop — 서브에이전트 시작 / 종료 (matcher: 서브에이전트 타입)
- Stop — 턴 종료 시 (matcher 무시, 항상 매치)
[!note] matcher 매칭 규칙 — “단순 토큰은 완전일치, 특수문자 있으면 정규식”
"*"또는""또는 생략 = 전부 매치Bash,Edit|Write처럼 영숫자·_·|만 있으면 완전일치 (부분일치 아님 —Bash는BashOutput에 안 걸림)^Bash$,mcp__filesystem__.*처럼 특수문자가 있으면 정규식으로 처리
선언은 설정 레이어를 낮은 우선순위부터 훑으며 발견된다(discovery.rs). 둘 수 있는 위치:
-
~/.codex/hooks.json(사용자, JSON 파일) -
~/.codex/config.toml의 인라인[hooks]테이블 (사용자, TOML) -
<repo>/.codex/hooks.json(프로젝트 —.codex/레이어가 신뢰될 때만) -
<repo>/.codex/config.toml의[hooks](프로젝트) - 기업 관리
requirements.toml의[hooks](managed, 신뢰 불필요) - 플러그인 번들: 플러그인 루트의
hooks/hooks.json
[!warning] 신뢰(trust)와 핸들러 제약
- 비관리(non-managed) 훅은 처음에 review & trust 되어야 실행된다. 기업 관리 훅만 신뢰 없이 돈다.
- 핸들러
type은command/prompt/agent세 가지로 파싱되지만, 현재 실제로 실행되는 건command뿐이다(prompt/agent는 파싱만).async옵션은 파싱되지만 아직 미지원이라 스킵된다.
실제 예시
정확한 선언 스키마 (참고용)
이벤트 키 값은 MatcherGroup의 배열이고, 각 그룹은 matcher 하나 + hooks(핸들러) 배열을 갖는다(config/src/hook_config.rs).
| 필드 | 위치 | 의미 |
|---|---|---|
matcher | MatcherGroup | 정규식/완전일치 문자열. "*"·""·생략 = 전부 |
hooks[].type | HookHandlerConfig | command(실행됨)/prompt/agent |
hooks[].command | HookHandlerConfig | 실행할 쉘 명령(stdin으로 JSON 받음) |
hooks[].timeout | HookHandlerConfig | 타임아웃(초, 필드명 timeout_sec). 초과 시 실패 |
훅이 stdout으로 돌려주는 JSON의 핵심 효과(engine/output_parser.rs):
| 이벤트 | 차단 신호 | 컨텍스트 주입 |
|---|---|---|
| PreToolUse | permissionDecision:"deny"+permissionDecisionReason, 또는 레거시 decision:"block"+reason, 또는 exit 2 + stderr | hookSpecificOutput.additionalContext |
| PostToolUse | decision:"block"+reason(비면 무효) | hookSpecificOutput.additionalContext |
| SessionStart / SubagentStart | (차단 불가) | additionalContext + plain stdout |
| UserPromptSubmit | decision:"block"+reason | additionalContext |
[!note] 모든 이벤트 공통(universal) 필드:
continue(false면 stopped 표시),stopReason,systemMessage(UI 경고),suppressOutput(미구현). 파서는 stdout을 trim 후 JSON 오브젝트일 때만 파싱하고, 빈/비오브젝트는 무출력 성공으로 본다.
1) 선언 — ~/.codex/config.toml (또는 <repo>/.codex/config.toml)
# 공식문서 발췌 형태: /mnt/d/6study/10_프레임워크분석/_원문아카이브/codex/50_hooks.md
[[hooks.PreToolUse]]
matcher = "^Bash$" # Bash 도구에만
[[hooks.PreToolUse.hooks]]
type = "command"
command = "python3 ~/.codex/hooks/guard.py"
timeout = 10
statusMessage = "Checking Bash command"
2) 훅 스크립트 — ~/.codex/hooks/guard.py (stdin=JSON, stdout=JSON)
#!/usr/bin/env python3
# /home/seunghyeong/.codex/hooks/guard.py
import sys, json
data = json.load(sys.stdin) # {session_id, cwd, hook_event_name, tool_name, tool_input, ...}
cmd = (data.get("tool_input") or {}).get("command", "")
if "rm -rf /" in cmd:
# 차단: deny + 사유 (사유 비면 무효 → 차단 안 됨)
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by hook."
}}))
else:
# 통과 + 모델에게 메모만 주입
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": "이 명령은 생성 파일을 건드립니다."
}}))
# (대안) 차단은 exit 2 + stderr 로도 가능
만들 때 체크리스트
- 이벤트 키 정확히(
PreToolUse등 대소문자 일치), 값은[[hooks.<Event>]]배열 -
matcher: 단순 토큰=완전일치(Bash≠BashOutput), 정규식 쓰려면^Bash$/.* - 핸들러
type = "command"(prompt/agent는 아직 실행 안 됨) - 명령은 절대경로 권장(상대경로
.codex/hooks/...는 시작 cwd 따라 깨질 수 있음) - stdout JSON은 오브젝트여야 파싱됨. 무출력+exit0 = 성공/계속
- 차단 시 reason/permissionDecisionReason 반드시 비어있지 않게
- 비관리 훅은 처음에 review & trust 필요
- Windows 분기 필요하면
commandWindows(command_windows) 추가 -
additionalContext는 developer 메시지로 주입됨 — 모델이 읽을 문장으로 작성
요약 & 셀프체크
- Hooks는 “이벤트(시점) → matcher(필터) → command(외부 프로그램) → stdout JSON(판정)“의 4단계로 코드 수정 없이 흐름을 제어한다.
- 판정은 차단(deny/block 또는 exit 2), 맥락 주입(additionalContext), 통과(무출력) 세 갈래이며, 주입된 맥락은 developer 역할 메시지로 다음 모델 입력에 들어간다.
- 지원 이벤트 10종 중 실행되는 핸들러는
command뿐이고, 비관리 훅은 신뢰가 있어야 작동한다.
셀프체크:
- matcher에
Bash라고만 쓰면BashOutput도구에도 걸릴까? (정답: 안 걸린다. 단순 토큰은 완전일치) - 위험 명령을 막고 싶을 때 stdout으로 돌려줄 최소 조건 두 가지는?
- 훅이 모델에게 회사 규칙을 알려주려면 어떤 필드를 쓰고, 그 텍스트는 어떤 역할의 메시지가 되나?
연결
[!tip] Codex 교차검증(소스 근거) 본 노트의 동작 기술은 아래 소스 파일에서 확인된 것이다.
- 선언 발견/스키마:
hooks/src/engine/discovery.rs,config/src/hook_config.rs,hooks/src/declarations.rs- 이벤트/핸들러:
hooks/src/events/mod.rs,hooks/src/events/common.rs(matcher 규칙),hooks/src/engine/dispatcher.rs(select_handlers/execute_handlers동시실행)- 실행/출력:
hooks/src/engine/command_runner.rs(쉘$SHELL -lc, stdin 주입,timeout_sec/kill_on_drop),hooks/src/engine/output_parser.rs- 런타임 트리거:
core/src/hook_runtime.rs(run_pre_tool_use_hooks등 턴 생명주기 호출)- 맥락 주입:
core/src/context/hook_additional_context.rs(HookAdditionalContextfragment,role()="developer", marker 빈 문자열 → developer 대화 아이템으로record_conversation_items기록)- 공식문서 원문:
/mnt/d/6study/10_프레임워크분석/_원문아카이브/codex/50_hooks.md