Skip to content

preToolUse hooks silently bypassed under parallel tool calls (timeout->allow fallback + serial dispatch) #2893

@torumakabe

Description

@torumakabe

Describe the bug

preToolUse hooks are silently bypassed under parallel tool calls because:

  1. timeoutSec does not terminate the hook process — when a hook takes longer than timeoutSec, the CLI stops waiting but the hook subprocess keeps running. The CLI then proceeds with an implicit allow (fail-open). Any permissionDecision: deny that the hook eventually writes to stdout is discarded.
  2. Hook invocations are serialized — when several tools are invoked in parallel (e.g. from the agent), preToolUse hooks are dispatched one-by-one with ~1.5–4 s gaps, even though the tool calls themselves land within a <100 ms window. Combined with (1), the later items in the queue are the ones most likely to exceed timeoutSec and get fail-open'd.

The net effect is that a security-oriented hook (e.g. a local guard that blocks dangerous commands) becomes probabilistically unreliable as soon as the agent fires more than a handful of tools concurrently. The reported "hook race condition" turns out not to be a race inside the hook, but a silent allow-fallback on timeout.

Affected version

1.0.35-2 (Windows_NT)

Likely affects other recent versions; not tested across matrix.

Steps to reproduce the behavior

Minimal reproducer using a Python 3 hook. Save as slow-deny.py:

import json, sys, time
sys.stdin.read()
time.sleep(15)  # longer than timeoutSec
print(json.dumps({"permissionDecision": "deny", "permissionDecisionReason": "should block"}))

And ~/.copilot/hooks/hooks.json:

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      {
        "type": "command",
        "bash":       "python3 \"$HOME/slow-deny.py\"",
        "powershell": "py -3 \"$HOME\\slow-deny.py\"",
        "timeoutSec": 10
      }
    ]
  }
}

Start copilot and ask the agent to run any shell command (e.g. echo hi).

Expected: the command is blocked (the hook wrote deny, even if late).
Actual: the command executes. The hook logs permissionDecision: deny after the CLI has already moved on.

A second reproducer shows the serialization behavior: ask the agent to run ~5 shell commands in the same turn. Logging the hook's start time (datetime.now()) shows the hooks firing at +0 s, +1.5 s, +4.6 s, +7.1 s, +11.1 s despite the tool calls all starting within ~100 ms.

Expected behavior

At least one of:

  • timeoutSec should kill the hook process (so subsequent failure modes are visible), and a timed-out hook should fall back to deny (fail-closed) — not to an implicit allow.
  • Provide an explicit configuration knob (e.g. onTimeout: "deny" | "allow") so operators can opt into fail-closed semantics.
  • Document the current behavior clearly; right now the docs describe timeoutSec without specifying the fallback direction.

Ideally also: dispatch preToolUse hooks in parallel rather than serializing them, since each tool call is independent.

Additional context

  • OS: Windows 11, powershell invocation path
  • Reproduced with minimal py -3 hook (no uv, no third-party dependencies), so the latency is CLI-side scheduling rather than hook startup cost
  • Observations:
    • dur_stdout_written - dur_invoked for the slow hook is ~15 s (full sleep), confirming the process is not killed at the 10 s timeout
    • For 5 parallel tool invocations, hook start times were spaced 1.5–4 s apart — consistent with a serial queue on the CLI side
    • The bypass probability grows with parallelism, matching community reports of "preToolUse race condition under high concurrency"
  • Security impact: any user who relies on preToolUse hooks for safety (e.g. blocking writes to sensitive paths, blocking dangerous URLs) silently loses that protection under high concurrency

Happy to share a full minimal repro repo or detailed logs on request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:permissionsTool approval, security boundaries, sandbox mode, and directory restrictionsarea:pluginsPlugin system, marketplace, hooks, skills, extensions, and custom agents

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions