22/03/2026

Claude Code Hooks: The Deterministic Security Layer Your AI Agent Needs
> APPSEC_ENGINEERING // CLAUDE_CODE // FIELD_REPORT

Claude Code Hooks: The Deterministic Security Layer Your AI Agent Needs

CLAUDE.md rules are suggestions. Hooks are enforced gates. exit 2 = blocked. No negotiation. If you're letting an AI agent write code without guardrails, here's how you fix that.

// March 2026 • 12 min read • security-first perspective

Why This Matters (Or: How Your AI Agent Became an Insider Threat)

Since the corporate suits decided to go all in with AI (and fire half of the IT population), the market has changed dramatically, let's cut through the noise. The suits in the boardroom are excited about AI agents. "Autonomous productivity!" they say. "Digital workforce!" they cheer. Meanwhile, those of us who actually hack things for a living are watching these agents get deployed with shell access, API keys, and service-level credentials — and zero security controls beyond a politely worded system prompt.

The numbers are brutal. According to a 2026 survey of 1,253 security professionals, 91% of organizations only discover what an AI agent did after it already executed the action. Only 9% can intervene before an agent completes a harmful action. The other 91%? 35% find it in logs after the fact. 32% have no visibility at all. Let that sink in: for every ten organizations running agentic AI, fewer than one can stop an agent from deleting a repository, modifying a customer record, or escalating a privilege before it happens.

And this isn't theoretical. 37% of organizations experienced AI agent-caused operational issues in the past twelve months. 8% were significant enough to cause outages or data corruption. Agents are already autonomously moving data to untrusted locations, deleting configs, and making decisions that no human reviewed.

NVIDIA's AI red team put it bluntly: LLM-generated code must be treated as untrusted output. Sanitization alone is not enough — attackers can craft prompts that evade filters, manipulate trusted library functions, and exploit model behaviors in ways that bypass traditional controls. An agent that generates and runs code on the fly creates a pathway where a crafted prompt escalates into remote code execution. That's not a bug. That's the architecture working as designed.

Krebs on Security ran a piece this month on autonomous AI assistants that proactively take actions without being prompted. The comments section was full of hackers (the good kind) asking the same question: "Who's watching the watchers?" Because your SIEM and EDR tools were built to detect anomalies in human behavior. An agent that runs code perfectly 10,000 times in sequence looks normal to these systems. But that agent might be executing an attacker's will.

OWASP saw this coming. They released a dedicated Top 10 for Agentic AI Applications — the #1 risk is Agent Goal Hijacking, where an attacker manipulates an agent's objectives through poisoned inputs. The agent can't tell the difference between legitimate instructions and malicious data. A single poisoned email, document, or web page can redirect your agent to exfiltrate data using its own legitimate access.

So here's the thing. You can write all the CLAUDE.md rules you want. You can put "never delete production data" in your system prompt. But those are requests, not guarantees. The model might ignore them. Prompt injection can override them. They're advisory — and advisory doesn't cut it when the agent has kubectl access to your prod cluster.

Hooks are the answer. They're the deterministic layer that sits between intent and execution. They don't ask the model nicely. They enforce. exit 2 = blocked, period. The model cannot bypass a hook. It's not running in the model's context — it's a plain shell script triggered by the system, outside the LLM entirely.

If you're an AppSec hacker who's been watching this AI agent gold rush with growing anxiety — this post is your field manual. We're going to cover what hooks are, how to wire them up, and the 5 production hooks that should be non-negotiable on every Claude Code deployment. The suits can keep their "digital workforce." We're going to make sure it can't burn the house down.

TL;DR

Claude Code hooks are user-defined scripts that fire at specific lifecycle events — before a tool runs, after it completes, when a session starts, or when Claude stops responding. They run outside the LLM as plain scripts, not prompts. exit 0 = allow. exit 2 = block. As of March 2026: 21 lifecycle events, 4 handler types (command, HTTP, prompt, agent), async execution, and JSON structured output. This post covers what they are, how to configure them, and 5 production hooks you should deploy today.

What Are Claude Code Hooks?

Hooks are shell commands, HTTP endpoints, or LLM prompts that execute automatically at specific points in Claude Code's lifecycle. They run outside the LLM — plain scripts triggered by Claude's actions, not prompts interpreted by the model. Think of them as tripwires you set around your agent's execution path.

This distinction is what makes them powerful. Function calling extends what an AI can do. Hooks constrain what an AI does. The AI doesn't request a hook — the hook intercepts the AI. The model has zero say in whether the hook fires. It's not a polite suggestion in a system prompt that the model can "forget" when it's 50 messages deep. It's a shell script with exit 2. Deterministic. Unavoidable.

Claude Code execution
Event fires
Matcher evaluates
Hook executes

Your hook receives JSON context via stdin — session ID, working directory, tool name, tool input. It inspects, decides, and optionally returns a decision. exit 0 = allow. exit 2 = block. exit 1 = non-blocking warning (action still proceeds).

// HACKERS: READ THIS FIRST

Exit code 1 is NOT a security control. It only logs a warning — the action still goes through. Every security hook must use exit 2, or you've built a monitoring tool, not a gate. This is the rookie mistake I see everywhere. If your hook exits 1, the agent smiled at your warning and kept going.


The 21 Lifecycle Events

Here are the critical events. The ones you'll use 90% of the time are PreToolUse, PostToolUse, and Stop.

EventWhen It FiresBlocks?Use Case
SessionStartSession begins, resumes, clears, or compactsNOEnvironment setup, context injection
PreToolUseBefore any tool executionYES — deny/allow/escalateSecurity gates, input validation, command blocking
PostToolUseAfter tool completes successfullyYES — blockAuto-formatting, test runners, security scans
PostToolUseFailureAfter a tool failsYES — blockError handling, retry logic
PermissionRequestPermission dialog about to showYES — allow/denyAuto-approve safe ops, deny risky ones
UserPromptSubmitUser submits a promptYES — blockPrompt validation, injection detection
StopClaude finishes respondingYES — blockOutput validation, prevent premature stops
SubagentStopSubagent completesYES — blockSubagent task verification
SubagentStartSubagent startsNODB connection setup, agent-specific env
NotificationClaude sends a notificationNODesktop/Slack alerts, logging
PreCompactBefore compactionNOTranscript backup, context preservation
ConfigChangeConfig file changes during sessionYES — blockAudit logging, block unauthorized changes
SetupVia --init or --maintenanceNORepository setup and maintenance
// SUBAGENT RECURSION

Hooks fire for subagent actions too. If Claude spawns a subagent, your PreToolUse and PostToolUse hooks execute for every tool the subagent uses. Without recursive hook enforcement, a subagent could bypass your safety gates.


Configuration: Where Hooks Live

FileScopeCommit?
~/.claude/settings.jsonUser-wide (all projects)NO
.claude/settings.jsonProject-level (whole team)YES — COMMIT THIS
.claude/settings.local.jsonLocal overridesNO (gitignored)
// BEST PRACTICE

Put non-negotiable security gates in .claude/settings.json (project-level, committed to repo). Every team member gets the same guardrails automatically. Personal preferences go in .claude/settings.local.json.


The 4 Handler Types

1. Command Hooks — type: "command"

Shell scripts that receive JSON via stdin. The workhorse for most use cases.

{ "type": "command", "command": ".claude/hooks/block-rm.sh" }

2. HTTP Hooks — type: "http"

POST requests to an endpoint. Slack notifications, audit logging, webhook CI/CD triggers.

{ "type": "http", "url": "https://your-webhook.example.com/hook" }

3. Prompt Hooks — type: "prompt"

Send a prompt to a Claude model for single-turn semantic evaluation. Perfect for decisions regex can't handle — "does this edit touch authentication logic?"

{ "type": "prompt", "prompt": "Does this change modify auth logic? Input: $ARGUMENTS" }

4. Agent Hooks — type: "agent"

Spawn subagents with access to Read, Grep, Glob for deep codebase verification. The most powerful handler for complex multi-file security checks.


5 Production Hooks You Should Deploy Today

HOOK 01

Block Destructive Shell Commands

Event: PreToolUse | Matcher: Bash

Prevent rm -rf, DROP TABLE, chmod 777, and other commands that would make any hacker wince. Your AI agent doesn't need to nuke filesystems or wipe databases. If it tries, something has gone very wrong and you want that action dead before it executes.

// .claude/hooks/block-dangerous.sh

#!/bin/bash
# Read JSON from stdin
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Define dangerous patterns
DANGEROUS_PATTERNS=(
  "rm -rf"
  "rm -fr"
  "chmod 777"
  "DROP TABLE"
  "DROP DATABASE"
  "mkfs"
  "> /dev/sda"
  ":(){ :|:& };:"
)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qi "$pattern"; then
    echo "BLOCKED: Destructive command: $pattern" >&2
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Blocked by security hook"
      }
    }'
    exit 2
  fi
done

exit 0

// settings.json config

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}
HOOK 02

Auto-Format on Every File Write

Event: PostToolUse | Matcher: Write|Edit|MultiEdit

Every time Claude writes or edits a file, Prettier runs automatically. No prompt needed. No permission dialog. No exceptions.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\""
          }
        ]
      }
    ]
  }
}
HOOK 03

Block Access to Sensitive Files

Event: PreToolUse | Matcher: Read|Edit|Write|MultiEdit|Bash

Prevent Claude from reading or modifying .env, private keys, credentials, kubeconfig, and other sensitive files. This is Least Privilege 101 — the same principle every pentester exploits when they find an overprivileged service account. Don't let your AI agent become the next one.

// .claude/hooks/block-sensitive.sh

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')

# Sensitive file patterns
SENSITIVE_PATTERNS=(
  "\.env$"      "\.env\."
  "secrets\."   "credentials"
  "\.pem$"      "\.key$"
  "id_rsa"      "id_ed25519"
  "\.pfx$"      "kubeconfig"
  "\.aws/credentials"
  "\.ssh/"      "vault\.json"
  "token\.json"
)

for pattern in "${SENSITIVE_PATTERNS[@]}"; do
  if echo "$FILE_PATH" | grep -qiE "$pattern"; then
    echo "BLOCKED: Sensitive file: $FILE_PATH" >&2
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Sensitive file access blocked"
      }
    }'
    exit 2
  fi
done

exit 0
HOOK 04

Run Tests After Code Changes

Event: PostToolUse | Matcher: Write|Edit|MultiEdit

Automatically run your test suite on modified files. Catch regressions immediately instead of waiting for CI.

// .claude/hooks/run-tests.sh

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Only run tests for source files
if echo "$FILE_PATH" | grep -qE '\.(js|ts|py|jsx|tsx)$'; then
  # Skip test files to avoid loops
  if echo "$FILE_PATH" | grep -qE '(test|spec|__test__)'; then
    exit 0
  fi

  # Detect framework and run
  if [ -f "package.json" ]; then
    npm test --silent 2>&1 | tail -5
  elif [ -f "pytest.ini" ] || [ -f "pyproject.toml" ]; then
    python -m pytest --tb=short -q 2>&1 | tail -10
  fi
fi

exit 0
HOOK 05

Slack / Desktop Notification on Completion

Event: Stop | Matcher: (any)

When Claude finishes a long-running task, get notified immediately. Never forget about a background session again.

// .claude/hooks/notify-complete.sh

#!/bin/bash
INPUT=$(cat)
STOP_REASON=$(echo "$INPUT" | jq -r '.stop_reason // "completed"')

# macOS notification
osascript -e "display notification \"Claude: $STOP_REASON\" with title \"Claude Code\""

# Optional: Slack webhook
SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"
if [ -n "$SLACK_WEBHOOK" ]; then
  curl -s -X POST "$SLACK_WEBHOOK" \
    -H 'Content-Type: application/json' \
    -d "{\"text\": \"Claude Code finished: $STOP_REASON\"}" \
    > /dev/null 2>&1
fi

exit 0

Advanced: PreToolUse Input Modification

Starting in v2.0.10, PreToolUse hooks can modify tool inputs before execution — without blocking the action. You intercept, modify, and let execution proceed with corrected parameters. The modification is invisible to Claude.

Use cases: automatic dry-run flags on destructive commands, secret redaction, path correction to safe directories, commit message formatting enforcement.

// Example — Force dry-run on kubectl delete:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -q "kubectl delete" && \
   ! echo "$COMMAND" | grep -q "--dry-run"; then
  MODIFIED=$(echo "$COMMAND" | sed 's/kubectl delete/kubectl delete --dry-run=client/')
  jq -n --arg cmd "$MODIFIED" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "allow",
      updatedInput: { command: $cmd }
    }
  }'
  exit 0
fi

exit 0

Advanced: Prompt Hooks for Semantic Security

Shell scripts handle pattern matching. But what about context-dependent decisions like "does this edit touch authentication logic?" or "does this query access PII columns?"

Prompt hooks delegate the decision to a lightweight Claude model:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "You are a security reviewer. Does this change modify auth, authz, or session management? If yes: {\"hookSpecificOutput\": {\"hookEventName\": \"PreToolUse\", \"permissionDecision\": \"escalate\", \"permissionDecisionReason\": \"Auth logic — human review required\"}}. If no: {}. Change: $ARGUMENTS"
          }
        ]
      }
    ]
  }
}

The escalate decision surfaces the action to the user for manual approval — perfect for high-risk changes that need a human in the loop.


Security Considerations

// 01: HOOKS RUN WITH YOUR USER PERMISSIONS

There is no sandbox. Your hooks execute with the same privileges as your shell. A malicious hook has full access to your filesystem, network, and credentials. Treat hook scripts like production code. Review them. Version control them. Don't curl | bash random hook repos from some stranger's GitHub. You wouldn't run an unvetted binary — don't run unvetted hooks either.

// 02: EXIT 2 VS EXIT 1 — THIS MATTERS

exit 2 = action is BLOCKED. Claude sees the rejection and suggests alternatives.
exit 1 = non-blocking warning. Action still proceeds.
Every security hook must use exit 2. Exit 1 = you're logging, not enforcing.

// 03: SUBAGENT RECURSION LOOPS

A UserPromptSubmit hook that spawns subagents can create infinite loops if those subagents trigger the same hook. Check for a subagent indicator in hook input before spawning. Scope hooks to top-level agent sessions only.

// 04: PERFORMANCE IS THE REAL CONSTRAINT

Each hook runs synchronously, adding execution time to every matched tool call. Threshold: if a PostToolUse hook adds >500ms to every file edit, the session becomes sluggish. Profile with time. Keep each under 200ms.

// 05: CLAUDE.MD = ADVISORY. HOOKS = ENFORCED.

"Never modify .env files" in CLAUDE.md = a polite request. The model might ignore it. A prompt injection will definitely override it.
A PreToolUse hook blocking .env access with exit 2 = a locked door. The model doesn't have the key.
Stop writing rules. Start writing hooks.


Getting Started Checklist

  • Start with two hooks: Destructive command blocker (Hook 01) and sensitive file gate (Hook 03). These prevent the most common AI agent mistakes with zero maintenance.
  • Commit to .claude/settings.json in your repo so the whole team shares the same guardrails automatically.
  • Use claude --debug when hooks don't fire as expected — shows exactly what's matching and executing.
  • Keep hooks fast — under 200ms each. Profile with time. Ten fast hooks outperform two slow ones.
  • Use $CLAUDE_PROJECT_DIR prefix for hook paths in settings.json for reliable path resolution.
  • Toggle verbose mode with Ctrl+O to see stdout/stderr from hooks in real-time during a session.

// References

  • Anthropic Official Docs — docs.anthropic.com/en/docs/claude-code/hooks
  • Claude Code Hooks Reference — code.claude.com/docs/en/hooks
  • GitHub: claude-code-hooks-mastery — github.com/disler/claude-code-hooks-mastery
  • 5 Production Hooks Tutorial — blakecrosley.com/blog/claude-code-hooks-tutorial
  • SmartScope Complete Guide — smartscope.blog/en/generative-ai/claude/claude-code-hooks-guide
  • PromptLayer Docs — blog.promptlayer.com/understanding-claude-code-hooks-documentation