PreToolUse hooks do not fire for the internal patch/edit tool
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
- 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.
- Install the plugin via local marketplace (
codex plugin add).
- 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."
- 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-writeand--dangerously-bypass-approvals-and-sandboxmodes
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-trustflag is used; the issue is not about hook trust but about matcher coverage
8 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Update: Internal tool name is
apply_patchChecked Codex session transcripts (
~/.codex/sessions/*.jsonl) and found the internal tool name:The tool is registered as
custom_tool_callwith nameapply_patch. Current PreToolUse matchers (Write,Edit,Bash) do not match this tool name.Proposed fix: Either:
apply_patchtool calls (most consistent)apply_patchas a valid matcher name so plugins can register for itapply_patchto the existingWrite/Editmatchers since it performs the same operationCandidate 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 createand 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.
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 -rfbefore execution.For matcher compatibility, I would expose two fields instead of choosing only one name.
apply_patchfile_writeorfile_editThen existing policy hooks can keep matching the capability class used by
Write/Edit, while advanced hooks can targetapply_patchexactly.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._
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:
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:
https://github.com/safal207/LS/issues/595
https://github.com/safal207/LS/issues/596
https://github.com/safal207/LS/issues/597
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?
Closing as not reproducible.
apply_patchalready emitsPreToolUse/PostToolUseevents, andWriteandEditare explicitly supported matcher aliases. This coverage is present in the reportedv0.137.0release and passes on currentmain. If the hook still does not fire for a realcustom_tool_call: apply_patch, please share a minimal plugin reproduction, the rollout entry, and the exact CLI build.