Skip to content

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

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions