PreToolUse updatedInput resolution depends on handler completion timing

Open 💬 1 comment Opened Jul 28, 2026 by trevi00

What variant of Codex are you using?

Codex CLI/TUI hooks on current main (49025589b0216b876b1a6a20977536c7d55cdb8b).

What issue are you seeing?

When more than one matching PreToolUse command hook returns updatedInput, the selected rewrite depends on handler completion timing rather than configured order.

The dispatcher runs matching handlers concurrently with FuturesUnordered and records completion order:

[https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/engine/dispatcher.rs#L90-L116](<https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/engine/dispatcher.rs#L90-L116>)

PreToolUse then explicitly selects the rewrite from the handler that finished last:

[https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/events/pre_tool_use.rs#L151-L168](<https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/events/pre_tool_use.rs#L151-L168>)

The existing unit test last_completed_updated_input_wins codifies this behavior:

[https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/events/pre_tool_use.rs#L414-L440](<https://github.com/openai/codex/blob/49025589b0216b876b1a6a20977536c7d55cdb8b/codex-rs/hooks/src/events/pre_tool_use.rs#L414-L440>)

This makes the effective tool input timing-dependent. Identical configuration and model output can execute different commands when handler latency changes because of scheduling, cold starts, I/O, or load.

Minimal reproduction

Create two handlers:

# hook_a.py
import json
import time

time.sleep(0.2)
print(json.dumps({
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "allow",
        "updatedInput": {"command": "echo from-hook-a"},
    }
}))
# hook_b.py
import json

print(json.dumps({
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "allow",
        "updatedInput": {"command": "echo from-hook-b"},
    }
}))

Register both in the same configured order:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "^Bash$",
      "hooks": [
        {"type": "command", "command": "python hook_a.py", "timeout": 5},
        {"type": "command", "command": "python hook_b.py", "timeout": 5}
      ]
    }]
  }
}

Trigger a Bash/shell tool call. A finishes last, so the effective command is echo from-hook-a. Move the sleep(0.2) from A to B without changing hook configuration order; the effective command becomes echo from-hook-b.

This is particularly risky for policy middleware. For example, one handler may remove unsafe arguments while another normalizes a path; completion-order replacement can silently discard either rewrite.

Expected behavior

Multiple rewrites should have deterministic semantics unrelated to wall-clock completion timing.

Preferred contract:

  1. execute rewriting handlers in configured order;
  2. pass each accepted updatedInput as the next handler's tool_input;
  3. preserve blocking dominance: once a handler denies the operation, no rewrite is executed;
  4. return the fully composed input after the final handler.

If sequential composition is not desired, deterministic configured-order precedence or explicit rejection of competing rewrites would still be safer than completion-order selection.

Suggested acceptance tests

  • swapping artificial handler delays does not change the final tool input;
  • configured order determines rewrite composition;
  • the second handler receives the first handler's rewritten input;
  • a deny result prevents execution regardless of other rewrites;
  • invalid rewrites cannot erase a previously valid security transformation;
  • reporting order and execution semantics remain deterministic.

Related but distinct: openai/codex#18491 requests updatedInput support and broader tool coverage; it does not define conflict or composition semantics for multiple rewriting handlers. openai/codex#15266 concerns ordering between different lifecycle events, not multiple handlers for one event.

I can prepare a focused implementation and regression tests if maintainers confirm the intended composition contract and invite a PR.

View original on GitHub ↗

1 Comment

trevi00 · 26 days ago

Reconfirmed against the exact official rust-v0.146.0 tag (e363b08c9175ac1cbe5893615dd2cb9ddf95043b).

The upstream codex-hooks test events::pre_tool_use::tests::last_completed_updated_input_wins passes on that tag. The dispatcher still assigns completion_order from FuturesUnordered, and latest_updated_input selects the rewrite with the greatest completion order.

This confirms that updatedInput resolution remains dependent on handler completion timing in 0.146.0.