Created
December 6, 2025 08:52
-
-
Save cruftyoldsysadmin/84b2c66ddd0fa170a840fc0cb649612b to your computer and use it in GitHub Desktop.
Claude Code: Custom Bash Permission Hook - Auto-approve, deny, or prompt for commands
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Claude Code: Custom Bash Permission Hook | |
| A minimal example showing how to create a custom permission hook that auto-approves, denies, or prompts for Bash commands in Claude Code. | |
| ## How It Works | |
| When Claude Code wants to run a Bash command, it triggers a `PermissionRequest` event. Your hook script receives JSON on stdin and decides: | |
| | Output | Result | | |
| |--------|--------| | |
| | JSON with `"behavior": "allow"` | Command runs immediately | | |
| | JSON with `"behavior": "deny"` | Command blocked with message | | |
| | No output | User sees approval prompt | | |
| ## Files | |
| ### 1. `hooks.json` | |
| Place in your plugin's `hooks/` directory or `~/.claude/hooks.json`: | |
| ```json | |
| { | |
| "hooks": { | |
| "PermissionRequest": [ | |
| { | |
| "matcher": "Bash", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "/path/to/bash-permission-handler.sh" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } | |
| ``` | |
| > **Note**: Use `${CLAUDE_PLUGIN_ROOT}` in plugins to reference the plugin directory: | |
| > ```json | |
| > "command": "${CLAUDE_PLUGIN_ROOT}/scripts/bash-permission-handler.sh" | |
| > ``` | |
| ### 2. `bash-permission-handler.sh` | |
| ```bash | |
| #!/bin/bash | |
| # Bash permission handler for Claude Code | |
| # | |
| # Input: JSON on stdin with structure {"tool_input": {"command": "..."}} | |
| # Output: JSON decision or nothing (prompts user) | |
| set -e | |
| # Read JSON input | |
| input=$(cat) | |
| # Extract command using jq | |
| command=$(echo "$input" | jq -r '.tool_input.command // empty') | |
| # No command = prompt user | |
| [ -z "$command" ] && exit 0 | |
| # Extract actual command name, skipping env var prefixes like VAR=value | |
| # Example: "AWS_PROFILE=prod aws s3 ls" -> "aws" | |
| first_word=$(echo "$command" | awk '{for(i=1;i<=NF;i++){if(index($i,"=")==0){print $i;exit}}}') | |
| # ============================================================================= | |
| # DENY LIST - Block dangerous patterns | |
| # ============================================================================= | |
| # Block rm -rf on root or home | |
| if echo "$command" | grep -qE 'rm\s+(-rf|-fr)\s+(/|~|\$HOME)'; then | |
| echo '{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "deny", "message": "Blocked: dangerous rm command targeting root or home"}}}' | |
| exit 0 | |
| fi | |
| # Block sudo | |
| if [ "$first_word" = "sudo" ]; then | |
| echo '{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "deny", "message": "Blocked: sudo requires manual approval"}}}' | |
| exit 0 | |
| fi | |
| # ============================================================================= | |
| # ALLOW LIST - Auto-approve safe commands | |
| # ============================================================================= | |
| allowed_commands=( | |
| # Read-only filesystem | |
| "ls" "cat" "head" "tail" "find" "grep" "wc" | |
| # Development tools | |
| "git" "python3" "pip" "npm" "node" | |
| # AWS CLI | |
| "aws" "awslocal" | |
| # Common utilities | |
| "echo" "jq" "curl" "date" | |
| ) | |
| for allowed in "${allowed_commands[@]}"; do | |
| if [ "$first_word" = "$allowed" ]; then | |
| echo '{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "allow"}}}' | |
| exit 0 | |
| fi | |
| done | |
| # ============================================================================= | |
| # UNKNOWN - No output means Claude Code prompts the user | |
| # ============================================================================= | |
| exit 0 | |
| ``` | |
| Make it executable: | |
| ```bash | |
| chmod +x bash-permission-handler.sh | |
| ``` | |
| ## Input JSON Structure | |
| Your script receives JSON like this on stdin: | |
| ```json | |
| { | |
| "tool_name": "Bash", | |
| "tool_input": { | |
| "command": "AWS_PROFILE=prod aws s3 ls", | |
| "description": "List S3 buckets", | |
| "timeout": 120000 | |
| } | |
| } | |
| ``` | |
| ## Output JSON Structure | |
| ### Allow | |
| ```json | |
| { | |
| "hookSpecificOutput": { | |
| "hookEventName": "PermissionRequest", | |
| "decision": { | |
| "behavior": "allow" | |
| } | |
| } | |
| } | |
| ``` | |
| ### Deny | |
| ```json | |
| { | |
| "hookSpecificOutput": { | |
| "hookEventName": "PermissionRequest", | |
| "decision": { | |
| "behavior": "deny", | |
| "message": "Reason shown to user" | |
| } | |
| } | |
| } | |
| ``` | |
| ### Prompt User | |
| Output nothing and exit 0. | |
| ## Advanced: Git Command Handling | |
| Handle git specially to allow most commands but block dangerous ones: | |
| ```bash | |
| if [ "$first_word" = "git" ]; then | |
| # Block dangerous git patterns | |
| if echo "$command" | grep -qE 'git\s+reset\s+--hard'; then | |
| echo '{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "deny", "message": "Blocked: git reset --hard discards changes"}}}' | |
| exit 0 | |
| fi | |
| if echo "$command" | grep -qE 'git\s+push\s+.*--force'; then | |
| echo '{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "deny", "message": "Blocked: force push rewrites history"}}}' | |
| exit 0 | |
| fi | |
| # Allow all other git commands | |
| echo '{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "allow"}}}' | |
| exit 0 | |
| fi | |
| ``` | |
| ## Tips | |
| 1. **Order matters**: Check deny patterns before allow patterns | |
| 2. **Use `grep -qE`**: For regex pattern matching on the full command | |
| 3. **Handle env vars**: Commands often start with `VAR=value cmd args` | |
| 4. **Fail open**: Unknown commands prompt the user (safest default) | |
| 5. **Test with `jq`**: Ensure jq is installed for JSON parsing | |
| ## Dependencies | |
| - `bash` | |
| - `jq` (for JSON parsing) | |
| - `awk` (for word extraction) | |
| - `grep` (for pattern matching) | |
| ## License | |
| MIT - Use freely in your own Claude Code plugins. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment