Auto-approve compound Bash commands in Claude Code
npx claudepluginhub oryband/claude-code-auto-approveAuto-approve compound Bash commands (pipes, chains, subshells) by parsing each segment via shfmt AST and checking against allow/deny lists
Share bugs, ideas, or general feedback.
A Claude Code hook that auto-approves compound Bash commands when every sub-command is in your allow list and none are in your deny list.
Claude Code matches Bash(cmd *) permissions against the full command string. ls | grep foo doesn't match Bash(ls *) or Bash(grep *), so you get prompted even though both commands are individually allowed. Same for nvm use && yarn test, git log | head, mkdir -p dir && cd dir, etc.
This hook parses compound commands into segments and checks each one.
Requires bash 4.3+ (auto-detected; re-execs with Homebrew bash on macOS if needed), shfmt, and jq.
brew install shfmt jq
Copy the script somewhere and register it in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "~/.claude/scripts/approve-compound-bash.sh",
"timeout": 3
}]
}]
},
"permissions": {
"allow": [
"Bash(ls *)", "Bash(grep *)", "Bash(git *)" // ...
],
"deny": [
"Bash(git push --force *)", "Bash(rm -rf / *)" // ...
]
}
}
The hook reads permissions from all settings layers (global, global local, project, project local), supports all permission formats (Bash(cmd *), Bash(cmd:*), Bash(cmd)), and strips env var prefixes (NODE_ENV=prod npm test matches npm).
Simple commands (no |, &, ;, `, $() are checked directly against your prefix lists. No parsing overhead.
Compound commands are parsed into a JSON AST by shfmt, walked by a jq filter that extracts every sub-command (including inside $(...), <(...), subshells, if/for/while/case bodies, bash -c arguments, etc.), then each segment is checked.
Three outcomes:
On any error the hook falls through. It never approves something it can't fully analyze.
Extract sub-commands from a compound command:
echo 'nvm use && yarn test' | ./approve-compound-bash.sh parse
# nvm use
# yarn test
Verbose mode shows matching decisions on stderr:
echo '{"tool_input":{"command":"ls | grep foo"}}' | ./approve-compound-bash.sh --debug
97 tests across parsing, permissions, and security. Requires BATS.
bats test/
bash -c on simple path: bash -c 'echo hello' has no shell metacharacters, so it takes the fast path and matches against the prefix list as-is without recursing into the inner command. Don't add bash, sh, or zsh to your allow list.
Why this hook exists. Claude Code evaluates Bash(cmd *) permissions against the full command string. Compound commands like ls | grep foo or nvm use && yarn test don't match individual prefix rules, so users get prompted even when every sub-command is already allowed. As of March 2026, this remains an open issue with no native fix.
Why bash + shfmt + jq. Claude Code plugins are expected to be transparent and auditable — compiled binaries and obfuscated code are explicitly discouraged. A bash script with well-known dependencies meets this standard. shfmt and jq are both small, fast, and available via standard package managers.
Why shfmt for parsing. shfmt (mvdan.cc/sh) is the most complete and battle-tested bash parser available. Its JSON AST output covers all compound constructs: pipes, chains, subshells, command/process substitution, control flow, and declarations. Alternatives like tree-sitter-bash are designed for editor highlighting rather than semantic analysis, and hand-written parsers (as used by Dippy) trade external dependencies for ongoing maintenance burden and potential correctness gaps.
Why not a compiled binary. A Go rewrite using mvdan.cc/sh as a library would eliminate the shfmt and jq subprocesses, but would produce an opaque binary that conflicts with the plugin ecosystem's source-readability expectations. The current approach adds ~100–150ms of subprocess overhead per compound command, well within Claude Code's hook timeout defaults.