PreToolUse deny (exit 2 + permissionDecision JSON) is not enforced for apply_patch - hook fires, write proceeds

Open 💬 2 comments Opened Jun 12, 2026 by Lukeini

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

  1. Project .codex/hooks.json (trusted; [features].hooks = true in config.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
          }
        ]
      }
    ]
  }
}
  1. 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.

  1. 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).
  1. 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.

View original on GitHub ↗

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