openai / openai/codex

Interrupted synchronous `PostToolUse` emits `hook/started` without `hook/completed`

Open
#46,765 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app-server bug CLI hooks windows-os
Dominant language
Rust
Stars
125k
Forks
19.5k
PR merge metrics
PR metrics pending

Description

After the interrupted turn has ended, app-server consumers still see the hook run as started, with
no terminal state distinguishing cancellation from continued execution.

Versions

  • Platform: Windows 11
  • Originally observed: codex-cli 0.155.0-alpha.9.2
  • Reproduced with an isolated fixture: codex-cli 0.155.1

The current official documentation says synchronous command hooks are awaited before Codex
continues:
Codex hooks documentation.

Reproduction

The Windows reproduction used an empty working directory containing target.txt:

before

.codex/hooks.json configured one synchronous PostToolUse hook:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "^apply_patch$",
        "hooks": [
          {
            "type": "command",
            "commandWindows": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File .codex\\blocking-post-tool-use.ps1",
            "command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File .codex\\blocking-post-tool-use.ps1",
            "timeout": 180
          }
        ]
      }
    ]
  }
}

.codex/blocking-post-tool-use.ps1 recorded that the hook had started, then blocked:

$ErrorActionPreference = 'Stop'
$payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
$markerPath = Join-Path $PSScriptRoot 'hook-started.json'
$payload | ConvertTo-Json -Depth 20 -Compress |
    Set-Content -LiteralPath $markerPath -Encoding utf8
Start-Sleep -Seconds 180

The Codex app server was launched from that directory with plugins disabled:

codex --dangerously-bypass-hook-trust `
  -c features.plugins=false `
  -c features.hooks=true `
  app-server --stdio

An ephemeral turn applied this patch:

Call apply_patch exactly once with this exact patch and then stop:
*** Begin Patch
*** Update File: target.txt
@@
-before
+after
*** End Patch

After both hook/started(postToolUse, run.id) and .codex/hook-started.json were observed,
turn/interrupt was sent. The relevant event sequence was:

apply_patch
hook/started(postToolUse, run.id)
turn/interrupt
turn/completed(status=interrupted)

In the verified run, the turn reached status interrupted, but no matching hook/completed was
observed for the same run.id during the following five seconds. Plugins were disabled, and this
was the only configured hook.

Requested observable result

A cancelled synchronous hook run is represented by a terminal outcome with the same run.id,
without delivering its output to the model. The existing core/tests/suite/hooks.rs coverage
excludes Windows, where this reproduction was verified.

Source locations

core/src/hook_runtime.rs::run_post_tool_use_hooks publishes HookStarted before awaiting the hook
and publishes HookCompleted only after the await returns. The tool remains non-terminal in
core/src/tools/registry.rs::dispatch_any_with_terminal_outcome during that wait, while
core/src/tools/parallel.rs aborts the non-terminal dispatch task on interruption. Once that task
is dropped, no remaining path publishes HookCompleted for the started run.

Related issues

#46060 covers Windows process-tree cleanup and
missing lifecycle events for non-builtin asynchronous hooks. This report covers a synchronous hook
that publishes hook/started but loses its terminal event after turn interruption.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in core/src/hook_runtime.rs::run_post_tool_use_hooks and trace interruption through core/src/tools/registry.rs::dispatch_any_with_terminal_outcome and core/src/tools/parallel.rs. Review core/tests/suite/hooks.rs, noting its current Windows exclusion, and reproduce the isolated synchronous PostToolUse case. Done means interruption yields a terminal hook outcome with the same run.id, without sending hook output to the model, with Windows coverage.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.