codex exec still does not dispatch hooks with valid hooks.json shape

Resolved 💬 5 comments Opened Jun 4, 2026 by MoRezaeirad Closed Aug 6, 2026
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

This is a follow-up to #25875, filed as a new issue because #25875 was closed as completed and I cannot reopen it.

The original issue body in #25875 used the wrong top-level hooks.json shape:

{ "SessionStart": [...] }

A maintainer correctly pointed out that the supported shape is:

{ "hooks": { "SessionStart": [...] } }

That part is confirmed. #26426 is useful because it adds a parse warning for the malformed top-level shape.

However, after rerunning with the supported top-level hooks object, I still reproduce the original codex exec behavior: commands execute, but no hooks are dispatched. Deny hooks also do not block command execution.

Test matrix

| Version | Config | Mode | Result |
| --------------------------- | ------------ | ---- | ------------------------------------- |
| codex-cli 0.137.0 | hooks.json | log | hook_count=0, command_reached=yes |
| codex-cli 0.137.0 | hooks.json | deny | hook_count=0, command_reached=yes |
| codex-cli 0.138.0-alpha.2 | hooks.json | log | hook_count=0, command_reached=yes |
| codex-cli 0.138.0-alpha.2 | hooks.json | deny | hook_count=0, command_reached=yes |

Why this is a new issue

#25875 was closed after identifying the malformed JSON shape in the original report. That malformed-shape issue is real and separate.

The remaining issue is different: even with a valid hook configuration using the supported { "hooks": { ... } } top-level shape, codex exec still appears not to dispatch hooks. Since I cannot reopen #25875, I am filing this clean follow-up with the corrected config shape and the current failing matrix.

Environment

Observed with:

  • codex-cli 0.137.0
  • codex-cli 0.138.0-alpha.2
  • Linux x86_64
  • hooks.json under an isolated temporary CODEX_HOME
  • [features].hooks = true
  • trusted project config
  • --dangerously-bypass-hook-trust

Minimal reproduction

This uses a temporary CODEX_HOME, writes a valid top-level hooks object, trusts the temporary working directory, bypasses hook trust, and asks codex exec to run one command.

Run with default MODE=log, then run again with MODE=deny.

set -eu

MODE="${MODE:-log}"
tmp="$(mktemp -d)"
codex_home="$tmp/codex-home"
work="$tmp/work"
marker="$tmp/hook-fired"

mkdir -p "$codex_home" "$work"
export MARKER="$marker"

cat > "$codex_home/config.toml" <<CONFIG
[features]
hooks = true

[projects."$work"]
trust_level = "trusted"
CONFIG

if [ "$MODE" = "deny" ]; then
  cat > "$codex_home/hooks.json" <<'HOOKS'
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "sh -c 'printf SessionStart >> \"$MARKER\"'" }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "sh -c 'printf UserPromptSubmit >> \"$MARKER\"'" }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          { "type": "command", "command": "sh -c 'printf PreToolUse >> \"$MARKER\"; printf %s \"{\\\"decision\\\":\\\"deny\\\",\\\"reason\\\":\\\"probe deny\\\"}\"'" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          { "type": "command", "command": "sh -c 'printf PostToolUse >> \"$MARKER\"'" }
        ]
      }
    ]
  }
}
HOOKS
else
  cat > "$codex_home/hooks.json" <<'HOOKS'
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "sh -c 'printf SessionStart >> \"$MARKER\"'" }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "sh -c 'printf UserPromptSubmit >> \"$MARKER\"'" }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          { "type": "command", "command": "sh -c 'printf PreToolUse >> \"$MARKER\"'" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          { "type": "command", "command": "sh -c 'printf PostToolUse >> \"$MARKER\"'" }
        ]
      }
    ]
  }
}
HOOKS
fi

cd "$work"
CODEX_HOME="$codex_home" HOME="$codex_home" \
  codex exec --sandbox workspace-write --dangerously-bypass-hook-trust \
  "run exactly: /bin/bash -lc 'printf command-ran > .command-ran'" \
  < /dev/null

hook_count=0
if [ -f "$marker" ]; then
  hook_count="$(wc -c < "$marker")"
fi

command_reached=no
if [ -f .command-ran ]; then
  command_reached=yes
fi

printf 'hook_count=%s command_reached=%s\n' "$hook_count" "$command_reached"

Actual behavior

For both log mode and deny mode:

hook_count=0 command_reached=yes

The command reaches execution, but no SessionStart, UserPromptSubmit, PreToolUse, or PostToolUse hook writes the marker file.

In deny mode, the PreToolUse hook should return a deny decision if invoked, but command execution still reaches .command-ran, which indicates the hook did not run.

Expected behavior

With a valid top-level hooks object, hooks enabled, trusted project config, and --dangerously-bypass-hook-trust:

  • at least one configured hook should be dispatched during codex exec, or
  • the CLI/docs should explicitly state that codex exec does not support user hook dispatch.

For deny mode specifically, if PreToolUse is dispatched and returns a deny decision, command execution should not reach the shell command.

Notes

This does not appear to be the malformed-config problem from the original issue. The corrected config shape still produces zero hook dispatch under codex exec.

View original on GitHub ↗

5 Comments

github-actions[bot] contributor · 2 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #26383
  • #25875

Powered by Codex Action

jsai-xfab · 2 months ago

Can confirm I also se this bug. A timeline on the fix would he helpful.

nodeax · 2 months ago

I can confirm I see this issue even on 0.139.0

anandgupta42 · 2 months ago

Independent repro + root-cause corroboration on codex-cli 0.139.0 (macOS, npx):

Probed exhaustively — hooks never dispatch under codex exec in any of these combinations:

  • project .codex/hooks.json and user $CODEX_HOME/hooks.json
  • both file shapes (top-level events and {"hooks": {…}})
  • match-all and Bash matchers
  • -c features.hooks=true (no-op — the feature is already stable/default-on)
  • -c features.unified_exec=false (irrelevant — PreToolUse dispatch precedes handler choice)
  • --dangerously-bypass-hook-trust — prints its warning, still zero hooks fire, including SessionStart/UserPromptSubmit/Stop from user-level hooks.json, which rules out matcher/tool-path explanations

Reading the source at rust-v0.139.0, the behavior matches exactly what #26434 (draft) fixes: exec applies bypass_hook_trust to its local Config (which is what prints the warning), but starts its thread via the app-server with config: None (codex-rs/exec/src/lib.rs ~1052 / ~1077, same at current main), and the app-server rebuilds the thread's Config — so the bypass never reaches hook discovery, where enabled-but-untrusted handlers are dropped (codex-rs/hooks/src/engine/discovery.rs ~520). The TUI forwards bypass_hook_trust in its thread-start override map (codex-rs/tui/src/app_server_session.rs ~1302), which appears to be the entire interactive/headless divergence.

Two consequences worth calling out beyond the one-line fix:

  1. The skip is completely silent. Enabled-but-untrusted hooks produce no stderr warning in headless runs, so automation authors have no signal that their hooks were dropped.
  2. It fails open. A PreToolUse deny hook does not block the command under exec — anyone using deny hooks as a guardrail in CI isn't getting one.

Workaround until #26434 lands (per the trust model — discovery filtering is the only gate): trust the hooks once in the interactive TUI so the trusted hash persists to user config, and ensure the project layer is trusted; exec should then dispatch them.

abhinav-oai contributor · 1 month ago

Fixed by #26434, which forwards bypass_hook_trust through codex exec thread start and resume instead of losing it when app-server rebuilds the thread config. That PR also added start/resume coverage and an end-to-end codex exec hook regression test; the focused tests pass on current main. Closing as fixed.