Stop Babysitting Your AI Agent: Safety Hooks for Claude Code

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 variants
if 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 --force
if echo "$COMMAND" | grep -qE 'git\s+push\s+.*(-f|--force)\b'; then
deny "Blocked: force push can rewrite remote history."
fi
# git reset --hard
if 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 PRs
if 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: allow
exit 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 0
fi
deny () {
local reason="$1"
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$reason"
exit 0
}
# Secrets and credentials
if [[ "$FILE_PATH" =~ \.env$ ]] || [[ "$FILE_PATH" =~ \.env\. ]]; then
deny "Blocked: .env files may contain secrets."
fi
if [[ "$FILE_PATH" =~ \.aws/credentials$ ]]; then
deny "Blocked: AWS credentials must not be modified by the agent."
fi
# Git internals
if [[ "$FILE_PATH" =~ /\.git/ ]]; then
deny "Blocked: git internals must not be edited directly."
fi
# Lock files
if [[ "$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."
fi
exit 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.

Comments

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.