Skip to content

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

Description

@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.

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

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIappIssues related to the Codex desktop appbugSomething isn't workinghooksIssues related to event hookswindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions