지식위키

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라는 일꾼이 집안일을 하다가 문을 지날 때마다 센서가 작동하고, 미리 정해 둔 규칙대로 문을 열어주거나 막아 선다.

  1. 사건이 발생한다. Codex가 일하다 정해진 순간에 도달한다. 예를 들어 “Bash 명령을 실행하기 직전”, “도구 작업을 끝낸 직후”, “세션이 막 시작될 때” 같은 시점이다. 이런 시점을 이벤트라고 부른다.

  2. 누구한테 반응할지 고른다(matcher). 모든 도구에 다 반응하면 시끄럽다. 그래서 “Bash 도구일 때만”, “Edit이나 Write일 때만”처럼 matcher라는 필터로 대상을 고른다. CCTV를 현관에만 달지, 모든 방에 달지를 정하는 것과 같다.

  3. 내가 등록한 프로그램이 실행된다(command). 조건이 맞으면 Codex가 내 스크립트(쉘 명령, 파이썬 파일 등)를 자동으로 띄운다. 이때 Codex는 “지금 무슨 일이 벌어졌는지”를 JSON 쪽지에 적어 stdin(표준입력)으로 건네준다. 어떤 도구인지, 어떤 명령을 쓰려는지 등이 그 쪽지에 담긴다.

  4. 내 프로그램이 판정을 돌려준다(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처럼 영숫자·_·|만 있으면 완전일치 (부분일치 아님 — BashBashOutput에 안 걸림)
  • ^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 되어야 실행된다. 기업 관리 훅만 신뢰 없이 돈다.
  • 핸들러 typecommand/prompt/agent 세 가지로 파싱되지만, 현재 실제로 실행되는 건 command이다(prompt/agent는 파싱만).
  • async 옵션은 파싱되지만 아직 미지원이라 스킵된다.

실제 예시

정확한 선언 스키마 (참고용)

이벤트 키 값은 MatcherGroup의 배열이고, 각 그룹은 matcher 하나 + hooks(핸들러) 배열을 갖는다(config/src/hook_config.rs).

필드위치의미
matcherMatcherGroup정규식/완전일치 문자열. "*"·""·생략 = 전부
hooks[].typeHookHandlerConfigcommand(실행됨)/prompt/agent
hooks[].commandHookHandlerConfig실행할 쉘 명령(stdin으로 JSON 받음)
hooks[].timeoutHookHandlerConfig타임아웃(초, 필드명 timeout_sec). 초과 시 실패

훅이 stdout으로 돌려주는 JSON의 핵심 효과(engine/output_parser.rs):

이벤트차단 신호컨텍스트 주입
PreToolUsepermissionDecision:"deny"+permissionDecisionReason, 또는 레거시 decision:"block"+reason, 또는 exit 2 + stderrhookSpecificOutput.additionalContext
PostToolUsedecision:"block"+reason(비면 무효)hookSpecificOutput.additionalContext
SessionStart / SubagentStart(차단 불가)additionalContext + plain stdout
UserPromptSubmitdecision:"block"+reasonadditionalContext

[!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: 단순 토큰=완전일치(BashBashOutput), 정규식 쓰려면 ^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뿐이고, 비관리 훅은 신뢰가 있어야 작동한다.

셀프체크:

  1. matcher에 Bash라고만 쓰면 BashOutput 도구에도 걸릴까? (정답: 안 걸린다. 단순 토큰은 완전일치)
  2. 위험 명령을 막고 싶을 때 stdout으로 돌려줄 최소 조건 두 가지는?
  3. 훅이 모델에게 회사 규칙을 알려주려면 어떤 필드를 쓰고, 그 텍스트는 어떤 역할의 메시지가 되나?

연결

CX_개요 · _분석축_루브릭

[!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(HookAdditionalContext fragment, role()="developer", marker 빈 문자열 → developer 대화 아이템으로 record_conversation_items 기록)
  • 공식문서 원문: /mnt/d/6study/10_프레임워크분석/_원문아카이브/codex/50_hooks.md