PreToolUse deny (exit 2 + permissionDecision JSON) is not enforced for apply_patch - hook fires, write proceeds
Summary
With a project-scoped .codex/hooks.json registering a PreToolUse hook on matcher apply_patch|Edit|Write (trusted via the per-hook trust prompt, [features].hooks = true), the hook FIRES for apply_patch - but a deny emitted through both documented channels (exit code 2 with a stderr message, and stdout JSON hookSpecificOutput.permissionDecision: "deny") is not enforced. The transcript shows the hook ran and reports Failed, yet apply_patch reports success and the file is written with exactly the content the hook denied.
Related to but distinct from #26733: there the hook never fires because Write|Edit|Bash matchers do not cover the internal patch tool. In our setup apply_patch IS a matchable name and the hook dispatches - the gap is one level deeper, at deny enforcement.
Reproduction
- Project
.codex/hooks.json(trusted;[features].hooks = trueinconfig.toml):
{
"hooks": {
"PreToolUse": [
{
"matcher": "apply_patch|Edit|Write",
"hooks": [
{
"type": "command",
"command": "pwsh -NoProfile -ExecutionPolicy Bypass -Command '$repo = if ($env:ASSISTANT_TEAM_ROOT) { $env:ASSISTANT_TEAM_ROOT } else { (& git rev-parse --show-toplevel).Trim() }; & (Join-Path $repo \"Tools/RuntimeValidation/validate-written-artifact.ps1\") -HookProvider codex -Mode PreToolUse; exit $LASTEXITCODE'",
"timeout": 15
}
]
}
]
}
}
- On a deny finding, the hook command emits all documented deny signals at once:
- stdout (compact JSON):
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"<reason>"}} - a human-readable message on stderr
- exit code 2
Verified standalone: running the exact hook command in a terminal against the same payload returns exit 2 and prints the JSON above.
- Confirm the hooks are live in the session: a PostToolUse hook registered for the SAME matcher in the same file executes and logs on every
apply_patch(this is the oracle proving dispatch, payload parsing, and command execution all work).
- Ask Codex to create a file whose content triggers the deny, using
apply_patch. In our case: a Markdown draft containing the U+2014 character written into a folder our validator classifies as an external draft location.
Expected behavior
Per the hooks documentation, a PreToolUse hook exiting 2 (or returning permissionDecision: "deny") blocks the tool call, and PreToolUse intercepts apply_patch.
Actual behavior
Transcript shows hook: PreToolUse -> Failed, then apply_patch outputs Success. Updated the following files: A <path> - the file is created with the denied content. The write is not blocked and the permissionDecisionReason is never surfaced.
Data points
- Codex CLI 0.133.0 (Windows 11, pwsh 7 hook command): 3 runs post trust-grant - PreToolUse fired, was marked
Failed, and the write proceeded each time. - Codex Desktop 0.138.0-alpha.7 (same machine, fresh session): probe pair - the deny-triggering probe was applied despite the dual deny channel; a control probe (non-deny content) executed and was logged by the PostToolUse hook within the same minute, confirming hooks were live in that exact session.
- The same script's PostToolUse mode works reliably on both builds (it logs findings; we use it as a post-write revert fallback - delete plus feedback message - and it executed live during a real session).
- Claude Code honors the same JSON deny emitted by the same validator script, so the script side is proven cross-provider.
Impact
PreToolUse cannot serve as a write guardrail for apply_patch even when it fires and matches. Projects that need enforcement must fall back to a PostToolUse revert (write-then-delete), which is what we shipped - workable, but the denied content transiently exists on disk, and pre-write blocking remains unavailable on the Codex side.
Environment
- Codex CLI 0.133.0 and Codex Desktop 0.138.0-alpha.7 (both reproduce)
- Windows 11; hook command runs pwsh 7 with
-NoProfile - Project-scoped
.codex/hooks.json;[features].hooks = true; hooks trusted via the per-hook trust prompt
Notes for reproducers
- After ANY change to a hook command in
hooks.json, per-hook trust must be re-granted or the changed hook is silently skipped (sessions started before the change keep the previously trusted config in memory). This initially produced a false "PreToolUse never fires" trail for us; all data points above were collected post trust-grant. - Happy to attach full transcripts and the validator script on request.
Related issues: #26733 (internal patch tool not matched by Write/Edit/Bash - note that apply_patch does match and fire on our builds), #20204 (PreToolUse coverage inconsistency across tool handlers), #26452 (codex exec hook dispatch flakiness - observed by us as well, excluded from this report). This report is specifically about deny enforcement when the hook DOES fire.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗