You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Hooks] Add opt-in fail-closed handling for PreToolUse hook failures #41979
Codex CLI and app-server. The behavior was characterized against rust-v0.148.0 at commit 3ba0f711642a888aec92a611a3f3b2211157ff89.
What feature would you like to see?
Add an opt-in fail-closed policy for PreToolUse command hooks, parallel to the UserPromptSubmit proposal in #33630.
Today a PreToolUse hook can block through a valid exit-2 or permissionDecision: "deny" result, but a timeout, spawn failure, crash/non-protocol exit, or malformed output is treated as a failed hook with should_block=false, so the tool is dispatched. That makes a policy hook least reliable precisely when its process is unavailable.
Omitted or continue preserves the existing behavior.
block prevents tool dispatch when the hook times out, cannot be spawned, crashes or exits without a valid protocol result, emits malformed output, or encounters event-serialization failure.
The user receives a redacted blocking diagnostic that contains no command, tool-input, or hook-payload values.
The setting participates in hook trust/config hashing, and effective-config reads expose the resolved value to app-server/Desktop consumers.
Tests prove every failure class blocks before tool execution while valid allow and deny results remain unchanged.
This is the PreToolUse counterpart to #33630; it does not require changing the compatibility default for advisory hooks.
Status update: OpenClaw delivery is proceeding against released Codex contracts. The local custom-contract prototype described below is abandoned for this fix and is not a merge dependency. The original source findings and earlier prototype test limitations remain recorded below.
This also affects synchronous mcp_tool policy hooks, and the same opt-in needs to cover PermissionRequest to preserve approval precedence. Context: openclaw/openclaw#91009.
I inspected rust-v0.153.4 and main source frozen at 36f0dbe796d9bb1a18a0fc0640ed08b3e1d54564. At that unpatched main snapshot:
PreToolUse records runtime errors as Failed while leaving should_block = false.
PermissionRequest similarly leaves decision = None. The approval router then invokes its reviewer, which can approve. Enforcing only PreToolUse does not cover an outage between the two hook phases.
For an MCP reproduction, enable features.hooks and explicitly trust the exact handler below using the normal hook trust flow. This is the hooks configuration fragment:
Leave policy unconfigured, then request a shell command that writes a marker. That unpatched snapshot treats the failed hook as non-blocking. For the PermissionRequest counterpart, use an action that actually enters approval and a reviewer that otherwise approves; full-access settings alone can bypass PermissionRequest and would be a misleading test.
The narrow contract I prototyped is:
Optional on_error: "block" on synchronous command/MCP handlers for these two events; omission or continue retains existing behavior.
Successful required evaluation needs canonical JSON with the matching hookSpecificOutput.hookEventName. An explicit envelope with no decision acknowledges successful evaluation and retains ordinary approval handling. PreToolUse retains deny with a nonempty reason and allow with updatedInput; this does not add ask or bare allow. PermissionRequest retains its canonical allow/deny decisions, including the existing default denial message.
Runtime, timeout, MCP isError, empty/plain-text, malformed, mismatched, or semantically invalid output blocks PreToolUse or produces an explicit PermissionRequest deny. Deny must win over another matching hook's allow. Cancellation must not dispatch the original tool.
Preserve enabled/trusted/managed eligibility and include the option in the trust hash. This is an execution policy for supported handlers, not a general change to unsupported/malformed configuration handling.
Embedding clients need an additive initialize capability before relying on this setting, because older binaries can ignore the unknown field. The prototype calls it supportsFailClosedPermissionHooks; absent means unsupported. A ${$} MCP template forwards the complete event, including optional subagent fields, without recursively expanding payload strings.
Validation so far is against earlier local prototype revisions on that frozen main source, not an official Codex release: 506 hooks/config tests passed; a core approval test proved required failure prevents full-access reviewer fallback; a test using a real MCP connection to a synthetic server completed a subsequent call on the same connection and confirmed the cancelled command never wrote its marker. The native CLI built and its initialize response advertised the proposed capability.
The latest prototype remains unqualified. The full native MCP dispatch matrices needed a test diagnostic correction because public hook events retain only the outer MCP error, and the corrected rerun was interrupted by validation infrastructure. The corrected fixtures use otherwise-valid delayed acknowledgments/error results plus dispatch markers. Canonical generated schemas and the broader workspace suite are also pending. No official release support or complete integration qualification is claimed.
Independent confirmation on codex-cli 0.153.3 (official release tarball, Linux x86_64), with a minimal repro that isolates the malformed-output sub-case.
Setup. A project-level hooks.json registering a synchronous PreToolUse handler for ^Bash$:
Trust the hook through the ordinary /hooks review flow; codex exec runs with the hook active.
The hook binary is a small stdin/stdout JSON filter. In the failing configuration it mis-decodes the request envelope and exits with status 2without writing a decision to stdout (it writes a fixed diagnostic to stderr).
Drive one turn whose Bash step writes a sentinel file (e.g. printf data > /tmp/out.txt).
Observed on 0.153.3:
The hook process exits 2.
Its stderr diagnostic is surfaced on the codex exec error surface prefixed with Command blocked by PreToolUse hook: — so the operator-visible text looks like a block.
Despite that, the command executes: the turn completes with a command_execution JSONL item and the sentinel file is created. codex exec exits 0.
So in this state the hook is worse than absent: the error text reads as enforcement to anyone watching the output, while the tool ran. We only noticed because the absence of the expected filesystem side-effect assertion failed on our side after we fixed the decoder — before the fix, every request from our hook failed decode and nothing was ever actually blocked.
After fixing the hook (well-formed JSON decision on stdout, exit 0): denials are enforced correctly (no command_execution item for the turn, command not executed), so the happy path works fine on this version.
Ask. As proposed in this issue: an opt-in per-hook (or global) failureMode: block so a crashing/malformed/failing hook fails closed, plus — if possible — distinguishing the surfaced text for "hook blocked the command" vs "hook itself failed" (in our repro both render as Command blocked by PreToolUse hook: ..., which actively misleads during diagnosis).
Happy to provide the hook filter script or extra traces if useful.
Addendum: one-shot reproduction script
A self-contained shell script reproduces the issue with no dependencies beyond codex itself. It creates a minimal project whose single PreToolUse handler for ^Bash$ always fails (reads stdin, writes a diagnostic to stderr, exits 2, no decision on stdout), runs codex exec asking codex to create a sentinel file, then reports which failure mode occurred:
fail-open (the bug): the sentinel file exists and codex exits 0, while the output misleadingly shows Command blocked by PreToolUse hook: policy-hook: could not parse request envelope
fail-closed (expected): the sentinel file is absent and/or the exit is nonzero
The script (save as run-repro.sh, chmod +x, run it in an empty directory; tested on codex-cli 0.153.3, official release tarball, Linux x86_64). NOTE: it is interactive by necessity — newly written hooks start Untrusted and never run until trusted through the ordinary /hooks flow, so the script pauses once for you to trust the hook (using --dangerously-bypass-hook-trust instead would change what is being tested and is deliberately not used):
#!/usr/bin/env bash# One-shot reproduction: sets up a codex project whose PreToolUse hook# FAILS (exit 2, no decision on stdout) for every Bash call, then asks# codex to touch a sentinel file. After the run:# - If hooks fail CLOSED: proof.txt absent, run fails/blocked.# - If hooks fail OPEN: proof.txt EXISTS and codex exits 0 while the# output misleadingly says "Command blocked by# PreToolUse hook: ..." — this is the bug.## TRUST: new hooks start Untrusted and will NOT run until trusted. The# script therefore (a) writes the hook files, then (b) runs `codex exec`# interactively ONCE so you can accept the hook via the /hooks flow when# prompted (or open /hooks and trust the handler), and only then (c) runs# the actual failing-hook scenario. A fully automatic repro is impossible without# either the interactive trust step or --dangerously-bypass-hook-trust# (which would change what is being tested — do not use it here).set -uo pipefail
cd"$(dirname "$0")"
cat > failing-hook.sh <<'HOOK'#!/usr/bin/env bashread -r _ # consume the JSON envelope from stdin (do not parse it)echo "policy-hook: could not parse request envelope" >&2exit 2HOOK
chmod +x failing-hook.sh
cat > hooks.json <<'HOOK'{ "hooks": { "PreToolUse": [ { "matcher": "^Bash$", "hooks": [ { "type": "command", "command": "./failing-hook.sh", "timeout": 10, "mode": "sync" } ] } ] }}HOOKecho"Step 1/3: hook files written."echo"Step 2/3: trust the hook (required — untrusted hooks never run)."echo" Easiest: run: codex exec --search # any harmless command, then"echo" follow the trust prompt, OR open the TUI and use /hooks to trust"echo" './failing-hook.sh'. Alternatively run this script's step 3 with"echo" --dangerously-bypass-hook-trust ONLY if you accept that caveat."echoread -r -p "Press Enter after the hook shows as Trusted in /hooks..."
rm -f proof.txt
echo"Step 3/3: running codex exec with the FAILING hook active."
codex exec"run this exact bash command: printf hello > proof.txt"
status=$?echoecho"=== codex exec exit status: $status"if [ -f proof.txt ];thenecho"=== RESULT: proof.txt EXISTS -> the command RAN even though the"echo" PreToolUse hook failed (exit 2, no decision). Hooks failed OPEN."elseecho"=== RESULT: proof.txt absent -> the failing hook blocked (fail-closed)."fi
[ "$status"-ne 0 ] &&echo"=== codex exited nonzero ($status), not 0."
The hook deliberately ignores stdin content — any Bash step triggers the same failure path, so nothing about the request matters except that it is a Bash call.
+1, with evidence from codex-cli 0.152.1 on macOS: a PreToolUse command hook that crashed, one that exceeded its timeout, and one that printed output the harness could not parse each surfaced a hook failure, and in all three cases the shell command then ran, its effect was on disk afterwards, and a PostToolUse event followed.
For the shape: a per-entry opt-in that defaults to today's behaviour, as proposed here (failureMode: "block" or on_error: "block"), is exactly what I need. A hook that renders a status line should keep proceeding on failure; a hook that answers deny should be able to say that its silence means no. The PermissionRequest coverage noted above matters for the same reason: an outage between the two phases is otherwise an approval.
Source confirmation for released 0.159.3, plus one coverage requirement for native child dispatch.
At 0.159.3 pre_tool_use.rs, callback failures and malformed results still leave should_block false. No-match and serialization-failure paths also continue. Required managed-hook loading checks startup loading; it does not establish successful runtime validation.
Our requirement: a specifically designated mandatory validator must return a valid allow decision before native child allocation. Missing/unmatched required coverage, an untrusted/unavailable validator, crash, timeout, malformed result, or serialization failure must deny before allocation. Optional/advisory hooks can retain current defaults.
Acceptance needs valid-allow/explicit-deny cases, every required-validator failure class, coverage after configuration reload, and proof that denied calls create no child. A matched-hook failure policy alone would leave absent/mismatched mandatory coverage unresolved.
Evidence limit: inspected released source; earlier 0.159.2 testing confirmed a sampled explicit native denial before child creation. We have not performed live failure injection on 0.159.3.
Is a supported implementation or tracked patch available for this required-validation boundary? Related contract-visibility dependency: #46939.
Feature request: opt-in fail-closed policy for synchronous PreToolUse hook failures
Please support an opt-in policy that denies a pending tool when a synchronous PreToolUse command hook fails. This would let deployments that use hooks for policy enforcement stay on the official CLI instead of maintaining a private patch.
I tested the official Linux x64 Codex CLI 0.163.0. The native ELF SHA-256 was 399ff69c431c066e40de318fba45c2b07f45a76b19939d247ee4828fc70c4215.
With a trusted synchronous hook matching Bash and apply_patch, both a native file patch and a shell request to a loopback HTTP receiver still executed after each of these hook failures:
Hook failure
Native hook status
Tool still executed
One-second timeout
failed
Yes, both tools
Exit code 1
failed
Yes, both tools
Invalid JSON on stdout
failed
Yes, both tools
Missing executable
failed
Yes, both tools
Healthy hooks allowed both tools. A Python missing-script error returned exit code 2 with stderr and denied both tools, consistent with the documented exit-code contract. The twelve-case test used disposable profiles, a loopback mock provider and a loopback destination; no real outbound message or customer request was made.
For example, this registered and trusted hook times out before the tool runs:
After trusting it in a disposable test profile, request a harmless file patch or a request to a local test HTTP receiver. The hook reports hook timed out after 1s, but the native tool still produces its side effect.
I understand that hook errors continuing execution are part of the current documented behavior. The request is for an explicit alternative for synchronous policy hooks, while preserving the existing default and asynchronous hooks' advisory behavior. Under the opt-in policy, timeout, launch failure, nonzero non-deny exit, unusable structured output and pre-dispatch input serialization failure should deny the pending tool. Healthy hooks and explicit valid decisions should retain their current behavior.
+1 from a deployment that runs many concurrent Codex CLI agents behind a local policy hook.
Confirmed on the official 0.163.0 Linux x64 build. With a trusted synchronous PreToolUse command hook, allow and explicit deny (exit 2) both behave correctly. When the hook exceeds its timeout, though, the host marks the hook failed and the original command still runs. A wrapper inside the hook can turn its own child's failures into a deny, but it can't cover the host's own timeout or spawn-failure branch. So there's currently no way to make a policy hook fail closed from the hook side. This is what keeps us from moving Codex onto a consolidated policy hook.
The proposed per-entry failureMode: "block" (default unchanged) covers our need. One addition: once a block-mode hook is loaded, a config reload shouldn't be able to silently remove it or downgrade it to continue mid-session. Either refuse that reload and keep the prior snapshot, or require re-trust. Otherwise a policy guard can disappear from a running session without anyone noticing.
Run: codex exec --json - with the prompt "Run this shell command exactly once: echo probe".
Result: the hook started at second 1792487332. Codex stopped it (hookend was never written) and ran the command at 1792487337.1, about 5 seconds later. The command printed "probe" with exit code 0. The JSON event stream held no hook error or hook event, standard error was empty, and codex exec exited 0. The same run with the plain command "sleep 30" gave the same result (about 5.3 seconds, then the command ran).
Default timeout. A hook with no "timeout" field that slept 1500 seconds was stopped after about 599 seconds, then the Bash command ran with no error. This matches a default of 600 seconds.
So a slow or hung PreToolUse guard becomes a silent allow, and nothing in the event stream shows that the guard never decided. A "failureMode": "block" option as proposed here would close this. Until then, a visible event for a hook timeout would at least make the allow auditable.
Activity
vincentkoc commentedon Sep 13, 2026
Status update: OpenClaw delivery is proceeding against released Codex contracts. The local custom-contract prototype described below is abandoned for this fix and is not a merge dependency. The original source findings and earlier prototype test limitations remain recorded below.
This also affects synchronous
mcp_toolpolicy hooks, and the same opt-in needs to coverPermissionRequestto preserve approval precedence. Context: openclaw/openclaw#91009.I inspected
rust-v0.153.4and main source frozen at36f0dbe796d9bb1a18a0fc0640ed08b3e1d54564. At that unpatched main snapshot:Failedwhile leavingshould_block = false.decision = None. The approval router then invokes its reviewer, which can approve. Enforcing only PreToolUse does not cover an outage between the two hook phases.For an MCP reproduction, enable
features.hooksand explicitly trust the exact handler below using the normal hook trust flow. This is the hooks configuration fragment:{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"mcp_tool","server":"policy","tool":"check","input":{},"timeout":1}]}]}}Leave
policyunconfigured, then request a shell command that writes a marker. That unpatched snapshot treats the failed hook as non-blocking. For the PermissionRequest counterpart, use an action that actually enters approval and a reviewer that otherwise approves; full-access settings alone can bypass PermissionRequest and would be a misleading test.The narrow contract I prototyped is:
on_error: "block"on synchronous command/MCP handlers for these two events; omission orcontinueretains existing behavior.hookSpecificOutput.hookEventName. An explicit envelope with no decision acknowledges successful evaluation and retains ordinary approval handling. PreToolUse retains deny with a nonempty reason and allow withupdatedInput; this does not addaskor bareallow. PermissionRequest retains its canonical allow/deny decisions, including the existing default denial message.isError, empty/plain-text, malformed, mismatched, or semantically invalid output blocks PreToolUse or produces an explicit PermissionRequest deny. Deny must win over another matching hook's allow. Cancellation must not dispatch the original tool.supportsFailClosedPermissionHooks; absent means unsupported. A${$}MCP template forwards the complete event, including optional subagent fields, without recursively expanding payload strings.Validation so far is against earlier local prototype revisions on that frozen main source, not an official Codex release: 506 hooks/config tests passed; a core approval test proved required failure prevents full-access reviewer fallback; a test using a real MCP connection to a synthetic server completed a subsequent call on the same connection and confirmed the cancelled command never wrote its marker. The native CLI built and its initialize response advertised the proposed capability.
The latest prototype remains unqualified. The full native MCP dispatch matrices needed a test diagnostic correction because public hook events retain only the outer MCP error, and the corrected rerun was interrupted by validation infrastructure. The corrected fixtures use otherwise-valid delayed acknowledgments/error results plus dispatch markers. Canonical generated schemas and the broader workspace suite are also pending. No official release support or complete integration qualification is claimed.
chrysent commentedon Sep 17, 2026
Independent confirmation on codex-cli 0.153.3 (official release tarball, Linux x86_64), with a minimal repro that isolates the malformed-output sub-case.
Setup. A project-level
hooks.jsonregistering a synchronousPreToolUsehandler for^Bash$:{ "hooks": { "PreToolUse": [ { "matcher": "^Bash$", "hooks": [ { "type": "command", "command": "/usr/local/bin/policy-hook --state-dir /state", "timeout": 5, "mode": "sync" } ] } ] } }Repro.
/hooksreview flow;codex execruns with the hook active.2without writing a decision to stdout (it writes a fixed diagnostic to stderr).printf data > /tmp/out.txt).Observed on 0.153.3:
2.codex execerror surface prefixed withCommand blocked by PreToolUse hook:— so the operator-visible text looks like a block.command_executionJSONL item and the sentinel file is created.codex execexits0.So in this state the hook is worse than absent: the error text reads as enforcement to anyone watching the output, while the tool ran. We only noticed because the absence of the expected filesystem side-effect assertion failed on our side after we fixed the decoder — before the fix, every request from our hook failed decode and nothing was ever actually blocked.
After fixing the hook (well-formed JSON decision on stdout, exit 0): denials are enforced correctly (no
command_executionitem for the turn, command not executed), so the happy path works fine on this version.Ask. As proposed in this issue: an opt-in per-hook (or global)
failureMode: blockso a crashing/malformed/failing hook fails closed, plus — if possible — distinguishing the surfaced text for "hook blocked the command" vs "hook itself failed" (in our repro both render asCommand blocked by PreToolUse hook: ..., which actively misleads during diagnosis).Happy to provide the hook filter script or extra traces if useful.
Addendum: one-shot reproduction script
A self-contained shell script reproduces the issue with no dependencies beyond
codexitself. It creates a minimal project whose single PreToolUse handler for^Bash$always fails (reads stdin, writes a diagnostic to stderr, exits 2, no decision on stdout), runscodex execasking codex to create a sentinel file, then reports which failure mode occurred:Command blocked by PreToolUse hook: policy-hook: could not parse request envelopeThe script (save as run-repro.sh, chmod +x, run it in an empty directory; tested on codex-cli 0.153.3, official release tarball, Linux x86_64). NOTE: it is interactive by necessity — newly written hooks start Untrusted and never run until trusted through the ordinary /hooks flow, so the script pauses once for you to trust the hook (using --dangerously-bypass-hook-trust instead would change what is being tested and is deliberately not used):
The hook deliberately ignores stdin content — any Bash step triggers the same failure path, so nothing about the request matters except that it is a Bash call.
soycarts commentedon Sep 18, 2026
+1, with evidence from codex-cli 0.152.1 on macOS: a
PreToolUsecommand hook that crashed, one that exceeded itstimeout, and one that printed output the harness could not parse each surfaced a hook failure, and in all three cases the shell command then ran, its effect was on disk afterwards, and aPostToolUseevent followed.For the shape: a per-entry opt-in that defaults to today's behaviour, as proposed here (
failureMode: "block"oron_error: "block"), is exactly what I need. A hook that renders a status line should keep proceeding on failure; a hook that answersdenyshould be able to say that its silence means no. ThePermissionRequestcoverage noted above matters for the same reason: an outage between the two phases is otherwise an approval.Runbook and sanitized captures for the three failure trials: https://github.com/approval-md/approval.md/blob/main/docs/codex-hook-probe.md (see the hook-failure observations in the native evidence section).
chiuwill87 commentedon Oct 1, 2026
Source confirmation for released 0.159.3, plus one coverage requirement for native child dispatch.
At 0.159.3 pre_tool_use.rs, callback failures and malformed results still leave
should_blockfalse. No-match and serialization-failure paths also continue. Required managed-hook loading checks startup loading; it does not establish successful runtime validation.Our requirement: a specifically designated mandatory validator must return a valid allow decision before native child allocation. Missing/unmatched required coverage, an untrusted/unavailable validator, crash, timeout, malformed result, or serialization failure must deny before allocation. Optional/advisory hooks can retain current defaults.
Relevant boundary: core pre-tool handling, followed by native child creation.
Acceptance needs valid-allow/explicit-deny cases, every required-validator failure class, coverage after configuration reload, and proof that denied calls create no child. A matched-hook failure policy alone would leave absent/mismatched mandatory coverage unresolved.
Evidence limit: inspected released source; earlier 0.159.2 testing confirmed a sampled explicit native denial before child creation. We have not performed live failure injection on 0.159.3.
Is a supported implementation or tracked patch available for this required-validation boundary? Related contract-visibility dependency: #46939.
vinifonz commentedon Oct 18, 2026
Feature request: opt-in fail-closed policy for synchronous PreToolUse hook failures
Please support an opt-in policy that denies a pending tool when a synchronous
PreToolUsecommand hook fails. This would let deployments that use hooks for policy enforcement stay on the official CLI instead of maintaining a private patch.I tested the official Linux x64 Codex CLI 0.163.0. The native ELF SHA-256 was
399ff69c431c066e40de318fba45c2b07f45a76b19939d247ee4828fc70c4215.With a trusted synchronous hook matching
Bashandapply_patch, both a native file patch and a shell request to a loopback HTTP receiver still executed after each of these hook failures:Healthy hooks allowed both tools. A Python missing-script error returned exit code 2 with stderr and denied both tools, consistent with the documented exit-code contract. The twelve-case test used disposable profiles, a loopback mock provider and a loopback destination; no real outbound message or customer request was made.
For example, this registered and trusted hook times out before the tool runs:
{ "hooks": { "PreToolUse": [{ "matcher": "^(Bash|apply_patch)$", "hooks": [{ "type": "command", "command": "python3 -c 'import time; time.sleep(3)'", "timeout": 1 }] }] } }After trusting it in a disposable test profile, request a harmless file patch or a request to a local test HTTP receiver. The hook reports
hook timed out after 1s, but the native tool still produces its side effect.I understand that hook errors continuing execution are part of the current documented behavior. The request is for an explicit alternative for synchronous policy hooks, while preserving the existing default and asynchronous hooks' advisory behavior. Under the opt-in policy, timeout, launch failure, nonzero non-deny exit, unusable structured output and pre-dispatch input serialization failure should deny the pending tool. Healthy hooks and explicit valid decisions should retain their current behavior.
cortexive commentedon Oct 19, 2026
+1 from a deployment that runs many concurrent Codex CLI agents behind a local policy hook.
Confirmed on the official 0.163.0 Linux x64 build. With a trusted synchronous
PreToolUsecommand hook, allow and explicit deny (exit 2) both behave correctly. When the hook exceeds itstimeout, though, the host marks the hook failed and the original command still runs. A wrapper inside the hook can turn its own child's failures into a deny, but it can't cover the host's own timeout or spawn-failure branch. So there's currently no way to make a policy hook fail closed from the hook side. This is what keeps us from moving Codex onto a consolidated policy hook.The proposed per-entry
failureMode: "block"(default unchanged) covers our need. One addition: once a block-mode hook is loaded, a config reload shouldn't be able to silently remove it or downgrade it tocontinuemid-session. Either refuse that reload and keep the prior snapshot, or require re-trust. Otherwise a policy guard can disappear from a running session without anyone noticing.ion-rbl commentedon Oct 20, 2026
Measurements on Codex CLI 0.162.1 (macOS) that confirm the timeout case still fails open:
Minimal reproduction, run on 2026-10-20. In a trusted project, with the hook trusted:
.codex/hooks.json:
{"hooks": {"PreToolUse": [{"matcher": "Bash", "hooks": [
{"type": "command", "command": "date +%s > /tmp/hookstart; sleep 30; date +%s > /tmp/hookend", "timeout": 5}]}]}}
Run: codex exec --json - with the prompt "Run this shell command exactly once: echo probe".
Result: the hook started at second 1792487332. Codex stopped it (hookend was never written) and ran the command at 1792487337.1, about 5 seconds later. The command printed "probe" with exit code 0. The JSON event stream held no hook error or hook event, standard error was empty, and codex exec exited 0. The same run with the plain command "sleep 30" gave the same result (about 5.3 seconds, then the command ran).
Default timeout. A hook with no "timeout" field that slept 1500 seconds was stopped after about 599 seconds, then the Bash command ran with no error. This matches a default of 600 seconds.
So a slow or hung PreToolUse guard becomes a silent allow, and nothing in the event stream shows that the guard never decided. A "failureMode": "block" option as proposed here would close this. Until then, a visible event for a hook timeout would at least make the allow auditable.