github / github/copilot-cli

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

Open
#2,893 2 comments 0 reactions 0 assignees View on GitHub
area:permissions area:plugins
Dominant language
Shell
Stars
11.2k
Forks
1.9k
Avg merge
14h 16m
Merged PRs (30d)
6

Description

## 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`:

```python
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`:

```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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.