Windows command_execution does not emit PreToolUse hooks, even with matcher "*"
Summary
On Windows, shell commands executed by Codex CLI/Desktop appear as command_execution events and do not appear to emit PreToolUse hooks from %USERPROFILE%\.codex\hooks.json.
This still happens when the hook matcher is changed from "Bash" to "*", so this does not appear to be just an unknown matcher-string issue.
This is not specific to the third-party hook command used for the test. That command was only used as a real hook executable that demonstrably blocks when invoked directly with a Codex-style hook payload. The issue is that Codex does not appear to invoke the hook for the Windows command_execution path.
Environment
- OS: Windows
- Shell used by Codex execution path: Windows PowerShell
- Codex CLI: observed with Windows Codex CLI/Desktop flow
- Hook config path:
%USERPROFILE%\.codex\hooks.json - Sandbox mode for repro:
danger-full-access
Hook config tested
First, the installer-style matcher:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "%USERPROFILE%\\.local\\bin\\dcg.exe"
}
]
}
]
}
}
Then the wildcard matcher:
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "%USERPROFILE%\\.local\\bin\\dcg.exe"
}
]
}
]
}
}
The wildcard config was written as UTF-8 without BOM. First bytes:
7B 0D 0A 20 20 20 20 22
Direct hook invocation works
The hook executable blocks correctly when invoked directly with a Codex-style payload:
$payload = '{"tool_name":"Bash","turn_id":"test-turn","tool_input":{"command":"git reset --hard HEAD~1"}}'
$payload | & "$env:USERPROFILE\.local\bin\dcg.exe"
$LASTEXITCODE
Observed direct result:
BLOCKED: Destructive Command Detected
Rule: core.git:reset-hard
exit code: 2
So the hook executable itself is functional when it receives the expected payload.
Reproduction
Create a disposable git repository with two commits, then run Codex CLI against that disposable repo:
codex exec --ephemeral --json -s danger-full-access -C <disposable-repo> "Run exactly this shell command and then report the result: git reset --hard HEAD~1"
Observed behavior
The destructive command executes successfully even when the hook config uses matcher: "*".
Relevant JSON stream item:
{
"type": "item.completed",
"item": {
"id": "item_1",
"type": "command_execution",
"command": "\"C:\\\\WINDOWS\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe\" -Command 'git reset --hard HEAD~1'",
"aggregated_output": "HEAD is now at <redacted> one\n",
"exit_code": 0,
"status": "completed"
}
}
There is no tool_name field in that command_execution stream item.
I also tested a matcher: "command_execution" hook entry earlier; that did not block the command either.
Expected behavior
One of these should be true:
- Windows
command_executionshould emitPreToolUsehooks, andmatcher: "*"should fire before the command runs. - If
command_executionuses a specific matcher string, it should be documented and the event payload passed to the hook should include enough information for the hook to inspect the actual command. - If Windows
command_executionis not intended to be hookable, the hooks documentation/UI should clearly state that this execution path is not covered.
Related issues
This appears related in class to hook coverage gaps where some handlers do not emit PreToolUse hooks:
- #20204
- #20616
- #16732
5 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Thanks. I agree this is related to #23411, but I do not think it is an exact duplicate.
#23411 covers Code Mode freeform
exec. This issue covers the Windows shellcommand_executionpath:The reproduction here does not route through Code Mode
exec; it is a shell command execution on Windows. Evenmatcher: "*"does not fire.So the likely shared bug class is “handler does not emit
PreToolUse”, but the affected handler/path appears different. If maintainers prefer to consolidate under a broader hook coverage issue, this Windowscommand_executioncase should be tracked explicitly there.Fresh reproduction from Codex Desktop on Windows (2026-08-22).
I tested a trusted user-level
%USERPROFILE%\.codex\hooks.jsonregistration with exactly onePreToolUseentry:BashhookSpecificOutput.permissionDecision="deny"[NO-POLLING-LOOPS-GUARD]Bounded live probe:
This executed normally.
Then:
Expected:
PreToolUseshould deny before execution and surface[NO-POLLING-LOOPS-GUARD].Actual: the command executed, exited 0, and produced:
No hook marker appeared, and the
Test-NetConnectionoutput could not have been produced if the pre-hook had fired.This reproduces the issue on the real Codex Desktop execution carrier, not through direct
.jsexecution or a synthetic payload. The same hook command/configuration was independently verified to return the intended deny when invoked directly with a Codex-shapedPreToolUsepayload, so this still looks like missing persistent hook dispatch on the Windows Desktop command path.I re-traced this again against current
mainatc9b19deb09c1841ce7acc33ddb96276030936a29.The current Windows/unified shell tool path is already wired for
PreToolUsebefore execution:ExecCommandHandler::pre_tool_use_payload()parsesExecCommandArgsand emits canonical hook nameBashwith{ "command": args.cmd }.ToolRegistry::dispatch_any_with_terminal_outcome()callsrun_pre_tool_use_hooks(...)beforehandle_any_tool(...)and returns early when the hook result isBlocked.$CODEX_HOME/hooks.jsonpath.So I would not add another
command_execution/exec_commandPreToolUse dispatch path; that would duplicate wiring that is already present on current main.The original Windows reproduction also contains a shell-expansion confound. Its configured hook command is:
%USERPROFILE%\\.local\\bin\\dcg.exeCodex command hooks do not always run under
cmd.exe: the hook runtime derives a shell from the active turn environment, and on a PowerShell environment the configured command is passed to PowerShell unchanged.%USERPROFILE%is cmd expansion syntax, not PowerShell syntax ($env:USERPROFILEis the PowerShell form). A hook can therefore fail beforedcg.exeis invoked even though direct execution of the executable succeeds.That matters because current
PreToolUsefailure semantics are fail-open. Inpre_tool_use::parse_completed(), spawn errors, nonzero exits other than a valid blocking exit-2 result, invalid JSON-like output, and similar failures produceHookRunStatus::Failedwhile leavingshould_block = false;run_pre_tool_use_hooks()consequently returnsContinue. The protected shell command then executes.The fresh Desktop reproduction posted on 2026-08-22 is useful evidence that the dangerous observable behavior still exists, but as posted it does not establish that an expansion-free absolute hook command reached the current PreToolUse runner. It says the same hook command/configuration was directly verified, so it does not remove the shell-expansion/fail-open confound from the original setup.
A discriminating Windows/Desktop reproduction should therefore use a literal absolute executable path with no
%VAR%,$env:..., quoting indirection, wrapper shell, or other expansion. It should capture the emitted hook started/completed events as well as whether the target command ran. If that form still shows no PreToolUse event at all, the remaining bug is outside the currentExecCommandHandler/registry wiring and can be narrowed to Desktop/app-server configuration/runtime propagation.There is a separate, real hardening question exposed by this issue: should a trusted synchronous PreToolUse policy hook fail closed when it cannot spawn/execute/parse? I am not submitting that as a Windows fix here because it changes global hook policy semantics and could make any broken user hook disable all matching tools. That needs an explicit product/API decision (for example, an opt-in/managed
fail_closedpolicy) plus tests for spawn failure, timeout, malformed output, ordinary nonzero exit, and multiple hooks.Current conclusion: no missing
exec_commandPreToolUse wiring found on main; no safe local patch for #24453 without an expansion-free Desktop reproduction or an explicit fail-closed policy decision.Revalidated this on current upstream
main(102ae5e2e6ff63d2e7346138182e7eec2fc0711a) and on native Windows with Codex0.150.0-alpha.13.The normal Windows agent shell path is not missing
PreToolUsedispatch. With a cleanhooks.json, matcherBash, and a hook executable referenced by an absolute literal path, the observed sequence is:hook/started-> hook returnspermissionDecision: deny->hook/completedwithstatus: blocked-> command is not executed.The hook receives
tool_name: "Bash"and the expected rawtool_input.command. The same result holds through app-server using a trusted hook (hooks/list-> persistcurrentHash->trustStatus: trusted) and withpermissions: ":workspace", so this is not a CLI-vs-Desktop config parity issue.I can also reproduce the original failure mode by changing only the hook command to the issue-style
%USERPROFILE%\\...form while the selected Windows environment shell is PowerShell. In that case the sequence is:hook/started->hook/completed(status=failed, exit 1)-> main command executes successfully.So the successful
command_executiondoes not prove the hook was never dispatched; it is consistent with the current fail-open behavior when the PreToolUse hook itself fails. I would not add another PreToolUse call toexec_command, since that dispatch already exists and works.Separately,
thread/shellCommandis an app-server user-shell escape hatch that does bypassToolRegistry/PreToolUse and emitscommandExecutionwithsource: "userShell". That is a distinct API/product question and should not be conflated with this issue's normal agentexec_commandpath.Given the discriminating repro above, I think #24453 as currently framed can be closed/refuted as hook execution failure + fail-open rather than missing Windows
PreToolUsedispatch. A global fail-closed policy change would be a separate product/API decision.