agent/restrict-bash was a well-built allowlist โ 40 entries, deny-by-default, correct compound handling โ with zero registrations in hooks.json. Its unit tests passed for months against a control no session was ever subject to. This change makes it fire for the first time, and everything below was discovered in the act of doing that.
sync-bash-dispatcher "because that array demonstrably fires." It does fire โ behind an if gate admitting 13 command prefixes. Handing a deny-by-default allowlist that view inverts it: 28 of 32 allowlisted head-commands are never evaluated, and ssh, wget, nc, scp pass silently. An enumerated gate cannot front an allowlist โ whatever is not enumerated is never judged. So the gate is deleted, not widened, which is also the entirety of #3438.
restrict-bash timing entry appears." It would have โ git status reaches the hook. A green control test beside an ineffective control is the exact failure this issue exists to end, so case 4 of the new suite deliberately asserts on commands outside the old 13 prefixes.
| # | Finding | Why it matters |
|---|---|---|
| 1 | The if gate โ deleted from the Bash matcher block | Fixes #3438 too: network-egress-guard could see 1 of the 7 vectors it documents |
| 2 | Identity from payload, not env. process.env.CLAUDE_AGENT_ID โ input.agent_type ?? input.subagent_type, via normalizeAgentName | The env var has no guarantee inside a global hook; reading 'unknown' would restrict every session or none. ork: prefix stripping is the #3354 defect. |
| 3 | Member order decides the verdict. Moved before network-egress-guard | The dispatcher short-circuits on first non-silent result. Placed after, a restricted agent running nc -l 1234 got ask โ a prompt a human might approve โ instead of deny. Caught by case 4. |
Found while fixing a failing test. Four entries were filed under "Misc read-only":
'pwd', 'date', 'echo', 'which', 'env', 'printenv',
'node -e', 'node --eval', 'python -c', 'python3 -c', โ arbitrary code execution
python -c "import os; os.system(...)" passes a read-only allowlist whose own denial message promises "this agent investigates and reports โ it does not modify the system." Harmless while the control was inert; activating it would have shipped a live bypass on day one. Removed, with a regression test that fails if they return.
restrictBash(input) โ โโ agent = normalizeAgentName(input.agent_type ?? input.subagent_type ?? '') โโ RESTRICTED_AGENTS.has(agent)? โโ no โโโถ outputSilentSuccess() โ 99% of calls โ main thread lands here โโ compound operators? โโโโโโโโโโ yes โโโถ deny โโ allowlist match? โโโโโโโโโโโโโ no โโโโถ deny / yes โโโถ allow + log
This is an allowlist of restricted agents, deliberately not a test for "am I a subagent". A missing or unrecognised identity falls through to today's exact behaviour, so if CC's payload does not carry the field the change degrades to a no-op rather than to restrict-everything or restrict-nothing.
| Measurement | Value | Note |
|---|---|---|
| bare node startup | 33.6 ms | the floor โ unavoidable per hook process |
| dispatcher, benign command | 47.9 ms | ~14 ms is our own member chain |
| PreToolUse budget | 50 ms | ~2 ms headroom; CI runners are slower |
Removing the gate moves the dispatcher onto every Bash call, not just 13 prefixes. That is required for correctness but it is a real cost, so restrict-bash-budget.test.ts guards the part this change controls โ the in-process member chain โ rather than asserting wall-clock, which would be flaky on CI. My first two attempts to measure this were wrong (one spawned two python processes per iteration; the next used $SECONDS, granularity 1s, and reported "0ms"). The 47.9 figure is the third attempt, with timing outside the loop.
validate-counts.sh PASSED ยท typecheck clean ยท zero build driftagent_type on a real subagent Bash call โ that needs a live spawn and a timing entry in production telemetry. If CC does not send it, this change is a no-op rather than a regression, by the design above. The honest next check is a real restricted-agent run with a restrict-bash entry in the timing log.