claude-compound-bash
A Claude Code PreToolUse hook plugin that auto-approves Bash tool calls when every sub-command matches your existing permission rules or is a known-safe command.
See anthropics/claude-code#16561 for the upstream feature request.
The problem
Claude Code checks each Bash tool call against your permission rules before executing. Single commands like git status match fine, but compound commands like git add -A && git commit -m 'fix' are treated as a single opaque string that doesn't match any individual rule -- so you get prompted every time.
How it works
The plugin registers a PreToolUse hook that intercepts every Bash tool call. It parses the command using mvdan.cc/sh/v3 (the parser behind shfmt), walks the full AST to extract every sub-command, and checks each one against your allow/ask/deny patterns from settings files.
Rules are evaluated in the same order as Claude Code: deny → ask → allow. The first matching rule wins.
For each tool call, the hook returns one of three decisions:
allow -- every sub-command is either a known-safe command or matches an allow pattern. The command runs without prompting.
ask -- at least one sub-command matches an ask pattern or isn't in the allow list. Claude Code shows its normal permission prompt.
deny -- a sub-command matches an explicit deny pattern. The tool call is cancelled outright and Claude receives feedback explaining why.
What gets checked
Full AST walk -- commands inside $(...), `...`, <(...), subshells, loops, if-branches, case statements, heredocs, and function bodies are all extracted and checked individually.
For example, echo "there are $(ls | wc -l) files" is parsed into three sub-commands: echo (safe builtin), ls (safe read-only command), and wc (safe read-only command). Each is checked independently.
Dynamic command names rejected -- $CMD args cannot be statically resolved, so it always defers to the prompt.
Deny rules always win -- deny patterns from any scope (user or project settings) block approval, matching Claude Code's own semantics.
Redirect validation
Output redirects (>, >>, &>, etc.) are validated to prevent writes outside allowed directories. This matches the built-in Claude Code Bash tool's behavior.
Auto-allowed:
- Redirects to files inside the current working directory
- Safe devices:
/dev/null, /dev/stdout, /dev/stderr, /dev/stdin, /dev/zero, /dev/random, /dev/urandom
- FD-to-FD duplications:
2>&1, >&2
- Heredocs and here-strings:
<<EOF, <<<
Requires confirmation:
- Redirects outside the working directory (unless in
additionalDirectories)
- Protected paths:
.git/ and .claude/ directories at any nesting level
- Dynamic targets:
> $FILE, > ~/file, > *.log, extglob patterns
- Relative redirects when
cd, pushd, popd, or ln appear in the command (TOCTOU protection)
Not checked: Input redirects (<, <<) are not validated, matching the built-in Bash tool.
Symlinks are fully resolved before path checks to prevent escape attacks.
Additional output directories
To allow redirects to directories outside cwd (e.g., /tmp), add them to Claude Code's standard additionalDirectories setting:
{
"permissions": {
"additionalDirectories": ["/tmp", "/var/log/myapp"]
}
}
This is the same key Claude Code uses to extend its workspace, so no separate configuration is needed. On macOS, /tmp automatically includes /private/tmp and $TMPDIR. Paths must be absolute.
Command safety tiers
Commands are classified into tiers to minimize how many explicit allow rules you need:
Always safe -- auto-approved regardless of arguments. These are read-only commands that cannot cause side effects:
- Shell builtins:
true, false, :, test, [, [[
- Read-only commands:
ls, cat, head, tail, wc, uniq, date, whoami, basename, dirname, realpath, readlink, which, file, stat, uname, id, hostname, tr, cut, rev, seq, sleep, diff, comm, printenv
Safe builtins -- shell builtins that are auto-approved because any commands embedded in their arguments via $(...) or <(...) are extracted and checked separately:
echo, printf, cd, pwd, exit, return, shift, unset, read, pushd, popd, dirs, hash, type, umask, wait, times, ulimit, break, continue, getopts
Require explicit allow pattern -- these can execute arbitrary code or mutate shell behavior:
source, ., eval, exec, set, trap, builtin, alias, unalias, let
Everything else (external commands like git, npm, curl, sed, etc.) requires a matching allow pattern in your settings.
Install
Plugin (recommended)