Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save cruftyoldsysadmin/84b2c66ddd0fa170a840fc0cb649612b to your computer and use it in GitHub Desktop.

Select an option

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
# 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