Inconsistent PreToolUse hook coverage across tool handlers (most tools never emit hook events)
Summary
codex-rs/core/src/tools/registry.rs dispatches PreToolUse / PermissionRequest / PostToolUse generically: any handler that returns Some(payload) from pre_tool_use_payload will surface a hook event. Today only shell (Bash), unified_exec, apply_patch, and mcp opt in. Every other tool handler in core/src/tools/handlers/ falls back to the trait default (None), so hooks never fire for those tools.
The result is that the hooks system, which is otherwise a clean general purpose interception mechanism, behaves as if it were shell and edit-only. Anyone building tooling on top of Codex hooks (observability, audit logging, policy enforcement, telemetry, debugging instrumentation, third-party integrations) silently loses coverage on a large portion of the agent's tool surface.
Affected handlers (don't currently emit hook events)
From codex-rs/core/src/tools/handlers/:
- Filesystem reads:
list_dir,view_image - MCP resource access:
mcp_resource(asymmetric withmcptool calls, which do emit) - Agent task plan / objectives:
plan(update_plan),goal(create_goal/update_goal/get_goal) - Batch sub-agent orchestration:
agent_jobs(spawn_agents_on_csv) - Tool discovery:
tool_search,tool_suggest - Per-agent control:
multi_agents/*(5 sub-handlers),multi_agents_v2/*(7 sub-handlers) - Web search: no handler implementation in
core/src/web_search.rs
The asymmetry between apply_patch (file edits: emits hooks) and list_dir / view_image (file reads: silent) is particularly noticeable for any tool that wants a complete view of agent activity.
Why this matters
Codex hooks are a public extension point. Their value depends on coverage being consistent across tools. With today's gaps:
- Observability tools cannot record what files the agent listed or what plans it updated.
- Audit logs are incomplete:
apply_patchis logged, but theupdate_planthat decided to apply the patch is not. - Policy / safety tooling cannot enforce constraints on agent reasoning surface (
update_plan,goal) or on multi-agent orchestration. - Telemetry / debugging instrumentation has blind spots on the same boundaries.
This is a consistency gap in an existing public API surface.
Proposed change
Add pre_tool_use_payload implementations to the handlers above. The pattern is already established in apply_patch.rs:317-322 and mcp.rs. Each addition is 5 to 15 lines of Rust.
A working PoC is available on a fork branch:
- Branch: https://github.com/armoriq/codex/tree/feat/expand-pre-tool-use-payload-coverage
- Commits:
8597270a9("core: emit PreToolUse hooks from list_dir, view_image, mcp_resource, plan, goal, agent_jobs, tool_search, tool_suggest")f98155cf8("core: add tests for new pre_tool_use_payload impls")
The PoC covers 8 of the missing handlers:
list_dir.rsview_image.rsmcp_resource.rsplan.rsgoal.rsagent_jobs.rstool_search.rstool_suggest.rs
For handlers that wrap multiple sub-tools (mcp_resource, goal), the raw function arguments are forwarded as JSON. For handlers with a single args schema (list_dir, view_image, tool_search), the parsed fields are surfaced explicitly.
Diff size: 16 files changed, 503 insertions, 0 deletions (8 handler patches plus 8 test additions).
Tests: 10 new unit tests cover the new pre_tool_use_payload paths, asserting each handler returns the expected PreToolUsePayload shape (and None for non-Function payloads where applicable). Pattern follows apply_patch_tests.rs. Full codex-core lib test count: 1641 to 1651, all passing on the patched branch (after rebasing on current main).
Live verification: Built cargo build -p codex-cli --release and ran the patched binary in Codex Desktop. Captured a PreToolUse tool=update_plan hook event for the first time, confirming the dispatch path is correct end-to-end. Without this patch, update_plan calls go through silently.
Out of scope
Two pieces are intentionally deferred to keep the first PR reviewable:
multi_agents/*(5 sub-handlers) andmulti_agents_v2/*(7 sub-handlers) for fine-grained agent control. The big-picture orchestration entry (spawn_agents_on_csv) is already covered viaagent_jobs.rsin this PR. Per-agent control can land in a focused follow-up.web_search: no handler implementation exists today; needs a new handler skeleton incore/src/web_search.rs. Better as its own issue + PR.
Tested against
openai/codexmain(current head as of filing).- Codex CLI 0.125.0 baseline plus patched build.
- Codex Desktop on macOS (Apple Silicon).
Open questions
- Is there an internal roadmap for hook coverage that this overlaps with?
- Are there any handlers on the deferred list (
multi_agents,web_search) where you'd prefer not to emit hooks for design reasons (privacy, operational noise, etc.)? - Preference on PR shape: single PR for all 8, or split by category?
- Naming convention for hook payloads: should
update_plan'sHookToolNamebe"update_plan"or pulled from the canonical tool registry name?
Happy to iterate on the design before any code lands.
10 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Not a duplicate of #18491, though there is partial overlap. Reasons:
PreToolUseto non-Bash tools, and (b) implement theupdatedInputrewrite path in the hook response. This issue covers (a) only and does not propose any changes toupdatedInputsemantics.list_dir,view_image,mcp_resource,plan,goal,agent_jobs,tool_search,tool_suggest) with a working PoC, tests, and an explicit out-of-scope list (multi_agents/*,multi_agents_v2/*,web_search), rather than "all tool calls".read_file,grep,apply_patch) is partially obsolete on currentmain:apply_patchalready emits hook events.The bot likely matched on shared keywords (
PreToolUse, hooks, tool handlers). Happy to coordinate with #18491's author if maintainers prefer to consolidate, but flagging as a separate scope so the implementation half can move forward independently.I have a working fix for this on a fork branch.
Branch: https://github.com/gabrimatic/codex/tree/fix/expand-pre-tool-use-hook-coverage
Commit: https://github.com/gabrimatic/codex/commit/784846956c369ee287ae672c9ae31b5ae5081169
Diff vs
openai/codex@main: https://github.com/openai/codex/compare/main...gabrimatic:codex:fix/expand-pre-tool-use-hook-coverageApproach
Implemented
pre_tool_use_payloadfor the eight handlers identified in the issue, following the existing pattern inapply_patch.rs:317-322andmcp.rs:list_dirview_imagemcp_resource(asymmetric withmcptoday; emits a payload per resolved tool name —list_mcp_resources/list_mcp_resource_templates/read_mcp_resource)update_plan(plan.rs)get_goal/create_goal/update_goal(goal.rs)spawn_agents_on_csv(agent_jobs.rs)tool_searchtool_suggestThe hook tool name is
HookToolName::new(invocation.tool_name.display()), so handlers backed by multiple tool names report the specific tool that was invoked. Thetool_inputis the parsed JSON arguments, mirroringmcp.rs. A small shared helper,hook_tool_input_from_arguments, lives intools/handlers/mod.rs: empty arguments become an empty object so hook scripts can rely on a JSON object shape, and inputs that don't parse as JSON are surfaced as a string fallback. Non-Function/ToolSearchpayloads short-circuit toNone, matching the existingunified_execandmcpimplementations.No existing handler logic is modified — purely additive.
Tests
Each handler gets a unit test that asserts:
pre_tool_use_payloadreturns the expectedtool_nameand parsedtool_inputfor aFunction(orToolSearch) payload.None.goalalso locks in the empty-object fallback forget_goal(which has no arguments).Verification
Scope
13 files, 589 insertions, 0 deletions. All additions, no modifications to existing logic.
Multi-agent v1/v2 sub-handlers and
web_search(which has no handler implementation) are intentionally out of scope here — happy to follow up in a separate change if there's appetite. This change matches the eight-handler scope described in the issue.Per the contributing guidelines I'm not opening a PR; please let me know if you'd like me to do so under invitation, or if a maintainer would like to land the fix directly from the branch.
Indexed this hook ticket in the umbrella tracker: #21753
Goal: collect the scattered Codex hook requests and bugs into one parity matrix for Full Claude Code Hook Parity (29+), while preserving this issue as the detailed thread for its specific behavior.
This is the exact gap that makes hook based governance hard to trust.
If some tool paths emit hooks and others do not, external tools think they are seeing the agent action stream when they are only seeing part of it. That is worse than an unsupported surface because the blind spot is not obvious.
Two things would make this much safer:
That would let Codex events map cleanly into a neutral runtime evidence envelope, including AgentHook compatible collectors, without pretending the hook surface is complete while coverage is still handler dependent.
@gabrimatic Thank you for picking this up, your fork looks solid. The shared hook_tool_input_from_arguments helper is a much cleaner approach than my original per-handler PoC, and the per-tool-name dispatch in mcp_resource is exactly right.
I'd love to co-author with you on the PR upstream, with you as the primary author since the implementation is yours. To make that easy, here's what I can take off your plate:
Rebase onto current main. Your branch is now 424 commits behind upstream; I can rebase, resolve any conflicts (likely around the recent handler restructuring), and run the existing hook test suite to confirm coverage holds.
Add coverage tests. I have an integration test scaffold from my earlier PoC that exercises each of the 8 handlers and asserts the PreToolUse payload shape (tool name, parsed input). Happy to fold those in.
Open the PR and shepherd it through CI / review feedback.
If you'd prefer to drive the PR yourself, I'm equally happy to help in any other shape to review your branch, add tests in a follow-up commit, file companion documentation, whatever moves it forward. The goal is just getting hook parity merged so the blind-spot problem in #21753 (which @AgenticThinkingUK and others highlighted on the security side) closes for these 8 handlers.
Drop a comment or reach out to [harisudhan@armoriq.io](mailto:harisudhan@armoriq.io) or @Harihara04sudhan on GitHub and we can coordinate.
Good to see this moving.
From the runtime-evidence side, the key thing is full coverage across the tool surface, not just shell/edit paths. These handlers are exactly where audit, policy, and approval systems otherwise get blind spots.
Happy to test the PR against an AgentHook-compatible collector once it is open.
Thanks @Harihara04sudhan, I appreciate it.
I rebased and ported the change onto current
mainhere:https://github.com/gabrimatic/codex/tree/fix/pre-tool-use-hook-coverage-current-main
The refreshed branch is 1 commit ahead of upstream
mainand no longer carries the old 424-commit drift. I also adjusted the implementation for the current per-tool handler layout: goal and MCP resource handlers are now split,request_plugin_installhas replaced the old tool suggestion path, andlist_diris no longer present on currentmain.Local verification:
just fmtjust fix -p codex-corecargo test -p codex-core --lib -- pre_tool_use_payload(26 passed)I did not open an upstream PR yet because
docs/contributing.mdsays external PRs are by invitation only. If a maintainer is comfortable inviting this one, I can open the PR from the refreshed branch. I’m also happy to co-author with you: if you want to add your integration scaffold on top, the branch is ready for that.Adding one more handler to the missing-coverage list: Code Mode
exec(codex-rs/core/src/tools/code_mode/execute_handler.rsatrust-v0.130.0).CodeModeExecuteHandler::pre_tool_use_payloadis unimplemented and inherits the defaultNonefromToolHandler. Same root cause as the 8 handlers already in this issue, but worth calling out separately because Code Mode is the freeform JavaScript execution surface — a model dispatchingtools.exec_command,tools.apply_patch, etc. through Code Mode silently bypasses any hook-based policy/audit/sandboxing that's in place for the direct shell/edit paths.Probe verification: a model routing a SHA-256 prompt through Code Mode (custom_tool_call
name: "exec", input = raw JS callingtools.exec_command) shows zeroPreToolUseevents fired, vs. the same intent routed via directfunction_call: exec_commandwhich DOES fire the hook correctly.Proposed fix branch (3-file diff, ready to cherry-pick):
Adds:
pre_tool_use_payloadonCodeModeExecuteHandlerextracting raw JS fromToolPayload::Custom { input }astool_input.command(same convention asapply_patch/shell_command/unified_exec)HookToolName::code_mode_exec()constructor — canonical namecode_mode_execdistinct fromBash, withexecas matcher alias (mirrorsapply_patch's alias pattern)execute_handler_tests.rsfile)Happy to fold this into the in-flight branch (cc @gabrimatic, @Harihara04sudhan) — the patch is small and self-contained.
For comparison baseline: agent-manual (https://github.com/Ar9av/agent-manual) documents hook coverage across Claude Code, Gemini CLI, and Codex.
Claude Code fires
PreToolUse/PostToolUseconsistently for all built-in tools includingapply_patch(fixed in PR #18391) but coverage for MCP tools varies. Gemini CLI firesBeforeTool/AfterToolfor both native and MCP tools and includesmcp_contextandoriginal_request_namefields in the stdin payload to distinguish them.If the inconsistency here is specifically around MCP vs native tool hooks, Gemini CLI's approach of including MCP metadata in every tool hook (rather than separate event types) might be worth considering as a path forward.