codex exec still does not dispatch hooks with valid hooks.json shape
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.0codex-cli 0.138.0-alpha.2- Linux x86_64
hooks.jsonunder an isolated temporaryCODEX_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 execdoes 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.
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗