claude-bash-approve
Health Warn
- No license — Repository has no license file
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 7 GitHub stars
Code Pass
- Code scan — Scanned 8 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
This tool is a Claude Code hook that automatically approves, denies, or prompts the user for Bash commands based on their safety. It parses commands into an AST to evaluate chained commands, wrappers, and control flows before deciding whether to let them execute.
Security Assessment
Risk: Low
The tool functions specifically by evaluating and executing shell commands via Claude's PreToolUse hook. It does not make external network requests, nor does it store or transmit your data. The automated code scan checked 8 files and found no dangerous patterns, hardcoded secrets, or malicious payloads. Its core mechanism is designed to strictly enhance your security by acting as a gatekeeper for terminal commands.
Quality Assessment
The project is currently active, with its most recent push happening today. However, it has very low community visibility, evidenced by only 7 GitHub stars. The most significant drawback is the complete lack of a license file. Without an open-source license, the software is technically proprietary, meaning you have no explicit legal permission to use, modify, or distribute the code.
Verdict
Use with caution — the code itself appears safe, but the lack of a formal software license presents a legal ambiguity for professional or commercial use.
An alternative take on permissions for claude code.
claude-bash-approve
A Claude Code PreToolUse hook that auto-approves safe Bash commands and blocks dangerous ones. Written in Go for fast startup.
Install
In Claude Code:
/plugin install github:mariusvniekerk/claude-bash-approve
That's it. The hook registers automatically and the Go binary compiles on first use. Requires Go 1.25+.
How it works
When Claude Code is about to run a Bash command, 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.
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
install.sh
git clone https://github.com/mariusvniekerk/claude-bash-approve.git
cd claude-bash-approve
./install.sh
Builds the binary, creates ~/.claude/settings.json if needed, and adds the hook. Pass --force to merge into an existing settings file (requires jq).
Manual setup
- Clone this repo:
git clone https://github.com/mariusvniekerk/claude-bash-approve.git
- Add the hook to your Claude Code settings (
~/.claude/settings.json):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/claude-bash-approve/hooks/bash-approve/run-hook.sh"
}
]
}
]
}
}
Replace /path/to/ with the actual path to your clone.
- The hook auto-compiles on first run. The
run-hook.shshim rebuilds the Go binary whenever source files change, so there's no manual build step.
Configuration
Command categories are configured in hooks/bash-approve/categories.yaml. When this file is absent or empty, all commands are approved (with some exceptions noted below).
Enabled / Disabled
# Approve everything except git push
enabled:
- all
disabled:
- git push
# Only approve git and shell commands
enabled:
- git
- shell
disabled always overrides enabled — use it to carve out exceptions.
Default decisions by command
Most matched commands are auto-approved. Some have different defaults:
| Decision | Commands |
|---|---|
| deny (blocked, reason shown to Claude) | git stash, git revert, git reset --hard, git checkout ., git clean -f, rm -r, go mod vendor, roborev tui |
| ask (terminal, user prompted) | git tag |
| no-opinion (deferred to next hook) | git push, jj git push, gh pr create, go mod init |
To override a default, add the specific command name to enabled or disabled.
Available categories
Coarse groups (enable/disable entire ecosystems):
wrapper, git, jj, python, node, rust, make, shell, gh, go, kubectl, gcloud, bq, aws, acli, roborev, docker, ruby, brew, shellcheck, grpcurl
Fine-grained names (within each group):
| Group | Names |
|---|---|
| wrapper | timeout, nice, env, env vars, .venv, bundle exec, rtk proxy, command, node_modules/.bin, absolute path |
| git | git read op, git write op, git push, git tag, git destructive (git stash, git revert, git reset --hard, git checkout ., git clean -f) |
| jj | jj read op, jj write op, jj git push |
| python | pytest, python, ruff, uv, uvx |
| node | npm, npx, node -e, bun, bunx, vitest |
| rust | cargo, maturin |
| shell | read-only, touch, mkdir, cp -n, ln -s, shell builtin, shell vars, process mgmt, eval, echo, cd, source, sleep, var assignment, shell destructive (rm -r) |
| go | go, go mod vendor, go mod init, golangci-lint, ginkgo |
| gh | gh read op, gh pr create, gh write op, gh api |
| kubectl | kubectl read op, kubectl write op, kubectl port-forward, kubectl exec, kubectl cp |
| docker | docker, docker compose, docker-compose |
| ruby | rspec, rake, ruby, rails, bundle, gem, rubocop, solargraph, standardrb |
See categories.yaml for the full reference with examples.
Telemetry
Every decision is logged to a local SQLite database (telemetry.db, next to the binary). This lets you review what the hook approved, denied, or passed through:
sqlite3 hooks/bash-approve/telemetry.db "SELECT ts, decision, command, reason FROM decisions ORDER BY ts DESC LIMIT 20"
Telemetry is best-effort — if the database can't be opened or written to, the hook continues normally.
Debugging
Test the hook directly by piping JSON to stdin:
echo '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | \
go run ./hooks/bash-approve/
Output is a JSON object with the decision:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"git read op"}}
No output (exit 0) means the hook has no opinion.
Running tests
cd hooks/bash-approve
go test -v ./...
Discovering new patterns
The project includes a Claude Code skill (.claude/skills/bash-approve-telemetry/) that queries the telemetry database to find commands that need new rules. Ask Claude to "check the telemetry for approval candidates" or invoke /bash-approve-telemetry.
Adding new commands
- Add a
NewPattern(...)entry toallCommandPatternsorallWrapperPatternsinhooks/bash-approve/rules.go - Choose the right decision:
allow(default) — auto-approveWithDecision("deny")+WithDenyReason("...")— block with reasonWithDecision("ask")— terminal prompt to userWithDecision("")— no opinion, defer to next hook
- Add test cases in
main_test.go - Update the category listing in
categories.yamlif introducing a new group - Run
go test ./...
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found