Windows command_execution does not emit PreToolUse hooks, even with matcher "*"

Open 💬 5 comments Opened May 25, 2026 by Stef9332
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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:

  1. Windows command_execution should emit PreToolUse hooks, and matcher: "*" should fire before the command runs.
  2. If command_execution uses 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.
  3. If Windows command_execution is 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

View original on GitHub ↗

5 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #23411

Powered by Codex Action

Stef9332 · 3 months ago

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 shell command_execution path:

{
  "type": "command_execution",
  "command": "\"C:\\\\WINDOWS\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe\" -Command 'git reset --hard HEAD~1'"
}

The reproduction here does not route through Code Mode exec; it is a shell command execution on Windows. Even matcher: "*" 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 Windows command_execution case should be tracked explicitly there.

leonidrysev · 5 days ago

Fresh reproduction from Codex Desktop on Windows (2026-08-22).

I tested a trusted user-level %USERPROFILE%\.codex\hooks.json registration with exactly one PreToolUse entry:

  • matcher: Bash
  • command hook: valid; direct invocation returns hookSpecificOutput.permissionDecision="deny"
  • fixed deny marker: [NO-POLLING-LOOPS-GUARD]
  • no trust bypass
  • genuinely fresh Codex Desktop task

Bounded live probe:

Write-Output 'CODEX_GUARD_FRESH_ALLOW_20260822'

This executed normally.

Then:

do { Test-NetConnection 127.0.0.1 -Port 1 -InformationLevel Quiet; Start-Sleep -Milliseconds 100 } until ($true)

Expected: PreToolUse should deny before execution and surface [NO-POLLING-LOOPS-GUARD].

Actual: the command executed, exited 0, and produced:

WARNING: TCP connect to (127.0.0.1 : 1) failed
False

No hook marker appeared, and the Test-NetConnection output 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 .js execution or a synthetic payload. The same hook command/configuration was independently verified to return the intended deny when invoked directly with a Codex-shaped PreToolUse payload, so this still looks like missing persistent hook dispatch on the Windows Desktop command path.

charle-z · 5 days ago

I re-traced this again against current main at c9b19deb09c1841ce7acc33ddb96276030936a29.

The current Windows/unified shell tool path is already wired for PreToolUse before execution:

  • ExecCommandHandler::pre_tool_use_payload() parses ExecCommandArgs and emits canonical hook name Bash with { "command": args.cmd }.
  • ToolRegistry::dispatch_any_with_terminal_outcome() calls run_pre_tool_use_hooks(...) before handle_any_tool(...) and returns early when the hook result is Blocked.
  • user hook discovery still resolves through the normal Codex config layer / $CODEX_HOME/hooks.json path.

So I would not add another command_execution/exec_command PreToolUse 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.exe

Codex 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:USERPROFILE is the PowerShell form). A hook can therefore fail before dcg.exe is invoked even though direct execution of the executable succeeds.

That matters because current PreToolUse failure semantics are fail-open. In pre_tool_use::parse_completed(), spawn errors, nonzero exits other than a valid blocking exit-2 result, invalid JSON-like output, and similar failures produce HookRunStatus::Failed while leaving should_block = false; run_pre_tool_use_hooks() consequently returns Continue. 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 current ExecCommandHandler/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_closed policy) plus tests for spawn failure, timeout, malformed output, ordinary nonzero exit, and multiple hooks.

Current conclusion: no missing exec_command PreToolUse wiring found on main; no safe local patch for #24453 without an expansion-free Desktop reproduction or an explicit fail-closed policy decision.

charle-z · 1 day ago

Revalidated this on current upstream main (102ae5e2e6ff63d2e7346138182e7eec2fc0711a) and on native Windows with Codex 0.150.0-alpha.13.

The normal Windows agent shell path is not missing PreToolUse dispatch. With a clean hooks.json, matcher Bash, and a hook executable referenced by an absolute literal path, the observed sequence is:

hook/started -> hook returns permissionDecision: deny -> hook/completed with status: blocked -> command is not executed.

The hook receives tool_name: "Bash" and the expected raw tool_input.command. The same result holds through app-server using a trusted hook (hooks/list -> persist currentHash -> trustStatus: trusted) and with permissions: ":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_execution does 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 to exec_command, since that dispatch already exists and works.

Separately, thread/shellCommand is an app-server user-shell escape hatch that does bypass ToolRegistry/PreToolUse and emits commandExecution with source: "userShell". That is a distinct API/product question and should not be conflated with this issue's normal agent exec_command path.

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 PreToolUse dispatch. A global fail-closed policy change would be a separate product/API decision.