auto-approve-compound-bash
Auto-approve compound Bash commands (pipes, chains, subshells) by parsing each segment via shfmt AST and checking against allow/deny lists
Open source Open in the app JSON README (API)
About
Auto-approve compound Bash commands (pipes, chains, subshells) by parsing each segment via shfmt AST and checking against allow/deny lists
Details
- Kind
- Plugins
- Topic
- No topic detected
- Publisher
- oryband
- Origin
- marketplace
- Category
- ferramentas
- Stars
- 20
- Forks
- 7
- Open pull requests
- 2
- Last push
- 2026-07-29T20:50:41Z
- Repository state
- arquivado
- Language
- Shell
- License
- MIT
- Added
- 2026-08-30 01:48:58
- Updated
- 2026-08-30 01:48:58
- Origin id
oryband/claude-code-auto-approve/auto-approve-compound-bash
README
> ## ⚠️ Deprecated — archived 2026-07-29
>
> **Please don't install this hook.** Claude Code now solves the problem it was built for, and
> there is an open security issue that will not be fixed here.
>
> **1. Claude Code handles compound commands natively.** It parses a command into segments
> (including redirects) and matches each against your permission rules, failing closed on anything
> it can't statically verify. Hardened in `2.1.98`, `2.1.111`, `2.1.207`, and `2.1.216`. Combined
> with [auto mode](https://code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode)
> (`"permissions": { "defaultMode": "auto" }`), the original friction is addressed in-product.
>
> **2. Open security issue:** [#4](https://github.com/oryband/claude-code-auto-approve/issues/4) —
> redirects are stripped before matching, so an allow-listed `echo` permits
> `echo … >> ~/.ssh/authorized_keys` without a prompt. Deny rules share the blind spot.
>
> **What to do:**
>
> - **Using this hook?** Remove it and rely on native permission rules plus `auto` mode.
> - **Maintaining a fork?** Apply the redirect fix in
> [#5](https://github.com/oryband/claude-code-auto-approve/pull/5) before further use.
> - **Want to block destructive commands?** Use a deny-only guard such as
> [dcg](https://github.com/Dicklesworthstone/destructive_command_guard). Note it is a different
> tool, not a drop-in replacement: it only denies and cannot auto-approve.
>
> Thanks to everyone who filed issues, opened PRs, and forked the project.
>
> Original README below.
---
# approve-compound-bash
A [Claude Code](https://docs.anthropic.com/en/docs/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.
## The problem
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.
## Install
Requires **bash 4.3+** (auto-detected; re-execs with Homebrew bash on macOS if needed), [shfmt](https://github.com/mvdan/sh), and [jq](https://jqlang.github.io/jq/).
```bash
brew install shfmt jq
```
Copy the script somewhere and register it in `~/.claude/settings.json`:
```jsonc
{
"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`).
## How it decides
**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:
- **Approve** — all segments in allow list, none in deny list. Command runs.
- **Deny** — any segment matches the deny list. Command is blocked.
- **Fall through** — segment is unknown (not in allow or deny), or parse failed. Claude Code shows its normal permission prompt.
On any error the hook falls through. It never approves something it can't fully analyze.
## Debugging
Extract sub-commands from a compound command:
```bash
echo 'nvm use && yarn test' | ./approve-compound-bash.sh parse
# nvm use
# yarn test
```
Verbose mode shows matching decisions on stderr:
```bash
echo '{"tool_input":{"command":"ls | grep foo"}}' | ./approve-compound-bash.sh --debug
```
## Testing
97 tests across parsing, permissions, and security. Requires [BATS](https://bats-core.readthedocs.io/).
```bash
bats test/
```
## Known limitations
**`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.
## Design decisions
**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](https://github.com/anthropics/claude-code/issues/29491) [issue](https://github.com/anthropics/claude-code/issues/4236) with no native fix.
**Why bash + shfmt + jq.** Claude Code plugins are expected to be [transparent and auditable](https://code.claude.com/docs/en/discover-plugins) — 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](https://github.com/mvdan/sh) (`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](https://github.com/tree-sitter/tree-sitter-bash) are designed for editor highlighting rather than semantic analysis, and hand-written parsers (as used by [Dippy](https://github.com/ldayton/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.
## Credits
Based on [claude-code-plus](https://github.com/AbdelrahmanHafez/claude-code-plus) (MIT). Key differences: deny list support, active deny for compounds, fast path for simple commands, falls through on empty parse (the original approves), settings layer support, env var stripping, and a test suite.