If you use Claude Code with standard permissions, you quickly hit a productivity wall: permission fatigue.
You approve npm test, then git status, then pytest, then ls src/, then cat package.json, each one breaking flow. After a few sessions, your allowlist grows to hundreds of entries, and you still get prompted for new command variants.
Claude Code has a skip permissions mode, but fully auto approving everything is risky. A single hallucinated destructive command can discard work or rewrite remote history.
The practical middle ground is PreToolUse hooks: block or escalate the small set of dangerous actions, allow the rest, and give the agent short, precise feedback so it can recover safely.
What you can achieve with hooks
- Run fast: allow low risk commands without prompts.
- Block the sharp edges: deny destructive commands before they execute.
- Escalate grey areas: require a prompt for actions that are sometimes correct but high impact.
- Guide agent behaviour: return concise reasons that nudge Claude to a safer next step.
Why this is helpful
Allowlists are open ended and grow forever, which creates habituation and reduces real safety. A blocklist is small, stable, and auditable. The key additional benefit is that hooks can return permissionDecisionReason messages that help Claude choose a safer alternative without you stepping in.
Step by step setup
1. Create a hooks directory
mkdir -p ~/.claude/hooks
2. Create a PreToolUse command guard
Create ~/.claude/hooks/block-dangerous-commands.sh:
#!/bin/bash# PreToolUse hook: allow most commands, deny or ask on risky ones.INPUT=$(cat)COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')CMD_LOWER=$(echo "$COMMAND" | tr '[:upper:]' '[:lower:]')deny () { local reason="$1" printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$reason" exit 0}ask () { local reason="$1" printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"%s"}}\n' "$reason" exit 0}# -----------------------------# DENY examples (hard block)# -----------------------------# rm -rf variantsif echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)\b'; then deny "Blocked: recursive force delete is not allowed."fi# git push --forceif echo "$COMMAND" | grep -qE 'git\s+push\s+.*(-f|--force)\b'; then deny "Blocked: force push can rewrite remote history."fi# git reset --hardif echo "$COMMAND" | grep -qE 'git\s+reset\s+--hard\b'; then deny "Blocked: hard reset discards uncommitted changes."fi# destructive SQL (simple heuristic)if echo "$CMD_LOWER" | grep -qE '\b(drop\s+table|drop\s+database|truncate\s+|delete\s+from)\b'; then deny "Blocked: destructive SQL requires explicit human approval."fi# -----------------------------# ASK examples (prompt user)# -----------------------------# merging PRsif echo "$COMMAND" | grep -qE 'gh\s+pr\s+merge\b'; then ask "Confirmation required: PR merge is a high impact change."fi# deployment (example patterns)if echo "$COMMAND" | grep -qE '\b(terraform\s+apply|kubectl\s+apply|helm\s+upgrade)\b'; then ask "Confirmation required: this changes infrastructure or production state."fi# Everything else: allowexit 0
Make it executable:
chmod +x ~/.claude/hooks/block-dangerous-commands.sh
3. Create a sensitive file guard for edits and writes
Create ~/.claude/hooks/protect-sensitive-files.sh:
#!/bin/bash# PreToolUse hook: block edits to sensitive files.INPUT=$(cat)FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')if [ -z "$FILE_PATH" ]; then exit 0fideny () { local reason="$1" printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$reason" exit 0}# Secrets and credentialsif [[ "$FILE_PATH" =~ \.env$ ]] || [[ "$FILE_PATH" =~ \.env\. ]]; then deny "Blocked: .env files may contain secrets."fiif [[ "$FILE_PATH" =~ \.aws/credentials$ ]]; then deny "Blocked: AWS credentials must not be modified by the agent."fi# Git internalsif [[ "$FILE_PATH" =~ /\.git/ ]]; then deny "Blocked: git internals must not be edited directly."fi# Lock filesif [[ "$FILE_PATH" =~ package-lock\.json$ ]] || [[ "$FILE_PATH" =~ yarn\.lock$ ]] || [[ "$FILE_PATH" =~ pnpm-lock\.yaml$ ]]; then deny "Blocked: lock files should change only via the package manager."fiexit 0
Make it executable:
chmod +x ~/.claude/hooks/protect-sensitive-files.sh
4. Wire hooks into Claude Code settings
Edit ~/.claude/settings.json and add:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "~/.claude/hooks/block-dangerous-commands.sh" } ] }, { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "~/.claude/hooks/protect-sensitive-files.sh" } ] } ] }}
What this does: Bash commands are screened by your command guard, file edits and writes are screened by your file guard.
5. Enable skip permissions
With hooks in place, you can enable skip permissions knowing that:
- Low risk commands run without interruptions.
- Hard blocks stop destructive actions.
- Ask rules raise a human prompt only for high impact operations.
Reference examples: deny and ask responses
These are the core response shapes you will reuse.
Deny (block the action)
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Blocked: force push can rewrite remote history." }}
Ask (show the normal prompt)
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": "Confirmation required: PR merge is a high impact change." }}
Good permissionDecisionReason patterns
Keep these as single sentence, action oriented messages that explain the risk without lecturing.
- Blocked: recursive force delete is not allowed.
- Blocked: hard reset discards uncommitted changes.
- Blocked: force push can rewrite remote history.
- Blocked: destructive SQL requires explicit human approval.
- Blocked: .env files may contain secrets.
- Blocked: git internals must not be edited directly.
- Confirmation required: deployment changes production state.
- Confirmation required: PR merge is a high impact change.
- Confirmation required: this action can modify billing resources.
- Confirmation required: this may delete data outside version control.
Conclusion
Hooks give you a third option between slow manual approvals and unsafe auto approvals. A small blocklist plus a small set of ask rules delivers speed, safety, and a cleaner operator experience. The key is not just blocking, it is providing short, precise reasons that steer the agent towards safer next actions.
Leave a comment