Learn Claude Code: Safety Hooks as Security Gates

Hooks as Security Gates

Advanced 🕐 11 min Lesson 11 of 12
What you'll learn
  • Use PreToolUse hooks to enforce custom permission logic that rule syntax cannot express
  • Understand the interaction between blocking hooks and permission rules and predict which wins in each combination
  • Apply ConfigChange, Stop, and PermissionDenied hooks to build organizational audit and enforcement workflows

When Rules Are Not Enough

Permission rules are evaluated against the command string before a tool call runs. They are powerful for matching patterns — Bash(git push *) catches all git pushes — but they cannot inspect context. A rule cannot say "block edits to /auth/ unless the commit message references a JIRA ticket" or "require a second Claude session to review any changes to cryptographic code before they're committed." For those use cases, hooks are the mechanism.

Hooks are shell scripts that run in response to Claude Code events. PreToolUse hooks run before a tool call and can allow, prompt, or block it based on arbitrary logic — reading the tool input, calling external services, checking environment state, or anything else a script can do.

Blocking Hooks: The Strongest Veto

A PreToolUse hook that exits with code 2 blocks the tool call before permission rules are evaluated. This is the strongest veto in the permission system — it takes precedence over allow rules. An allow rule for Edit(/auth/**) does not let an edit proceed if a blocking hook exits 2 first.

Hook decisions interact with permission rules asymmetrically:

Hook exits 2 (block)

Blocks before rules. Allow rules cannot override. The strongest possible veto.

Hook returns \"allow\"

Permission rules still evaluate. Deny and ask rules still apply even after a hook approved the call.

Hook returns \"ask\"

Forces a prompt even when an allow rule would have auto-approved the call.

This asymmetry is intentional: hooks can make approval stricter (by blocking or forcing prompts even when rules say allow), but they cannot make approval more permissive (a hook cannot override a deny rule). The deny-first guarantee is preserved regardless of what hooks return.

PreToolUse for Custom Logic

A PreToolUse hook receives the tool name and input as JSON on stdin. It can read any field, call external services, check environment variables, or run any script. A hook that blocks edits to authentication code unless a JIRA ticket ID is present:

Exit 2 with message "Edits to /auth/ require a JIRA ticket ID in the task description" when the file_path starts with /auth/ and the current session context does not contain a JIRA ID pattern.

The exact implementation is in shell, Python, or any scripting language. Claude Code runs the hook and reads its stdout for messages to show in the transcript.

ConfigChange: Monitoring Settings Mutations

The ConfigChange hook fires when Claude attempts to modify its own configuration during a session — changes to .claude/settings.json, .claude/settings.local.json, or CLAUDE.md. Use it to:

  • Log every settings change to an audit trail
  • Block changes to specific settings keys that should only come from managed settings
  • Alert a security channel when Claude modifies its own permission rules

A blocking ConfigChange hook (exit 2) prevents Claude from changing its configuration at all during a session, making the settings read-only for that run. This is useful in CI pipelines where configuration drift would be unexpected and should be treated as an error.

Stop Hook: A Hard Gate on Turn Completion

The Stop hook fires when a turn is about to complete. If it exits non-zero, the turn does not complete — Claude is re-invoked to fix whatever the hook detected. Use it as a scripted gate that runs after every turn:

  • Run your test suite and block completion if tests fail
  • Check that no TODO markers were left in modified files
  • Verify that changed authentication code matches the expected diff pattern
  • Confirm that a required sign-off exists before a commit is created

Stop hooks are the programmatic equivalent of /goal — but they are scripted rather than described in natural language, making them deterministic and suitable for CI-style gates. The hook output is provided to Claude as context for why the turn did not complete, so Claude can address the failure.

InstructionsLoaded and PermissionDenied

Two more hooks complete the security picture:

  • InstructionsLoaded: Fires after CLAUDE.md files are loaded, with a list of which files were loaded and when. Use it to verify that required policy files were loaded before allowing the session to proceed. A blocking InstructionsLoaded hook that exits 2 when the organization's managed CLAUDE.md is absent prevents sessions from running without the policy in place.
  • PermissionDenied: Fires when the auto-mode classifier or a permission rule denies an action. Use it to log denials to a security audit trail, alert on specific denial categories, or trigger a review workflow when sensitive operations are attempted and blocked.

OpenTelemetry for Usage Monitoring

For broader usage visibility beyond individual session hooks, Claude Code exports OpenTelemetry metrics. Configure an OTLP endpoint with the --otel-endpoint flag or the OTEL_EXPORTER_OTLP_ENDPOINT environment variable, and Claude Code reports session metrics including tool call counts, permission decisions, and token usage. Feed this into your existing observability infrastructure to track Claude Code usage across your organization, detect anomalous session patterns, and maintain an audit record for compliance purposes.

Organizational Hook Policy

Hooks defined in project settings or user settings can add gate logic, but they can also circumvent organizational security gates if a project hook runs before or instead of a managed hook. Use allowManagedHooksOnly: true in managed settings to ensure only hooks from managed settings and force-enabled plugins load. User hooks, project hooks, and plugin hooks from non-managed plugins are all blocked, so the organizational gates are the only gates.

Key takeaways
  • A PreToolUse hook that exits code 2 blocks the tool call before permission rules evaluate — it is the strongest veto in the permission system and takes precedence over allow rules.
  • Hook approvals cannot override deny rules — a hook returning "allow" still lets deny and ask rules apply, preserving the deny-first guarantee regardless of hook output.
  • ConfigChange hooks fire on settings mutations and can block or log Claude changing its own configuration during a session — useful for CI pipelines where configuration drift should be treated as an error.
  • Stop hooks provide a scripted gate on turn completion: if the hook exits non-zero, the turn does not complete and Claude is re-invoked with the hook output as context for what needs to be fixed.
  • allowManagedHooksOnly:true in managed settings blocks user hooks, project hooks, and non-managed plugin hooks from loading — ensuring organizational security gates cannot be circumvented by project-level hook definitions.