You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ApplyPatchHandler doesn't emit PreToolUse/PostToolUse hook event. Hooks only fire for Bash tool. #16732
What terminal emulator and version are you using (if applicable)?
No response
What issue are you seeing?
PreToolUse and PostToolUse hooks registered via hooks.json never fire for file writes made through apply_patch. They only fire for Bash/shell tool calls.
Two root causes in the CLI source:
ApplyPatchHandler in registry.rs uses the default None implementation for pre_tool_use_payload() / post_tool_use_payload() (lines 63-65 of the trait), so the hook runtime skips it entirely.
hook_runtime.rs hardcodes tool_name: "Bash" (line 131) in both run_pre_tool_use_hooks and run_post_tool_use_hooks, so even if payloads were emitted, they'd be misidentified as Bash calls.
This means hook-based file access control, audit logging, or write coordination cannot cover the primary file write path.
What steps can reproduce the bug?
Create a hooks.json with a PreToolUse hook command that logs all invocations to a file
Ask Codex to edit a source file (triggers apply_patch)
Ask Codex to run a shell command (triggers Bash)
Check the log, observe only the Bash call appears and the file edit is missing
Tested with Codex CLI v0.118.0. Hook log captured 4 entries (2 PreToolUse + 2 PostToolUse), all with tool_name: "Bash" for sed and rg calls. The apply_patch call that actually modified the file produced zero hook events.
What is the expected behavior?
PreToolUse and PostToolUse hooks should fire for apply_patch with tool_name: "ApplyPatch" (or similar) and a payload containing the file paths being modified, consistent with how Bash tool calls emit hook events.
Additional information
We're building multi-agent orchestration that coordinates file writes across Codex, Claude Code, and OpenCode. Claude Code's SDK fires PreToolUse hooks on Write/Edit/MultiEdit tools, which lets us implement file-level locking between agents. This is foundational for implementing checkpoints in an environment where multiple agents edit the same worktree concurrently. Agents acquire a lock on a file before editing, providing a clear record of what changed and how to undo it.
Codex's hook infrastructure supports the same pattern architecturally, but apply_patch not opting in blocks the integration. The fix is scoped, implement pre_tool_use_payload() on ApplyPatchHandler and parameterize tool_name in hook_runtime.rs. Happy to elaborate on the use case or provide more analysis.
I dug into this and can confirm both root causes described in the issue. Here's a breakdown of the fix I have ready locally:
Root cause 1: hook_runtime.rs hardcodes tool_name: "Bash"
Both run_pre_tool_use_hooks() (line 131) and run_post_tool_use_hooks() (line 162) construct their hook request with tool_name: "Bash".to_string(). This means every hook event is misidentified as a Bash call regardless of the actual tool.
Fix: Add a tool_name: String parameter to both functions and pass it from dispatch_any() in registry.rs using invocation.tool_name. This way every tool gets correctly identified — not just Bash.
Root cause 2: ApplyPatchHandler doesn't implement hook payload methods
ApplyPatchHandler in tools/handlers/apply_patch.rs relies on the default None return from pre_tool_use_payload() and post_tool_use_payload() in the ToolHandler trait (registry.rs:63-74). Since both return None, the hook dispatch in dispatch_any() skips hooks entirely for apply_patch calls.
Fix: Implement both methods on ApplyPatchHandler, following the same pattern as ShellHandler (shell.rs:206-221):
Add a helper apply_patch_payload_command() that extracts the patch input from ToolPayload::Function (via ApplyPatchToolArgs) or ToolPayload::Custom
pre_tool_use_payload() returns PreToolUsePayload { command } with the patch input
post_tool_use_payload() returns PostToolUsePayload { command, tool_response } with the patch input and result text
Activity
github-actions commentedon Apr 3, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
aashish-thapa commentedon Apr 4, 2026
I dug into this and can confirm both root causes described in the issue. Here's a breakdown of the fix I have ready locally:
Root cause 1:
hook_runtime.rshardcodestool_name: "Bash"Both
run_pre_tool_use_hooks()(line 131) andrun_post_tool_use_hooks()(line 162) construct their hook request withtool_name: "Bash".to_string(). This means every hook event is misidentified as a Bash call regardless of the actual tool.Fix: Add a
tool_name: Stringparameter to both functions and pass it fromdispatch_any()inregistry.rsusinginvocation.tool_name. This way every tool gets correctly identified — not just Bash.Root cause 2:
ApplyPatchHandlerdoesn't implement hook payload methodsApplyPatchHandlerintools/handlers/apply_patch.rsrelies on the defaultNonereturn frompre_tool_use_payload()andpost_tool_use_payload()in theToolHandlertrait (registry.rs:63-74). Since both returnNone, the hook dispatch indispatch_any()skips hooks entirely for apply_patch calls.Fix: Implement both methods on
ApplyPatchHandler, following the same pattern asShellHandler(shell.rs:206-221):apply_patch_payload_command()that extracts the patch input fromToolPayload::Function(viaApplyPatchToolArgs) orToolPayload::Custompre_tool_use_payload()returnsPreToolUsePayload { command }with the patch inputpost_tool_use_payload()returnsPostToolUsePayload { command, tool_response }with the patch input and result textScope
3 files changed, 40 insertions, 2 deletions.
cargo check -p codex-corepasses clean and existing hook-related tests (shell_pre_tool_use_payload_uses_joined_command,shell_command_pre_tool_use_payload_uses_raw_command,build_post_tool_use_payload_uses_tool_output_wire_value) all pass.I have the implementation on a branch if it would be helpful: https://github.com/aashish-thapa/codex/tree/fix/apply-patch-hook-events
fix: emit hooks for apply_patch
feat(codex): mirror Phase 1 hooks to ~/.codex via curated allowlist (…
feat(codex): mirror Phase 1 hooks to ~/.codex via curated allowlist (…
henryl commentedon Apr 12, 2026
This would be helpful.
TyceHerrman commentedon Apr 14, 2026
This issue could be expanded to be about tracking expansion of matcher patterns for pretooluse/posttooluse generally.
feat(enforcement): Rail 2 — provider-neutral hook scripts
horiacristescu commentedon Apr 15, 2026
Please implement the ApplyPatchHandler/FileEdit hooks, my harness depends on them to enforce workflow discipline. I can't find a workaround.
33 remaining items
fix(core): emit hooks for apply_patch edits (openai#18391)
hooks: emit Bash PostToolUse when exec_command completes via write_st…
review: fix Codex hook dialect (apply_patch matcher + JSON deny shape)
review: correct stale #16732 caveat -> Codex >=0.123.0 version floor
v1.29.0: dual-native Claude Code + Codex CLI parity (#61)
fix(core): emit hooks for apply_patch edits (openai#18391)
hooks: emit Bash PostToolUse when exec_command completes via write_st…
feat(codex): mirror Phase 1 hooks to ~/.codex via curated allowlist (…
docs(loom): Codex hook live-fire result — inert upstream (openai/code…
feat(loom): repo-native practice-memory store — docs/loom/memory/ + d…
docs(F240): M9-A 轨收口设计定稿 — Codex hooks 合同(A3) + CODEX_HOME 与四方一致性诊断(A4)
docs(F240): M9-A 轨收口设计定稿 — Codex hooks 合同(A3) + CODEX_HOME 与四方一致性诊断(A4)
fix(core): emit hooks for apply_patch edits (#18391)
feat: an advisory tier on all four agents, four extraction fixes, and…
feat(loom): repo-native practice-memory store — docs/loom/memory/ + d…