Skip to content

fix(hooks): speak Codex 0.155's hook protocol - #30

Merged
KageBinary merged 9 commits into
mainfrom
fix/host-protocol
Oct 2, 2026
Merged

KageBinary merged 9 commits into
mainfrom
fix/host-protocol

Conversation

@KageBinary

@KageBinary KageBinary commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Checked against Codex 0.155.1 (the binary, and openai/codex at rust-v0.155.1):

  • Context only reached the user. pre_tool_use.py returned its context as systemMessage. In hooks/src/events/*.rs parse_completed, systemMessage only becomes a UI warning; hookSpecificOutput.additionalContext is what reaches the model. The output structs also reject unknown fields.
  • Most edits weren't seen. Codex edits files with apply_patch (core/src/tools/hook_names.rs), but the hooks only matched Bash.
  • The installer wrote an obsolete flag. codex_hooks is a deprecated alias of hooks (features/src/legacy.rs), which has been stable and on by default since 0.124 (Mark codex_hooks stable openai/codex#19012).
  • Hooks ran in a login shell. hooks.json used /bin/sh -lc, which sources the user's login profile on every hook. On the machine this was built on, the profile has a syntax error under sh, so no hook could run at all. Codex itself runs hooks with <shell> -c (session/mod.rs build_hooks_config).

What changes

  • One model_context() helper builds every hook's output, so model context goes in hookSpecificOutput.additionalContext. Stop has no model channel and keeps {"continue": true}.
  • apply_patch:
    • Pre and post hooks match Bash|apply_patch.
    • Files are read from the patch's Add/Update/Delete File and Move to headers, the same grammar as apply-patch/src/parser.rs. The patch arrives as tool_input.command.
    • The pre-edit warning runs ix impact on the first 3 files in parallel.
  • The installer no longer writes config.toml. It only warns if hooks are turned off there. .codex/config.toml, the TOML editor and its tests are removed.
  • Hooks run with /bin/sh -c.
  • Trust: per hooks/src/engine/discovery.rs, hooks in ~/.codex/hooks.json are skipped until the user trusts them (trusted_hash). Changing hooks.json, as this PR does, means users must review them again. The installer now prints a notice. hooks.md explains what moving to plugin-bundled hooks would take; those need the same trust step.
  • Version 2.4.3.

Tests

  • unittest: 111 → 112 passing (1 Windows-only skip): 19 TOML tests removed, 20 added.
  • The new tests/test_codex_hook_protocol.py checks hook output against Codex's own schemas, copied into tests/fixtures/codex-0.155.1/. Against the old hooks it gives 8 failures and 5 errors, including the old systemMessage shape.
  • test-local.sh and the header check pass.
  • Smoke run: a temp CODEX_HOME, a local mock Responses server (no real credentials) and a strict fake ix. With the hooks untrusted, codex exec skipped every one: the fake ix was never called. That confirms the trust step in practice. A positive run, with hook context actually reaching a model, wasn't done: it needs --dangerously-bypass-hook-trust or a real trust entry.

Notes

Not fixed here:

  • On Windows, the "python" "launcher" name hook command likely fails under PowerShell (-NoProfile -Command). commandWindows with & would fix it, but it's untested.
  • The SessionStart matcher startup|resume misses clear, compact and fork.
  • A file written from a subdirectory reaches ix impact with a path relative to that subdirectory, not to the workspace root.

🤖 Generated with Claude Code

KageBinary and others added 9 commits October 1, 2026 13:22
find_workspace_root stopped at the first `.codex/hooks.json` above the
session's cwd, and a `--home` install puts one in ~/.codex -- so with the
default install every hook resolved to $HOME, and the Stop hook ran
`ix map` there on every turn. It now prefers the git root of the payload's
cwd and never returns $HOME.

Every automatic map goes through one guard: a git repository that is not
$HOME, already mapped (`ix status --format json --root` says
graphCompleted: true), outside a per-root debounce window; then exactly
`ix map <root> --silent` from the root with IX_AUTO_MAP=1, detached.
PostToolUse no longer runs `ix map <file>`, which the CLI rejects
("Map path is not a directory"); a write requests the guarded root map.

- drop `--limit` from the intercepted `ix locate` (no such option)
- delete the /v2 runtime client (call_runtime, get_runtime, /v2/ix_query,
  /v2/ingest/map) and its secret scrubber; no Ix release serves /v2
- caches move from the shared $TMPDIR/ix-codex-hooks to a per-user state
  dir (XDG_STATE_HOME / LOCALAPPDATA), created 0700, ownership-checked,
  written atomically and never allowed to fail a hook; the briefing
  window is per project
- the Stop hook is now one `git rev-parse` plus one `ix status` (3 s + 4 s
  bounds) inside its 10 s timeout, instead of up to ~15 s
- tests: strict fake ix (rejects map <file>, locate --limit,
  smells --path, unknown commands) and the guard matrix; test-local.sh
  dry runs use a fake ix on PATH instead of the real backend

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- ix-bug-investigator: `ix bugs` does not exist; use
  `ix bug list --format text` and say it is Ix Pro only
- hooks.md / README / AGENTS.md: describe the guarded automatic map
  instead of a per-file `ix map` and an unconditional one at Stop
- PLUGIN_SPEC: mark the /v2 runtime migration as abandoned
- ci.yml: the comments described an MCP server this repo no longer ships

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- PreToolUse printed a top-level systemMessage. Codex turns that into a
  UI warning and never adds it to the model's input; only
  hookSpecificOutput.additionalContext reaches the model
  (codex-rs/hooks/src/events/pre_tool_use.rs parse_completed,
  rust-v0.155.1). Every hook that talks to the model now goes through
  one helper, model_context(), which emits that shape.
- PreToolUse and PostToolUse matched Bash only, but Codex edits files
  with apply_patch (core/src/tools/hook_names.rs; Edit/Write are matcher
  aliases) and passes the raw patch as tool_input.command
  (core/src/tools/handlers/apply_patch.rs). Both now match
  "Bash|apply_patch". The touched files are read from the patch's
  Add/Update/Delete File and Move to headers
  (apply-patch/src/parser.rs grammar). Pre-edit runs ix impact on up to
  3 of those files in parallel.
- hooks.json ran each hook through "/bin/sh -lc". Codex already runs
  the command with the session shell and no login profile
  (session/mod.rs build_hooks_config, use_login_shell=false), so the
  inner -l was the only thing sourcing the profile. On this machine
  "/bin/sh -lc" aborts in /etc/profile before the hook starts. It is now
  "/bin/sh -c".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`codex features list` (0.155.1) shows `hooks` as stable and on by
default. That has been true since 0.124 (openai/codex#19012).
`codex_hooks` is now a legacy alias in codex-rs/features/src/legacy.rs,
which Codex reports as deprecated. The installer no longer creates or
edits config.toml. It only reads config.toml to warn when
`[features] hooks` or `codex_hooks` is false. The repo's
.codex/config.toml, whose only content was the flag, is removed. So
are the TOML line editor and its suite, which existed only to write
that flag.

It now also tells the user about hook trust. Codex skips non-managed
hooks until their exact definition is reviewed and trusted
(hooks/src/engine/discovery.rs hook_trust_status), and `codex exec`
skips them outright.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The suites asserted whatever shape the hooks printed, so a PreToolUse
systemMessage that never reached the model passed. The new suite
validates each hook's stdout against the output schemas generated by
openai/codex@rust-v0.155.1, vendored under
tests/fixtures/codex-0.155.1/. It also checks that the text meant for
the model is in additionalContext, which is where Codex reads it. The
suite also covers:

- apply_patch path parsing
- the hooks.json matchers, under Codex's exact-matcher rules
- the non-login command, run with a profile that prints
- the installer leaving config.toml alone

Against the previous tree the suite fails with 8 failures and 5 errors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
hooks.md records what was verified against codex 0.155.1:
- the model-context channel
- tool names and apply_patch
- the non-login shell
- the feature flag
- hook trust
It also sets out what a move to plugin-bundled hooks would involve.
README and AGENTS now cover apply_patch, the trust prompt and the
dropped flag.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
main's tree is identical to the merged sweep head 604cc65, which this branch
already contains, so this branch's tree is kept unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@KageBinary
KageBinary changed the base branch from fix/ix-cli-sweep to main October 1, 2026 23:47
@KageBinary KageBinary closed this Oct 1, 2026
@KageBinary KageBinary reopened this Oct 1, 2026
@KageBinary
KageBinary merged commit 2cce8b5 into main Oct 2, 2026
5 checks passed
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