Code Mode `exec` doesn't fire `PreToolUse` hooks (fix patch attached)
Why
Code Mode's freeform exec tool — the JavaScript-execution surface in codex-rs/core/src/tools/code_mode/ — is not emitting PreToolUse hook events. This is the same bug class that #18391 fixed for apply_patch: a freeform tool whose handler doesn't opt into pre_tool_use_payload is silently invisible to the hook runtime.
Concretely, CodeModeExecuteHandler::pre_tool_use_payload is unimplemented and inherits the default None from ToolHandler, so tools::registry::run_pre_tool_use_hooks is never invoked for Code Mode dispatches. Hook-based policy, auditing, and write coordination observe shell commands (Bash) and apply_patch edits but miss every Code Mode execution — including the nested tool calls those executions issue via tools.exec_command, tools.apply_patch, and friends.
For users running Code Mode behind hook-based sandboxing or access control, this means policy enforcement that appears to be in effect for shell tools is silently bypassed whenever the model routes the same intent through a Code Mode dispatch.
Reproduction
Verified on rust-v0.130.0. Probe a model in Codex via any client with hooks configured (e.g. via Openclaw's @openclaw/codex plugin); request a tool call. The model often routes through Code Mode exec (a custom_tool_call with name: "exec", input = raw JavaScript). The configured PreToolUse hook command is never invoked. Direct function_call: exec_command dispatches DO fire the hook correctly, confirming the bug is specifically that Code Mode's handler doesn't opt in.
Proposed Fix
Branch ready in fork: https://github.com/Kaspre/codex/tree/fix/code-mode-pretooluse-hook
Compare URL (for direct review): https://github.com/openai/codex/compare/main...Kaspre:codex:fix/code-mode-pretooluse-hook
What Changed
- Added
PreToolUsepayload support toCodeModeExecuteHandlerviapre_tool_use_payload, extracting the raw JavaScript source fromToolPayload::Custom { input }and exposing it astool_input.command. - Added
HookToolName::code_mode_exec()modeled onHookToolName::apply_patch()— canonical hook namecode_mode_exec(distinct in logs and policies fromBashshell tools), withexecaccepted as a matcher alias for hook configurations written against the model-facing tool name.
The tool_input.command field name matches the existing convention shared by apply_patch, shell_command, and unified_exec — all carry "the thing to execute" under command regardless of whether the underlying input is shell text, patch text, or (with this change) JavaScript source. This lets a single hook policy expression apply uniformly across execution surfaces.
PostToolUse payload support is intentionally out of scope for this change — happy to add it as a follow-up if maintainers want symmetric coverage with apply_patch.
Verification
Added focused handler coverage in code_mode/execute_handler_tests.rs:
- Freeform Code Mode (
ToolPayload::Custom) dispatches produce aPreToolUsePayloadwhosetool_nameiscode_mode_execand whosetool_input.commandmatches the raw JavaScript source. - Non-Custom payloads return
None(defensive — the registry'smatches_kindalready gates this, but the handler shouldn't assume).
The pre-existing execute_handler_tests.rs in this directory was orphaned (#[path] declaration removed; references a parse_freeform_args function that no longer exists) and has been replaced. Wired in via the standard #[cfg(test)] #[path = "execute_handler_tests.rs"] mod tests; declaration at the bottom of execute_handler.rs, matching the convention used by apply_patch.rs and shell.rs.
Filing notes
External PR creation appears to be staff-restricted on this repo (gh pr create returns does not have the correct permissions to execute CreatePullRequest). Filing as an issue with the patch attached as a fork-branch ref for cherry-pick. Happy to convert this to a PR if a maintainer can open it on my behalf, or to follow whatever the preferred contribution flow is.
10 Comments
Reopening — on reflection, while #20204 documents the same root cause (handlers not opting into
pre_tool_use_payload), Code Modeexecis NOT in the 8-handler list that #20204 covers (list_dir, view_image, mcp_resource, plan, goal, agent_jobs, tool_search, tool_suggest). The fix in this issue is additive to #20204, not a duplicate.If maintainers prefer to consolidate, this issue's 3-file diff at https://github.com/Kaspre/codex/tree/fix/code-mode-pretooluse-hook can be cherry-picked alongside #20204's broader 8-handler patch, or folded into it as an extra commit. Code Mode
execis a particularly impactful handler to cover since it's the freeform JavaScript execution surface — a model dispatching nested tools through Code Mode silently bypasses any hook-based policy/audit/sandboxing that's in place for the direct shell/edit paths.(Cross-comment also posted on #20204.)
Confirmed still present as of 0.135 —
CodeModeExecuteHandlerstill doesn't implementpre_tool_use_payload, so Code Modeexecdispatches stay invisible toPreToolUsehooks while shell /apply_patchare covered. (Re-verified end-to-end: a hook-denied read routed through Code Modeexecstill succeeds, while the same read via shell is correctly blocked.)I've rebased the fix onto 0.135 and expanded it:
pre_tool_use_payloadonCodeModeExecuteHandler(canonical hook namecode_mode_exec, withexecas a matcher alias), modeled on the mergedapply_patchprecedent (#18391).with_updated_hook_inputso a hook'supdatedInput.commandrewrites the freeform JS before execution (non-stringcommandrejected with a model-visible error) — the Code Mode side of #18491.updatedInputfailing before execution).cargo test -p codex-coregreen;cargo fmt --checkclean.Updated branch: https://github.com/Kaspre/codex/tree/rebase/code-mode-pretooluse-0.135
(Rebased onto 0.135 for local deployment; the
apply_patchinfrastructure it mirrors is already onmain, so it ports directly — happy to provide amain-based branch if that's easier to merge.)I still can't open a PR (CreatePullRequest is staff-restricted for me on this repo) — could a maintainer pick this up, or open one on my behalf?
Confirmed this is still reproducible on Windows 11 with Codex Desktop /
codex-cli 0.147.0.Setup:
PreToolUsecommand hook^(Bash|shell_command)$Code-mode reproduction:
When dispatched as a nested shell tool from Code Mode, the command reached
rgand failed with Windows OS error 123. The expectedPreToolUserejection did not occur. The same command payload is rejected by the hook itself because positional wildcard paths must use-g/--glob.This confirms the gap affects nested Code Mode tool dispatch, not matcher syntax or the policy script. It also appears inconsistent with the current Hooks documentation section "Tool calls from code mode," which says
PreToolUsecan stop the nested tool before it runs: https://learn.chatgpt.com/docs/hooks#tool-calls-from-code-modeAdditional evidence from the current macOS Codex Desktop build:
Impact: while this path remains reachable, a hook-based global fail-closed Implementation Gate cannot establish the invariant “no valid implementation authorization means no real side effect.”
Related in-product feedback reference:
019ff063-2411-7313-a5a8-1075c60e04bd
No session transcript, private files, local paths, credentials, browsing history, or unrelated diagnostics are attached.
codex app 26.810 - bug still present!
openai, please fix!
Additional reproduction of the same hook-coverage problem on Linux with codex-cli 0.147.0:
A user-level ~/.codex/hooks.json contains UserPromptSubmit, Stop, and PostToolUse command hooks. UserPromptSubmit and Stop are delivered, but PostToolUse is not delivered when a successful
git pushis routed through the current Code Mode / nested exec path. The push completes and reaches the remote, but the downstream hook integration receives no PostToolUse payload and therefore cannot detect the push.The outer tool call is exposed as
functions.exec, while the nested shell operation isexec_command. The configured matcher coversexec_command|functions.exec_command, but no PostToolUse receipt is produced for this execution path. The same integration does receive push events from another client/runtime path, so this does not look like a hook-script or remote-service failure.This seems closely related to the Code Mode coverage gap described here. In particular, PostToolUse coverage for the outer Code Mode
execand its nested tool calls appears to be missing. A minimal sanitized reproducer can be provided if useful.The project is trusted, and the hook definition itself is trusted. The hook script and its dependencies are accessible from the corresponding client environment.
VS Code / Code Mode behavior
The model calls
functions.exec, which invokestools.exec_command(...)from JavaScript.A command producing approximately 16,300 characters completes successfully with exit code 0. However, the nested
exec_commandresult reaches the JavaScript caller unchanged. The configuredPostToolUsehook does not replace the raw result, and none of the hook-provided metadata is present.In my application,
HOOK_NOT_ACTIVEis only a diagnostic marker meaning that the expected canonical hook output was absent; it is not a Codex error message.Control test
Calling the hook script directly with an equivalent synthetic
PostToolUsepayload succeeds. It returns:continue: falsehookSpecificOutput.hookEventName = "PostToolUse"This indicates that the hook script and its downstream runtime are working. The failure appears to be in lifecycle dispatch or delivery for the Code Mode tool path.
Attempted direct-shell workaround
I also tried starting the interactive CLI with:
The effective feature list reports:
Nevertheless, the CLI reports:
The model then does not invoke a Bash or unified-exec tool at all. It responds without making a tool call, so there is no
PostToolUseevent.Expected behavior
At least one of these paths should work:
tools.exec_command(...)calls from Code Mode should emit a usablePostToolUseevent whose feedback can replace the nested result; orshell_toolandunified_execremain enabled, the model should be able to use the direct shell tool instead of failing closed.Questions
PostToolUsecurrently expected to fire fortools.exec_command(...)invoked insidefunctions.exec?code_mode_hostis disabled?I can provide a minimal synthetic reproduction if useful.
Managed
PreToolUsedeny is ignored by code-mode nested exec on WindowsEnvironment
10.0.26200.824626.818.8289.00.149.0-alpha.4.3%ProgramData%\OpenAI\Codex\requirements.tomlSummary
A managed
PreToolUsecommand hook receives a shell event and returns the documented JSON deny response, but a command issued throughfunctions.execand its nestedexec_commandstill executes.The desktop log records the sequence
hook/started→hook/completed→commandExecution. A controlled negative test then produces its filesystem marker, proving that the command ran after the deny response.The hook itself is not failing to classify the command: the same implementation passes direct protocol tests for the exact wrapper command, malformed-input tests, nearest-neighbor allow cases, and four concurrent invocations.
Minimal managed configuration
Minimal deny response
Steps to reproduce
C:\repro\write-marker.ps1so that it writes a harmless marker file.functions.execcell:``
text
``cmd.exe /d /c powershell.exe -NoProfile -File C:\repro\write-marker.ps1
Expected behavior
The nested command must not be started after a managed
PreToolUsehook returnspermissionDecision: "deny".Actual behavior
The deny decision is not propagated through the code-mode nested exec adapter, and the command executes.
Impact
Administrators cannot enforce a managed PowerShell entry point for code-mode commands. Instructions and post-execution auditing are currently the only controls that remain usable without breaking the host.
Related policy limitation
Restrictive
requirements.tomlprefix rules cannot safely replace the hook:pwshorpwsh.exeblocks the bundled shell used by Codex code mode;powershellorpowershell.exealso blocks an approved wrapper when that wrapper legitimately launches Windows PowerShell;[rules]table; an emptyprefix_rulesarray is rejected by the configuration validator.Requested fix
PreToolUsedeny decisions throughfunctions.exec→ nestedexec_command.Evidence available through
/feedbackThe corresponding
/feedbacksubmission includes the Codex diagnostic logs. Public attachments intentionally omit full local paths, task content, credentials, and unrelated log records.Implementation update for the remaining outer Code Mode gap:
I audited current upstream main and confirmed that the outer freeform Code Mode exec call still uses ToolPayload::Custom, while the generic PreToolUse path only handles function payloads. Nested Code Mode tool calls are a separate path and already have coverage from #28365.
A current reference implementation is now available here:
The patch:
Fresh local verification:
The new integration tests are included, but I cannot honestly claim a local green integration run on this macOS ARM environment. The checked-out upstream base currently has two independent stale field accesses in core/src/skills.rs that prevent compilation; I temporarily corrected only those two lines to isolate the focused unit tests and then reverted them. Building codex-code-mode-host is additionally blocked because the rusty_v8 v150.4.0 macOS ARM prebuilt archive URL returns HTTP 404. An existing Code Mode integration test fails under the same missing-host condition.
Credit to the earlier Kaspre reference branches linked in this issue; this version adapts that direction to the current runtime API and adds current regression coverage.
I did not open an upstream PR because the repository contribution policy explicitly says external code contributions and pull requests are not accepted. The fork branch is provided as reviewable/cherry-pickable evidence instead.
We reproduced the downstream SDLC impact in Codex Desktop.
Env: macOS 26.2 (build 25C56), Apple Silicon (arm64).
Setup:
Result:
This matches the impact described in this issue. We did not capture Codex hook-dispatch logs, so this confirms the observed impact, not the source-level cause.
@JulienJBO proposed patch covers PreToolUse only. Our workflow requires matched PreToolUse and PostToolUse events. Without PostToolUse, there is no reliable completion or status boundary.
Could the production fix include:
If Codex cannot emit a complete event pair, it should provide a visible diagnostic instead of silently continuing.