Hooks — Runtime enforcement

The only mechanism the agent cannot skip. PreToolUse blocks before execution. PostToolUse triggers after.

How hooks work

Hooks are declared in the YAML frontmatter of a SKILL.md file. Each hook specifies a type, a matcher, and a command to run.

hooks:
  PreToolUse:
    - matcher: Bash
      command: "./guard/bin/check-dangerous.sh"
  PostToolUse:
    - matcher: Write
      command: "./guard/bin/suggest-security.sh"
  • typePreToolUse runs before the tool executes. PostToolUse runs after.
  • matcher— which tool triggers the hook. Options: Bash, Write,Edit, Read, or * for all tools.
  • command— a shell script to execute. Receives the tool arguments as environment variables.

Exit codes

  • exit 0 — allow the tool to proceed
  • exit 1 — block the tool (PreToolUse only)
  • exit 2 — allow but flag for review

Why this matters

Prompt instructions are suggestions. The agent can ignore them, forget them, or reinterpret them under pressure. Hooks are different. They are enforced by the Claude Code runtime itself, outside the agent's control. The agent submits a tool call, the runtime intercepts it, runs the hook script, and only proceeds if the script allows it.

The agent never sees a blocked command execute. It receives the hook's stderr output as an explanation and must find an alternative approach.

Example: blocking dangerous commands

The /guard skill uses a PreToolUse hook to prevent destructive operations. Here is the full script:

#!/bin/bash
# bin/check-dangerous.sh
# PreToolUse hook: block dangerous bash commands

COMMAND="$TOOL_INPUT"

# Patterns that should never run without explicit approval
DANGEROUS_PATTERNS=(
  "rm -rf /"
  "git push --force"
  "git reset --hard"
  "DROP TABLE"
  "DROP DATABASE"
  "chmod -R 777"
  "dd if="
  "> /dev/sd"
  "mkfs\."
  ":(){:|:&};:"
)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qiE "$pattern"; then
    echo "BLOCKED: command matches dangerous pattern: $pattern" >&2
    echo "If you need to run this, ask the user for explicit approval first." >&2
    exit 1
  fi
done

exit 0

When the agent tries to run git push --force, the runtime calls this script. The script prints the reason to stderr and exits 1. The agent sees:

BLOCKED: command matches dangerous pattern: git push --force
If you need to run this, ask the user for explicit approval first.

Example: suggesting security review

A PostToolUse hook on Write operations. It checks whether the written file touches authentication or authorization logic and flags it for security review.

#!/bin/bash
# bin/suggest-security.sh
# PostToolUse hook: flag security-sensitive file changes

FILE_PATH="$TOOL_INPUT_FILE_PATH"

SECURITY_PATTERNS=(
  "auth"
  "login"
  "session"
  "token"
  "password"
  "permission"
  "rbac"
  "acl"
  "middleware/auth"
  "api/key"
)

for pattern in "${SECURITY_PATTERNS[@]}"; do
  if echo "$FILE_PATH" | grep -qi "$pattern"; then
    echo "SECURITY_SENSITIVE: $FILE_PATH matches pattern '$pattern'" >&2
    echo "Consider running /security before shipping this change." >&2
    exit 2
  fi
done

exit 0

Exit code 2 means: allow the write, but flag it. The agent sees the warning and can choose to run /security before proceeding.

What hooks cannot do

Hooks have real limitations. Understanding them prevents frustration:

  • Hooks cannot run between skills. They fire on tool use within a single agent session. There is no inter-skill hook mechanism.
  • Hooks cannot enforce artifact saving. If the agent forgets to call save-artifact.sh, no hook will catch that. Artifact saving is an instruction, not a hooked action.
  • Hooks cannot modify the agent's response. They can block actions and print warnings, but they cannot rewrite what the agent says to the user.
  • Hooks are Claude Code specific. Other agents (Cursor, Codex) do not have the same runtime interception. For those agents, hooks are advisory — documented in the SKILL.md but not enforced.

Creating your own hook

Minimal example: a hook that prevents writing files larger than 500 lines.

# In your SKILL.md frontmatter:
hooks:
  PreToolUse:
    - matcher: Write
      command: "./guard/bin/check-file-size.sh"
#!/bin/bash
# bin/check-file-size.sh

FILE_PATH="$TOOL_INPUT_FILE_PATH"
CONTENT="$TOOL_INPUT_CONTENT"

LINE_COUNT=$(echo "$CONTENT" | wc -l)

if [ "$LINE_COUNT" -gt 500 ]; then
  echo "BLOCKED: file would be $LINE_COUNT lines (limit: 500)" >&2
  echo "Split into smaller modules." >&2
  exit 1
fi

exit 0

Save the script, make it executable with chmod +x bin/check-file-size.sh, and the hook is active for any skill that declares it in its frontmatter.

PreviousArtifact pipelineNextCreate a skill