claude-bash-approve
A command approval runtime for Claude Code and OpenCode. It auto-approves safe Bash commands, repo-scoped Read/Grep calls for Claude Code, and blocks dangerous operations. Written in Go for fast startup.
A reusable pi package is also included under packages/pi-bash-approve/ for protecting pi bash, read, grep, find, and ls tool calls via the same Go runtime.
Install
Use unified installer:
python3 install.py install --target claude
python3 install.py install --target opencode
python3 install.py install --target opencode --scope project
python3 install.py install --target opencode --scope both
python3 install.py install --target codex
python3 install.py install --target codex --scope project
python3 install.py install --target codex --scope both
python3 install.py install --target all
For opencode and codex, default --scope is global.
Uninstall uses same entrypoint:
python3 install.py uninstall --target claude
python3 install.py uninstall --target opencode --scope both
python3 install.py uninstall --target codex --scope both
python3 install.py uninstall --target all
Installer requirements:
Shared runtime installs to $XDG_DATA_HOME/claude-bash-approve when XDG_DATA_HOME is an absolute path, else ~/.local/share/claude-bash-approve.
Claude Code
In Claude Code:
/plugin install github:mariusvniekerk/claude-bash-approve
For local/manual install, run python3 install.py install --target claude.
How it works
When Claude Code is about to run a matched tool call, this hook intercepts it and makes one of four decisions:
- deny — command is blocked (with a reason shown to Claude)
- ask — recognized command, user is prompted to confirm (terminal — no further hooks run) (e.g.
git tag)
- no opinion — hook has nothing to say, exits silently so the next hook in the chain can handle it (e.g.
git push, gh pr create, or unrecognized commands)
- allow — command runs immediately, no prompt
flowchart TD
A["Parse command AST"] --> C{"All segments\nmatched?"}
C -->|No| NOP["**no opinion**\nnext hook in chain"]
C -->|Yes| priority
subgraph priority["Decision priority"]
D{"any segment\ndenied?"} -->|Yes| DENY["**deny**\nblock command"]
D -->|No| E{"any segment\nask?"}
E -->|Yes| ASK["**ask**\nprompt user"]
E -->|No| F{"any segment\nno-opinion?"}
F -->|No| OK["**allow**\nrun immediately"]
end
F -->|Yes| NOP
Commands are parsed into an AST (using mvdan/sh) so chained commands (&&, ||, ;, |), subshells, command substitutions ($(…)), and control flow (if, for, while) are all handled correctly — every segment must be safe for the whole command to be approved.
For Read and Grep, the hook auto-approves only when the referenced paths stay inside the current Git repo or linked worktree root derived from the incoming cwd. Anything outside that boundary falls back to no-opinion.
Wrappers + Commands
The hook uses a compositional model: a command is split into wrappers (prefixes like timeout 30, env, VAR=val) and a core command (like git status, pytest). Both are matched against regex patterns organized into categories.
Alternative installation
git clone https://github.com/mariusvniekerk/claude-bash-approve.git
cd claude-bash-approve
python3 install.py install --target claude
OpenCode installs write plugin files under project/global OpenCode config and point them at shared runtime hook. Codex installs enable hooks and write PermissionRequest hook config pointing at shared runtime hook.
TypeScript support for the OpenCode plugin is enforced via bun --cwd opencode-tester run typecheck.
This repo also includes prek.toml for pre-commit checks:
prek install --prepare-hooks
Configured hooks run:
- builtin whitespace / EOF / large-file checks
uv run python -m unittest -v install_test.py
cd hooks/bash-approve && go test ./...
cd hooks/bash-approve && golangci-lint run ./...
bun run typecheck for OpenCode tester
bun run --cwd packages/pi-bash-approve typecheck for pi package
Manual setup
- Clone this repo:
git clone https://github.com/mariusvniekerk/claude-bash-approve.git
- Add hook to Claude Code settings (
~/.claude/settings.json):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.local/share/claude-bash-approve/run-hook.sh"
}
]
},
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "~/.local/share/claude-bash-approve/run-hook.sh"
}
]
}
]
}
}
- Copy runtime hook bundle into shared data dir: