PreToolUse hooks: shell operator chaining bypasses command detection
Summary
PreToolUse hooks that inspect tool_input.command for dangerous patterns are trivially bypassed when the LLM agent chains commands with shell operators (&&, ||, ;).
Reproduction
Given a hook that blocks a dangerous git pattern:
def check_command(command):
normalized_cmd = ' '.join(command.strip().split())
if normalized_cmd.startswith('git add'):
# check for dangerous patterns...
The agent can bypass it by chaining with cd:
cd /some/dir && <dangerous-command>
The hook sees the full string, checks if it starts with the target command — it doesn't — and approves. The dangerous subcommand passes through unchecked.
This affects any hook using startswith(), re.match(), or ^-anchored re.search() on the raw command string.
Impact
This is not a theoretical issue. In practice, LLM agents routinely chain cd with the actual command to ensure correct working directory. This bypassed all of my safety hooks until I discovered and patched them.
Current workaround
Hook authors can split on shell operators before matching:
import re
def check_command(command):
subcommands = re.split(r'\s*(?:&&|\|\||;)\s*', command)
for subcmd in subcommands:
should_block, reason = _check_single(subcmd.strip())
if should_block:
return True, reason
return False, None
Suggested platform fix
Consider having the PreToolUse hook system pre-split chained commands and invoke the hook once per subcommand (or at minimum, document this pitfall prominently). This would eliminate the entire class of bypass for all hook authors rather than requiring each one to independently discover and fix it.
Alternatively, adding a prominent warning in the hooks documentation about command chaining would help hook authors avoid this pattern.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗