s/agents-development

Hooks and Policy-as-Code

Последнее обновление @legostin · 2026-04-11T17:45:58+00:00

Hooks and Policy-as-Code

CLAUDE.md tells the agent what you want. Permission rules declare which tools it may use. Neither of those is enough on its own for production. Sooner or later you need deterministic, programmatic checks that run regardless of what the model decides — "if it tries to edit a file matching infra/prod/**, block it. Every time. No exceptions."

That's what hooks are for. A hook is a shell command, HTTP endpoint, or LLM prompt that Claude Code runs at a specific point in its lifecycle. Hooks get JSON on stdin, control the flow through exit codes, and can block actions the model was about to take.

This page covers the hook model, all the event types that matter in production, and a few concrete patterns.

Hook events

Event When it fires Can block?
SessionStart Session begins or resumes No
UserPromptSubmit User submits a prompt, before processing Yes
PreToolUse Before a tool call executes Yes
PostToolUse After a tool call succeeds No
PostToolUseFailure After a tool call fails No
PermissionRequest A permission dialog is about to show Yes
PermissionDenied Auto mode denied a tool call No
Stop Claude finishes responding Yes
SubagentStart / SubagentStop Subagent lifecycle Yes (Stop)
PreCompact / PostCompact Around context compaction No
SessionEnd Session terminates No

The ones you will actually use: PreToolUse (block dangerous actions), PostToolUse (lint, format, test after edits), SessionStart (load environment), and UserPromptSubmit (sanitize / log prompts).

Where hooks live

Hooks are configured in settings.json, which has a clear precedence chain:

~/.claude/settings.json          (user, all projects)
.claude/settings.json            (project, checked in)
.claude/settings.local.json      (project, gitignored)
managed-settings.json            (org-wide, cannot be overridden)

The structure is three levels: event → matcher group → handlers.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-bash.sh"
          }
        ]
      }
    ]
  }
}

The matcher field can be the tool name ("Bash"), a pipe-separated list ("Edit|Write"), or a JS regex ("mcp__memory__.*").

Handler types

Command hook — runs a shell command. Receives JSON on stdin with session info, cwd, tool name, and tool input.

{
  "type": "command",
  "command": "./.claude/hooks/check.sh",
  "timeout": 30
}

HTTP hook — POSTs JSON to a URL, reads the response. Useful for centralized policy servers.

{
  "type": "http",
  "url": "http://policy.internal/claude",
  "headers": { "Authorization": "Bearer $POLICY_TOKEN" },
  "allowedEnvVars": ["POLICY_TOKEN"]
}

Prompt hook — runs a quick LLM check (cheap model). Good for fuzzy rules like "does this command match our ops policy?".

Agent hook — runs a full agent review. Heavy, but sometimes worth it.

Exit code semantics

Claude Code's exit code conventions are unusual. Read this once and pin it.

Exit code Meaning
0 Success. Claude parses stdout as JSON for structured control.
2 Blocking error. Stops the action. stderr is shown to Claude.
anything else Non-blocking error. Shown in transcript. Execution continues.

Exit 1 is treated as a non-blocking error. Use exit 2 only for policy enforcement.

Pattern 1: block dangerous bash

#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)

if echo "$COMMAND" | grep -q 'rm -rf'; then
  echo "Blocked: rm -rf requires manual execution" >&2
  exit 2
fi

exit 0

Registered as:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-rm.sh" }
        ]
      }
    ]
  }
}

Pattern 2: PatchGate — lint and test after every edit

#!/bin/bash
# .claude/hooks/patchgate.sh — runs after Edit/Write
cd "$CLAUDE_PROJECT_DIR" || exit 0

if ! npm run lint --silent; then
  echo "Lint failed — please fix before continuing" >&2
  exit 2
fi

if ! npm test --silent -- --changed; then
  echo "Tests failed on changed files" >&2
  exit 2
fi

exit 0

Registered on PostToolUse for Edit|Write. This turns "green CI" into a per-edit invariant — the agent cannot advance past a broken state.

Pattern 3: redact secrets from prompts

#!/bin/bash
# .claude/hooks/redact-secrets.sh — fires on UserPromptSubmit
PROMPT=$(jq -r '.user_prompt' < /dev/stdin)

if echo "$PROMPT" | grep -qE 'AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{40,}'; then
  echo "Prompt contains what looks like a secret. Scrub and resubmit." >&2
  exit 2
fi

exit 0

JSON output for fine-grained control

Instead of plain exit codes, a hook can exit 0 and print structured JSON to stdout. This lets you allow, deny, modify input, or inject context:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Writes to infra/prod/ require a change ticket"
  }
}

Or to modify the tool call before it runs:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "updatedInput": {
      "command": "npm test -- --runInBand"
    }
  }
}

Interaction with permission rules

Hooks run before permission rules are consulted. A hook that exits 2 blocks the action regardless of allow rules. A hook that exits 0 does not bypass deny rules — they still fire afterward.

This lets you write open policies ("allow Bash") with targeted hooks that block specific patterns, rather than maintaining an ever-growing deny list.

Viewing and disabling

  • /hooks inside Claude Code shows a read-only browser of all configured hooks and their sources.
  • "disableAllHooks": true in settings disables everything temporarily — useful for debugging, never in CI.

Security warnings

  • Hook scripts run with your user's permissions. Treat them like any cron job — they can do damage if written wrong.
  • JSON on stdin can be corrupted by .bashrc or .zshrc output. Make sure hook output is only the JSON object.
  • Don't commit hooks that reference secrets. Use env vars and document them.

Next: Multi-Agent Patterns

Sources

История

Вся история
Ревизия {n}
@legostin · Смержил @legostin
Открыть diff
© 2026 HeyUpСделано на Laravel, Vue и Tailwind.