PreToolUse hooks do not fire for the internal patch/edit tool

Open 💬 8 comments Opened Jun 6, 2026 by new-TonyWang
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

PreToolUse hooks registered for Write, Edit, and Bash matchers never fire when the Codex agent edits files, because the agent uses an internal patch tool that is not exposed as a matchable tool name.

Reproduction

  1. Build a minimal Codex plugin with a PreToolUse hook:
{
  "hooks": {
    "PreToolUse": [
      {"matcher": "Write", "hooks": [{"type": "command", "command": "$PLUGIN_ROOT/hooks/block.sh"}]},
      {"matcher": "Edit",  "hooks": [{"type": "command", "command": "$PLUGIN_ROOT/hooks/block.sh"}]},
      {"matcher": "Bash",  "hooks": [{"type": "command", "command": "$PLUGIN_ROOT/hooks/block.sh"}]}
    ]
  }
}

Where block.sh writes a marker file and exits 2.

  1. Install the plugin via local marketplace (codex plugin add).
  1. Launch Codex TUI:
codex --no-alt-screen --dangerously-bypass-hook-trust \
  -s workspace-write \
  "Write the text 'TEST' to test.txt using the Write tool. Then stop."
  1. Observe:
  • The agent responds: "I don't have a tool named Write in this environment, so I'll make the requested file edit with the available patch tool."
  • The file is modified (Added test.txt (+1 -0))
  • The marker file is never created — the hook never fired
  • This happens in both -s workspace-write and --dangerously-bypass-approvals-and-sandbox modes

Expected behavior

PreToolUse hooks should fire for the internal patch/edit tool, or a new matcher name (e.g., Patch, ApplyPatch, or FileEdit) should be documented so plugins can match it.

Actual behavior

The internal patch tool bypasses all PreToolUse hook matchers. There is no documented matcher name for it.

Impact

Plugins that rely on PreToolUse hooks for safety enforcement (e.g., blocking writes to protected files) cannot intercept file edits made by the Codex agent. The only remaining enforcement path is post-validation after the agent commits.

Environment

  • Codex CLI: v0.137.0
  • OS: Linux
  • Plugin hooks feature: enabled and working for Stop hooks (confirmed via marker files)

Notes

  • Stop hooks work correctly — confirmed with marker-based control tests (hook starts, times out as expected)
  • PreToolUse hooks work for Bash when the model explicitly uses the Bash tool (e.g., echo > file), but the model prefers the internal patch tool for file edits
  • The --dangerously-bypass-hook-trust flag is used; the issue is not about hook trust but about matcher coverage

View original on GitHub ↗

8 Comments

github-actions[bot] contributor · 1 month ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #26729

Powered by Codex Action

new-TonyWang · 1 month ago

Update: Internal tool name is apply_patch

Checked Codex session transcripts (~/.codex/sessions/*.jsonl) and found the internal tool name:

{
  "type": "custom_tool_call",
  "name": "apply_patch",
  "input": "*** Begin Patch\n*** Update File: ...\n@@\n..."
}

The tool is registered as custom_tool_call with name apply_patch. Current PreToolUse matchers (Write, Edit, Bash) do not match this tool name.

Proposed fix: Either:

  1. Fire PreToolUse hooks for apply_patch tool calls (most consistent)
  2. Document apply_patch as a valid matcher name so plugins can register for it
  3. Map apply_patch to the existing Write/Edit matchers since it performs the same operation
trotsky1997 · 1 month ago

Candidate fix branch / compare URL:

https://github.com/openai/codex/compare/main...trotsky1997:fix-apply-patch-hooks

I attempted to open this as a draft PR, but both gh pr create and the GitHub connector were denied by repository permissions (CreatePullRequest / Resource not accessible by integration). So I cannot submit the PR directly from this environment, but the branch is pushed and ready for maintainers to inspect or open as a PR.

new-TonyWang · 1 month ago
Candidate fix branch / compare URL: main...trotsky1997:fix-apply-patch-hooks I attempted to open this as a draft PR, but both gh pr create and the GitHub connector were denied by repository permissions (CreatePullRequest / Resource not accessible by integration). So I cannot submit the PR directly from this environment, but the branch is pushed and ready for maintainers to inspect or open as a PR.

Thank you very much, I'm looking forward to trying it.

ishita-0301 · 1 month ago

If an agent is allowed to invoke destructive shell commands, then path confusion, stale context, or a simple typo can still lead to unintended deletions. Once the command is issued, recovery often depends on timing rather than guarantees.

A useful pattern is to enforce policies at the tool boundary. Pre execution hooks can inspect every Bash command before it runs and block known dangerous operations regardless of whether they come from the main agent, a sub agent, or a background task. Projects like https://github.com/FailproofAI/failproofai are exploring that approach with policies such as blocking rm -rf before execution.

Necmttn · 29 days ago

For matcher compatibility, I would expose two fields instead of choosing only one name.

  • raw tool name: apply_patch
  • normalized capability: file_write or file_edit

Then existing policy hooks can keep matching the capability class used by Write/Edit, while advanced hooks can target apply_patch exactly.

That matters because raw tool names are not a stable policy surface across providers or host versions. The policy usually wants the capability: this call mutates files before execution. The debug path wants the raw name: this was the internal patch tool. Keeping both makes the fix less brittle than only documenting a new matcher string.

---

_Generated with ax._

safal207 · 26 days ago

This issue highlights an important runtime boundary:

user-facing tool name
≠ semantic side-effect class

A safety policy should not need to know whether Codex performs a file mutation through "Write", "Edit", "Bash", "apply_patch", or an internal patch implementation.

The interception point should be based on the effect being requested:

FILE_MUTATION
PROCESS_EXECUTION
NETWORK_REQUEST
EXTERNAL_APP_ACTION
SECRET_ACCESS

rather than only the internal tool identifier.

A canonical pre-effect event could look like:

{
"schema_version": "pre-effect-event/v0.1",
"event_id": "effect-001",
"event_type": "PRE_EFFECT",
"trajectory_id": "repository-task-123",
"continuation_id": "thread-456",
"actor": {
"agent_id": "codex-root",
"session_id": "thread-456"
},
"effect": {
"class": "FILE_MUTATION",
"operation": "CREATE",
"targets": [
"test.txt"
]
},
"implementation": {
"tool_name": "internal_patch",
"handler": "codex_patch_runtime"
},
"decision": {
"status": "PENDING"
}
}

Plugins could then match the stable semantic class:

{
"matcher": {
"effect_class": "FILE_MUTATION",
"path": "production/**"
},
"decision": "BLOCK"
}

The central invariant would be:

«Every side effect must cross the same policy boundary, regardless of which internal tool or handler performs it.»

A second invariant:

«Adding or renaming an internal tool must not silently create a new path around existing safety hooks.»

This also suggests separating two layers:

tool event
→ describes how Codex attempted the operation

effect event
→ describes what external state would change

One tool invocation could produce multiple effects, while multiple tools could map to the same effect class.

For example:

shell: echo value > file
internal patch tool
IDE write operation
MCP filesystem tool

should all produce a matchable "FILE_MUTATION" event.

A deterministic conformance fixture could test:

  1. "Write" tool attempts a file creation → hook fires;
  2. internal patch tool performs the same creation → identical semantic hook fires;
  3. shell redirection performs the same creation → identical semantic hook fires;
  4. protected path policy returns "BLOCK" → no file mutation occurs;
  5. allowed path returns "ALLOW" → mutation proceeds;
  6. hook failure or timeout follows an explicit fail-open/fail-closed policy;
  7. post-effect event contains the actual result and content digest;
  8. a newly introduced internal edit tool automatically inherits the same boundary.

The post-effect record could bind the outcome:

{
"event_type": "POST_EFFECT",
"parent_event_id": "effect-001",
"effect_class": "FILE_MUTATION",
"target": "test.txt",
"status": "SUCCEEDED",
"after_digest": "sha256:..."
}

This would also improve replay and auditability because the runtime log would describe actual state transitions rather than only model-facing tool calls.

Related LS work:

  • evidence gates:

https://github.com/safal207/LS/issues/595

  • commit-before-effect:

https://github.com/safal207/LS/issues/596

  • deterministic replay:

https://github.com/safal207/LS/issues/597

  • recovered-evidence continuation:

https://github.com/safal207/LS/pull/651

Would a small vendor-neutral "PreEffectEvent" / "PostEffectEvent" fixture help test that all Codex file-mutation paths cross the same blocking policy boundary?

abhinav-oai contributor · 7 days ago

Closing as not reproducible. apply_patch already emits PreToolUse/PostToolUse events, and Write and Edit are explicitly supported matcher aliases. This coverage is present in the reported v0.137.0 release and passes on current main. If the hook still does not fire for a real custom_tool_call: apply_patch, please share a minimal plugin reproduction, the rollout entry, and the exact CLI build.