Skip to content

feat(codex): mirror Phase 1 hooks to ~/.codex via curated allowlist (ADR-182)#374

Merged
notque merged 2 commits intomainfrom
feat/adr-182-codex-hooks-mirror
Apr 11, 2026
Merged

feat(codex): mirror Phase 1 hooks to ~/.codex via curated allowlist (ADR-182)#374
notque merged 2 commits intomainfrom
feat/adr-182-codex-hooks-mirror

Conversation

@notque
Copy link
Copy Markdown
Owner

@notque notque commented Apr 11, 2026

Summary

Extend install.sh to mirror a curated allowlist of 6 Phase 1 hooks to ~/.codex/hooks/, generate ~/.codex/hooks.json from the allowlist, and set the codex_hooks feature flag in ~/.codex/config.toml. This gives Codex CLI sessions the same session-start injection and Bash-scanning governance that Claude Code sessions already have.

Implements ADR-182. The ADR (adr/182-codex-hooks-mirror.md, local only) was revised mid-implementation after an audit revealed the original draft Phase 1 list had misclassified 3 Edit/Write interceptors as safe. The corrected allowlist and the regression guard test catch this class of error automatically going forward.

Why a curated allowlist, not wholesale mirror

OpenAI Codex CLI v0.114.0 added experimental hook support with a schema nearly identical to Claude Code's, BUT openai/codex#16732 (still open as of April 2026) limits PreToolUse/PostToolUse hooks to fire only for the Bash tool. Tool calls through apply_patch, Write, Edit, MCP, and WebSearch do not trigger hooks.

A wholesale mirror of hooks/*.py would register every Edit/Write interceptor on Codex, where they would never fire. Silently-registered-but-never-invoked hooks are worse than missing hooks because users assume the hook is protecting them. The allowlist approach is explicit inclusion with a regression guard to prevent accidental Phase 2 promotion.

Phase 1 hooks (shipping now)

Event Hook Matcher
SessionStart kairos-briefing-injector.py startup|resume
SessionStart operator-context-detector.py startup|resume
SessionStart team-config-loader.py startup|resume
SessionStart rules-distill-injector.py startup|resume
Stop session-learning-recorder.py (none)
PostToolUse posttool-bash-injection-scan.py Bash

Phase 2 hooks (deliberately excluded)

Edit/Write interceptors: adr-enforcement.py, pretool-config-protection.py, creation-protocol-enforcer.py, posttool-rename-sweep.py, pretool-plan-gate.py, pretool-unified-gate.py, pretool-adr-creation-gate.py, reference-loading-enforcer.py, reference-loading-gate.py, plus three hooks the ADR draft misclassified (pretool-prompt-injection-scanner.py, suggest-compact.py, sql-injection-detector.py) which are actually PreToolUse/PostToolUse with Write|Edit matchers.

Phase 2 unblocks when openai/codex#16732 ships upstream.

What's in this PR

New files:

  • scripts/codex-hooks-allowlist.txt (authoritative Phase 1 list with exclusion rationale as comments)
  • scripts/generate-codex-hooks-json.py (allowlist to hooks.json generator, stdlib only)
  • scripts/ensure-codex-feature-flag.py (TOML-aware config.toml merger with backup)

New tests (50 passing + 1 intentional skip):

  • scripts/tests/test_generate_codex_hooks_json.py (16 tests: schema, CLI, determinism, event ordering, malformed input)
  • scripts/tests/test_ensure_codex_feature_flag.py (21 tests: TOML merge, idempotency, codex_hooks=false error path, preservation of existing sections)
  • scripts/tests/test_codex_hooks_allowlist.py (6 tests: regression guard against Phase 2 promotion. Scans hook source for Edit|Write|apply_patch matcher patterns; tests that Phase 2 hook filenames from the ADR are NOT in the allowlist)
  • scripts/tests/test_codex_hooks_install.py (7 tests + 1 skip: end-to-end install.sh verification against a tempdir HOME; mirrors files, generates valid hooks.json, sets feature flag, preserves existing config sections, uninstall symmetry)

Modified:

  • install.sh (+129 lines: CODEX_HOOKS_DIR variable, Syncing Codex hooks mirror block, uninstall cleanup, Codex version warning for <0.114.0)
  • README.md (+24 lines: new ## Codex CLI Parity section explaining what mirrors, what does not, and the #16732 upstream blocker)

Test plan

  • pytest scripts/tests/test_codex_hooks_*.py scripts/tests/test_generate_codex_hooks_json.py scripts/tests/test_ensure_codex_feature_flag.py -v (50 passed, 1 skipped in 43s)
  • ruff check and ruff format --check on all new Python files (clean)
  • bash -n install.sh (syntax OK)
  • bash install.sh --dry-run --symlink (prints correct Codex hooks section, no side effects)
  • Tempdir end-to-end: HOME=\$TMP bash install.sh --copy --force populates ~/.codex/hooks/ with all 6 Phase 1 files plus hooks/lib/, generates valid hooks.json with correct matchers, sets [features] codex_hooks = true in config.toml while preserving existing sections
  • Tempdir uninstall: bash install.sh --uninstall removes ~/.codex/hooks/, archives hooks.json with timestamp, leaves config.toml feature flag untouched
  • Regression guard test fails if any Phase 1 allowlisted hook contains Edit/Write matcher patterns
  • Allowlist format test fails on duplicate entries, missing hooks, wrong regex

References

notque added 2 commits April 11, 2026 12:22
…ADR-182)

Extend install.sh to mirror a curated allowlist of 6 hooks to ~/.codex/hooks/,
generate ~/.codex/hooks.json from the allowlist, and set the codex_hooks
feature flag in ~/.codex/config.toml.

Phase 1 hooks (functional + Codex-compatible, verified against source):
  SessionStart: kairos-briefing-injector, operator-context-detector,
                team-config-loader, rules-distill-injector
  Stop:         session-learning-recorder
  PostToolUse:  posttool-bash-injection-scan (matcher=Bash)

Phase 2 hooks (Edit/Write interceptors) deliberately excluded until
openai/codex#16732 ships upstream. Codex PreToolUse/PostToolUse currently
fire only for the Bash tool; a silently-registered hook is worse than a
missing one because users assume it is protecting them.

New scripts:
  scripts/codex-hooks-allowlist.txt     authoritative Phase 1 list
  scripts/generate-codex-hooks-json.py  allowlist to hooks.json generator
  scripts/ensure-codex-feature-flag.py  TOML-aware config.toml merger

New tests (50 passing, 1 intentional skip):
  test_generate_codex_hooks_json.py  16 tests  schema + CLI
  test_ensure_codex_feature_flag.py  21 tests  TOML merge + idempotency
  test_codex_hooks_allowlist.py       6 tests  regression guard against
                                               Phase 2 promotion
  test_codex_hooks_install.py         7 + 1s   end-to-end install.sh
                                               verification in tempdir HOME

README.md gets a Codex CLI Parity section documenting what mirrors, what
does not, and the openai/codex#16732 upstream blocker.

Verified: install.sh tempdir run populates ~/.codex/hooks/ with 6 Phase 1
hooks plus hooks/lib/, generates valid hooks.json with correct event
grouping (SessionStart matcher startup|resume, PostToolUse matcher Bash),
and sets config.toml feature flag while preserving existing [projects.*],
[plugins.*], and [notice] sections.
The test (3.10) job was failing because tomllib is stdlib only in Python
3.11+. Three changes:

1. scripts/ensure-codex-feature-flag.py: remove the unused `import tomllib`.
   The script uses pure string/regex operations for both read and write;
   the import was dead code.

2. scripts/tests/test_ensure_codex_feature_flag.py: wrap the tomllib import
   in try/except and define a `requires_tomllib` skip marker. Apply the
   marker to the 9 tests that parse output with tomllib.loads(). The 12
   tests that verify the write logic via string/regex checks run on 3.10+
   unchanged.

3. scripts/tests/test_codex_hooks_install.py: same pattern. Apply
   `requires_tomllib` to the 3 tests that parse the post-install
   config.toml. The 4 tests that verify files exist, hooks.json shape,
   and dry-run behavior run on 3.10+ unchanged.

Verified locally by simulating the 3.10 ImportError via sys.modules
manipulation: test modules collect and import successfully, decorated
tests are correctly skipped on 3.10 while non-decorated tests still run.
On Python 3.13 the full suite still passes (50 passed, 1 intentional skip).
@notque notque merged commit 2d1e918 into main Apr 11, 2026
4 checks passed
@notque notque deleted the feat/adr-182-codex-hooks-mirror branch April 11, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant