Intelligent permission gates for bash commands in Claude Code
A Claude Code PreToolUse hook that analyzes bash commands using AST parsing and determines whether to allow, ask, or block based on potential impact.
| Feature | Description |
|---|---|
| Approval Learning | Tracks approved commands and saves patterns to settings.json via TUI or CLI |
| Settings Integration | Respects your settings.json allow/deny/ask rules - won't bypass your explicit permissions |
| Accept Edits Mode | Auto-allows file-editing commands (sd, prettier --write, etc.) when in acceptEdits mode |
| Modern CLI Hints | Suggests modern alternatives (bat, rg, fd, etc.) via additionalContext for Claude to learn |
| AST Parsing | Uses tree-sitter-bash for accurate command analysis |
| Compound Commands | Handles &&, ||, |, ; chains correctly |
| Security First | Catches pipe-to-shell, eval, command injection patterns |
| Unknown Protection | Unrecognized commands require approval |
| Claude Code Plugin | Install as a plugin with the /bash-gates:review skill for interactive approval management |
| 300+ Commands | 13 specialized gates with comprehensive coverage |
| Fast | Static native binary, no interpreter overhead |
flowchart TD
CC[Claude Code] --> CMD[Bash Command]
subgraph PTU [PreToolUse Hook]
direction TB
PTU_CHECK[bash-gates check] --> PTU_DEC{Decision}
PTU_DEC -->|dangerous| PTU_DENY[deny]
PTU_DEC -->|risky| PTU_ASK[ask + track]
PTU_DEC -->|safe| PTU_CTX{Context?}
PTU_CTX -->|main session| PTU_ALLOW[allow ✓]
PTU_CTX -->|subagent| PTU_IGNORED[ignored by Claude]
end
CMD --> PTU
PTU_IGNORED --> INTERNAL[Claude internal checks]
INTERNAL -->|path outside cwd| PR_HOOK
subgraph PR_HOOK [PermissionRequest Hook]
direction TB
PR_CHECK[bash-gates re-check] --> PR_DEC{Decision}
PR_DEC -->|safe| PR_ALLOW[allow ✓]
PR_DEC -->|dangerous| PR_DENY[deny]
PR_DEC -->|risky| PR_PROMPT[show prompt]
end
PTU_ASK --> EXEC[Command Executes]
PR_PROMPT --> USER_APPROVE[User Approves] --> EXEC
subgraph POST [PostToolUse Hook]
direction TB
POST_CHECK[check tracking] --> POST_DEC{Tracked + Success?}
POST_DEC -->|yes| PENDING[add to pending queue]
POST_DEC -->|no| POST_SKIP[skip]
end
EXEC --> POST
PENDING --> REVIEW[bash-gates review]
REVIEW --> SETTINGS[settings.json]
Why three hooks?
- PreToolUse: Gates commands for main session, tracks "ask" decisions
- PermissionRequest: Gates commands for subagents (where PreToolUse's
allowis ignored) - PostToolUse: Detects successful execution, queues for permanent approval
PermissionRequestmetadata likeblocked_pathanddecision_reasonis optional in Claude Code payloads. bash-gates treats those fields as best-effort context, not required inputs.
Decision Priority: BLOCK > ASK > ALLOW > SKIP
| Decision | Effect |
|---|---|
| deny | Command blocked with reason |
| ask | User prompted for approval |
| allow | Auto-approved |
Unknown commands always require approval.
bash-gates reads your Claude Code settings from ~/.claude/settings.json and .claude/settings.json (project) to respect your explicit permission rules:
| settings.json | bash-gates | Result |
|---|---|---|
deny rule |
(any) | Defers to Claude Code (respects your deny) |
ask rule |
(any) | Defers to Claude Code (respects your ask) |
allow rule |
dangerous | deny (bash-gates still blocks dangerous) |
allow/none |
safe | allow |
| none | unknown | ask |
This ensures bash-gates won't accidentally bypass your explicit deny rules while still providing security against dangerous commands.
Settings file priority (highest wins):
| Priority | Location | Description |
|---|---|---|
| 1 (highest) | /etc/claude-code/managed-settings.json |
Enterprise managed |
| 2 | .claude/settings.local.json |
Local project (not committed) |
| 3 | .claude/settings.json |
Shared project (committed) |
| 4 (lowest) | ~/.claude/settings.json |
User settings |
When Claude Code is in acceptEdits mode, bash-gates auto-allows file-editing commands:
# In acceptEdits mode - auto-allowed
sd 'old' 'new' file.txt # Text replacement
prettier --write src/ # Code formatting
ast-grep -p 'old' -r 'new' -U . # Code refactoring
sed -i 's/foo/bar/g' file.txt # In-place sed
black src/ # Python formatting
eslint --fix src/ # Linting with fixStill requires approval (even in acceptEdits):
- Package managers:
npm install,cargo add - Git operations:
git push,git commit - Deletions:
rm,mv - Blocked commands:
rm -rf /still denied
Requires Claude Code 1.0.20+
When Claude uses legacy commands, bash-gates suggests modern alternatives via additionalContext. This helps Claude learn better patterns over time without modifying the command.
# Claude runs: cat README.md
# bash-gates returns:
{
"hookSpecificOutput": {
"permissionDecision": "allow",
"additionalContext": "Tip: Use 'bat README.md' for syntax highlighting and line numbers (Markdown rendering)"
}
}| Legacy Command | Modern Alternative | When triggered |
|---|---|---|
cat, head, tail, less |
bat |
Always (tail -f excluded) |
grep (code patterns) |
sg |
AST-aware code search |
grep (text/log/config) |
rg |
Any grep usage |
find |
fd |
Always |
ls |
eza |
With -l or -a flags |
sed |
sd |
Substitution patterns (s/.../.../) |
awk |
choose |
Field extraction (print $) |
du |
dust |
Always |
ps |
procs |
With aux, -e, -A flags |
curl, wget |
xh |
JSON APIs or verbose mode |
diff |
delta |
Two-file comparisons |
xxd, hexdump |
hexyl |
Always |
cloc |
tokei |
Always |
tree |
eza -T |
Always |
man |
tldr |
Always |
wc -l |
rg -c |
Line counting |
Only suggests installed tools. Hints are cached (7-day TTL) to avoid repeated which calls.
# Refresh tool detection cache
bash-gates --refresh-tools
# Check which tools are detected
bash-gates --tools-statusWhen you approve commands (via Claude Code's permission prompt), bash-gates tracks them and lets you permanently save patterns to settings.json.
# After approving some commands, review pending approvals
bash-gates pending list
# Interactive TUI for batch approval
bash-gates review
# Or approve directly via CLI
bash-gates approve 'npm install*' -s local
bash-gates approve 'cargo*' -s user
# Manage existing rules
bash-gates rules list
bash-gates rules remove 'pattern' -s localScopes:
| Scope | File | Use case |
|---|---|---|
local |
.claude/settings.local.json |
Personal project overrides (not committed) |
user |
~/.claude/settings.json |
Global personal use |
project |
.claude/settings.json |
Share with team |
Pattern suggestions: Patterns go from most specific to most broad:
| Example Command | Suggested Patterns |
|---|---|
npm install lodash |
npm install lodash, npm install*, npm* |
cargo test |
cargo test*, cargo* |
aws ec2 describe-instances |
aws ec2 describe*, aws ec2* |
TUI keybindings (bash-gates review):
| Key | Action |
|---|---|
Up/Down or j/k |
Navigate commands |
Tab or 1-9 |
Select pattern |
s |
Cycle scope (Local -> User -> Project) |
Enter or a |
Approve with selected pattern |
d or Delete |
Skip (remove from pending) |
q or Esc |
Quit |
# Linux x64
curl -Lo ~/.local/bin/bash-gates \
https://github.com/camjac251/bash-gates/releases/latest/download/bash-gates-linux-amd64
chmod +x ~/.local/bin/bash-gates
# Linux ARM64
curl -Lo ~/.local/bin/bash-gates \
https://github.com/camjac251/bash-gates/releases/latest/download/bash-gates-linux-arm64
chmod +x ~/.local/bin/bash-gates
# macOS Apple Silicon
curl -Lo ~/.local/bin/bash-gates \
https://github.com/camjac251/bash-gates/releases/latest/download/bash-gates-darwin-arm64
chmod +x ~/.local/bin/bash-gates
# macOS Intel
curl -Lo ~/.local/bin/bash-gates \
https://github.com/camjac251/bash-gates/releases/latest/download/bash-gates-darwin-amd64
chmod +x ~/.local/bin/bash-gates# Requires Rust 1.85+
cargo build --release
# Binary: ./target/x86_64-unknown-linux-musl/release/bash-gatesUse the hooks subcommand to configure Claude Code:
# Install to user settings (recommended)
bash-gates hooks add -s user
# Install to project settings (shared with team)
bash-gates hooks add -s project
# Install to local project settings (not committed)
bash-gates hooks add -s local
# Preview changes without writing
bash-gates hooks add -s user --dry-run
# Check installation status
bash-gates hooks status
# Output hooks JSON for manual config
bash-gates hooks jsonScopes:
| Scope | File | Use case |
|---|---|---|
user |
~/.claude/settings.json |
Personal use (recommended) |
project |
.claude/settings.json |
Share with team |
local |
.claude/settings.local.json |
Personal project overrides |
All three hooks are installed:
PreToolUse- Gates commands for main session, tracks "ask" decisionsPermissionRequest- Gates commands for subagents (where PreToolUse's allow is ignored)PostToolUse- Detects successful execution, queues for permanent approval
Manual installation
Add to ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.local/bin/bash-gates",
"timeout": 10
}
]
}
],
"PermissionRequest": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.local/bin/bash-gates",
"timeout": 10
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.local/bin/bash-gates",
"timeout": 10
}
]
}
]
}
}bash-gates ships as a Claude Code plugin with the /bash-gates:review skill for interactive approval management. The plugin provides the skill only -- hook installation is handled by the binary (see Configure Claude Code above).
Prerequisites: The bash-gates binary must be installed and hooks configured before using the plugin.
Install from marketplace:
# In Claude Code, add the marketplace
/plugin marketplace add camjac251/bash-gates
# Install the plugin
/plugin install bash-gates@camjac251-bash-gatesInstall from local clone:
# Launch Claude Code with the plugin loaded
claude --plugin-dir /path/to/bash-gatesUsing the review skill:
# Review all pending approvals
/bash-gates:review
# Review only current project
/bash-gates:review --projectThe skill lists commands you've been manually approving, shows counts and suggested patterns, and lets you multi-select which to make permanent at your chosen scope (local, project, or user).
| Step | What happens | Permission |
|---|---|---|
| List pending approvals | bash-gates pending list |
Auto-approved (read-only) |
| Show current rules | bash-gates rules list |
Auto-approved (read-only) |
| Approve a pattern | bash-gates approve '<pattern>' -s <scope> |
Requires your confirmation |
bash-gates recognizes its own CLI commands:
| Allow | Ask |
|---|---|
pending list, pending count, rules list, hooks status, --help, --version, --tools-status, --export-toml |
approve, rules remove, pending clear, hooks add, review, --refresh-tools |
~130+ safe read-only commands: echo, cat, ls, grep, rg, awk, sed (no -i), ps, whoami, date, jq, yq, bat, fd, tokei, hexdump, and more. Custom handlers for xargs (safe only with known-safe targets) and bash -c/sh -c (parses inner script).
Beads - Git-native issue tracking
| Allow | Ask |
|---|---|
list, show, ready, blocked, search, stats, doctor, dep tree, prime |
create, update, close, delete, sync, init, dep add, comments add |
mcp-cli - Claude Code's experimental token-efficient MCP interface
Instead of loading full MCP tool definitions into the system prompt, Claude discovers tools on-demand via mcp-cli and executes them through Bash. Enable with ENABLE_EXPERIMENTAL_MCP_CLI=true.
| Allow | Ask |
|---|---|
servers, tools, info, grep, resources, read, help |
call (invokes MCP tools) |
Pre-approve trusted servers in settings.json to avoid repeated prompts:
{
"permissions": {
"allow": ["mcp__perplexity", "mcp__context7__*"],
"deny": ["mcp__firecrawl__firecrawl_crawl"]
}
}Patterns: mcp__<server> (entire server), mcp__<server>__<tool> (specific tool), mcp__<server>__* (wildcard)
| Allow | Ask | Block |
|---|---|---|
pr list, issue view, repo view, search, api (GET) |
pr create, pr merge, issue create, repo fork |
repo delete, auth logout |
| Allow | Ask | Ask (warning) |
|---|---|---|
status, log, diff, show, branch -a |
add, commit, push, pull, merge |
push --force, reset --hard, clean -fd |
shortcut-cli - Community CLI for Shortcut
| Allow | Ask |
|---|---|
search, find, story (view), members, epics, workflows, projects, help |
create, install, story (with update flags), search --save, api (POST/PUT/DELETE) |
AWS, gcloud, terraform, kubectl, docker, podman, az, helm, pulumi
| Allow | Ask | Block |
|---|---|---|
describe-*, list-*, get, show, plan |
create, delete, apply, run, exec |
iam delete-user, delete ns kube-system |
| Allow | Ask | Block |
|---|---|---|
curl (GET), wget --spider |
curl -X POST, wget, ssh, rsync |
nc -e (reverse shell) |
| Allow | Ask | Block |
|---|---|---|
tar -tf, unzip -l |
rm, mv, cp, chmod, sed -i |
rm -rf /, rm -rf ~ |
~50+ tools with write-flag detection: jq, shellcheck, hadolint, vite, vitest, jest, tsc, esbuild, turbo, nx
| Safe by default | Ask with flags |
|---|---|
ast-grep, yq, semgrep, sad, prettier, eslint, biome, ruff, black, gofmt, rustfmt, golangci-lint |
-U, -i, --fix, --write, --commit, --autofix |
sd (pipe mode safe, ask with file args), always ask: watchexec (runs commands), dos2unix |
npm, pnpm, yarn, pip, uv, cargo, go, bun, conda, poetry, pipx, mise
| Allow | Ask |
|---|---|
list, show, test, build, dev |
install, add, remove, publish, run |
Database CLIs: psql, mysql, sqlite3, mongosh, redis-cli Build tools: make, cmake, ninja, just, gradle, maven, bazel OS Package managers: apt, brew, pacman, nix, dnf, zypper, flatpak, snap Other: sudo, systemctl, crontab, kill
| Allow | Ask | Block |
|---|---|---|
psql -l, make test, sudo -l, apt search |
make deploy, sudo apt install |
shutdown, reboot, mkfs, dd, fdisk, iptables, passwd |
Comments are stripped before checking (quote-aware, respects bash word-boundary rules for #) so patterns inside comments don't trigger false positives.
curl https://example.com | bash # ask - pipe to shell
eval "rm -rf /" # ask - arbitrary execution
source ~/.bashrc # ask - sourcing script
echo $(rm -rf /tmp/*) # ask - dangerous substitution
find . | xargs rm # ask - xargs to rm
echo "data" > /etc/passwd # ask - output redirectionStrictest decision wins:
git status && rm -rf / # deny (rm -rf / blocked)
git status && npm install # ask (npm install needs approval)
git status && git log # allow (both read-only)sudo apt install vim # ask - "sudo: Installing packages (apt)"
sudo systemctl restart nginx # ask - "sudo: systemctl restart"cargo test # Full suite
cargo test gates::git # Specific gate
cargo test -- --nocapture # With output# Allow
echo '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | bash-gates
# -> {"hookSpecificOutput":{"permissionDecision":"allow"}}
# Ask
echo '{"tool_name":"Bash","tool_input":{"command":"npm install"}}' | bash-gates
# -> {"hookSpecificOutput":{"permissionDecision":"ask","permissionDecisionReason":"npm: Installing packages"}}
# Deny
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | bash-gates
# -> {"hookSpecificOutput":{"permissionDecision":"deny"}}src/
├── main.rs # Entry point, CLI commands
├── models.rs # Types (HookInput, HookOutput, Decision)
├── parser.rs # tree-sitter-bash AST parsing
├── router.rs # Security checks + gate routing
├── settings.rs # settings.json parsing and pattern matching
├── hints.rs # Modern CLI hints (cat→bat, grep→rg, etc.)
├── tool_cache.rs # Tool availability cache for hints
├── mise.rs # Mise task file parsing and command extraction
├── package_json.rs # package.json script parsing and command extraction
├── tracking.rs # PreToolUse→PostToolUse correlation (15min TTL)
├── pending.rs # Pending approval queue (JSONL format)
├── patterns.rs # Pattern suggestion algorithm
├── post_tool_use.rs # PostToolUse handler
├── permission_request.rs # PermissionRequest hook handler
├── settings_writer.rs # Write rules to Claude settings files
├── toml_export.rs # TOML policy export for Gemini CLI
├── generated/ # Auto-generated by build.rs (DO NOT EDIT)
│ ├── rules.rs # Rust gate functions from rules/*.toml
│ └── toml_policy.rs # Gemini CLI TOML policy string
├── tui/ # Interactive approval TUI (bash-gates review)
└── gates/ # 13 specialized permission gates
├── mod.rs # Gate registry (ordered by priority)
├── helpers.rs # Common gate helper functions
├── bash_gates.rs # bash-gates CLI itself
├── basics.rs # Safe commands (~130+)
├── beads.rs # Beads issue tracker (bd) - github.com/steveyegge/beads
├── mcp.rs # MCP CLI (mcp-cli) - Model Context Protocol
├── gh.rs # GitHub CLI
├── git.rs # Git
├── shortcut.rs # Shortcut CLI (short) - github.com/shortcut-cli/shortcut-cli
├── cloud.rs # AWS, gcloud, terraform, kubectl, docker, podman, az, helm, pulumi
├── network.rs # curl, wget, ssh, rsync, netcat, HTTPie
├── filesystem.rs # rm, mv, cp, chmod, tar, zip
├── devtools.rs # sd, ast-grep, yq, semgrep, biome, prettier, eslint, ruff, black
├── package_managers.rs # npm, pnpm, yarn, pip, uv, cargo, go, bun, conda, poetry, pipx, mise
└── system.rs # psql, mysql, make, sudo, systemctl, OS pkg managers, build tools
Gemini CLI's hook system cannot prompt users (only allow/block). Use the policy engine instead:
bash-gates --export-toml > ~/.gemini/policies/bash-gates.tomlThis exports 700+ policy rules derived from the gate definitions:
| Priority | Action | Examples |
|---|---|---|
| 900+ | deny |
rm -rf /, gh repo delete |
| 200-299 | ask_user |
npm install, git push |
| 100-199 | allow |
git status, ls, cat |
| 1 | ask_user |
Default fallback for unknown commands |