Tag: llm

  • 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.