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

Open 💬 5 comments Opened Jun 4, 2026 by MoRezaeirad

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 ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗