My living permissions config for Claude Code. 3-layer defense that auto-approves safe commands, prompts for dangerous ones, and enforces OS-level sandboxing.
Out of the box, Claude Code prompts for permission on nearly every bash command. Compound commands (&&, |, ;) are always unique strings that never match glob patterns, so you end up approving everything manually.
settings.json auto-allows read tools (Read, Glob, Grep, WebSearch, WebFetch, Task), Bash, and all first-party Claude MCP servers (mcp__claude_ai_*). Denies reads to ~/.ssh, ~/.aws, ~/.gnupg. File edits (Edit) and third-party MCP servers still prompt.
check-bash-command.sh runs before every bash command. It splits compound commands into subcommands (&&, ||, ;, |, $()) and checks each against ~90 deny regex patterns. Dangerous commands prompt for approval (you can still say yes). Everything else auto-approves.
What it catches: rm -rf, sudo, force push, git reset --hard, gh repo delete, brew uninstall, 1Password mutations, cloud delete ops (terraform destroy, kubectl delete, aws deletions), database drops, curl | sh, osascript, keychain access, ssh/scp, curl -X DELETE, eval, npx, and ~70 more patterns.
What it allows through: ls, git status/diff/log/add/commit/push, brew install, pip install, npm install/run, op item list/get, docker ps/build, gcloud auth, and all other non-destructive commands.
Kernel-level enforcement. Even if layers 1 and 2 are bypassed:
- Filesystem: write only to working directory and
/tmp - Network: only allowed domains (GitHub, npm, PyPI, Google, Anthropic, Homebrew, 1Password)
- Reads blocked:
~/.ssh,~/.aws,~/.gnupg
mkdir -p ~/.claude/hooks
cp check-bash-command.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/check-bash-command.shcp settings.json ~/.claude/settings.jsonThat's it. ~/.claude/settings.json applies globally to all Claude Code sessions on your machine. No per-project config needed.
Start a new Claude Code session and run /permissions to confirm your rules are active.
| File | Purpose |
|---|---|
settings.json |
Permissions, sandbox config, hook registration. Copy to ~/.claude/settings.json |
check-bash-command.sh |
PreToolUse hook with ~90 deny patterns. Copy to ~/.claude/hooks/ |
test-hook.sh |
168-test suite. Run to verify the hook works |
analysis.md |
Research notes, tradeoffs, red team analysis |
./test-hook.shExpected output: 168 passed, 0 failed
Edit check-bash-command.sh and add a tuple to DENY_PATTERNS:
(r'\bmy-dangerous-command\s', 'my-dangerous-command'),Then add a test to test-hook.sh:
check ask "my-dangerous-command" "my-dangerous-command --help"Edit settings.json and add to sandbox.network.allowedDomains:
"my-api.example.com"Some tools (like gh, op, pyenv) don't work inside the sandbox due to TLS or socket issues. Add them to sandbox.excludedCommands:
"excludedCommands": ["gh", "op", "pyenv"]They still go through the hook.
The hook is a convenience safety net, not a security boundary. The OS sandbox is the real security boundary.
Can bypass the hook:
- Variable indirection:
CMD=rm; $CMD -rf . - Brace expansion:
{rm,-rf,.} - Scripting languages:
python3 -c "shutil.rmtree('.')" - Build tools:
make,npm runexecute arbitrary code from project files - Cross-tool attacks: edit a Makefile via
Edittool, then runmake
Caught by OS sandbox (even if hook is bypassed):
- Writes outside working directory
- Network to non-allowed domains
- Reading sensitive directories
Acceptable risk: Working directory damage is recoverable via git checkout.
Claude Code merges settings from multiple sources (highest priority first):
- Managed settings (enterprise/MDM)
- CLI arguments
.claude/settings.local.json(per-project, gitignored).claude/settings.json(per-project, committed)~/.claude/settings.json(global) <-- this repo
Project-level settings can override your global settings. This is useful for stricter rules on sensitive repos.