PTENES
4.2 ~50 min · Intermediate

🪝 Hooks: automate the harness

Configure event hooks to enforce policies, automate linting, and inject context—without touching the agent’s code.

SessionStart injects context PreToolUse validates/blocks ⚙️ Tool Bash · Read · Write run here PostToolUse lint · audit Stop cleanup hook.sh lint.sh Harness life cycle — hooks intercept at 4 points

🪝 What hooks are

Hooks are external scripts executed by the harness at specific points in the agent lifecycle. They receive event metadata via stdin and can influence the harness's behavior through the exit code and output.

The big advantage: you add behavior without modifying the prompt or agent. Policies live in versioned scripts in the repository and are automatically applied to the whole team.

🔔 SessionStart

Runs when a new session starts. Stdout is injected as the initial message in the context. Ideal for loading project state, the current branch, and pending tasks.

🔍 PreToolUse

Runs before each tool. Exit 0 = allows it. Non-zero exit = blocks the tool. Stderr with an error message is returned to the agent.

📋 PostToolUse

Runs after each tool returns. Stdout is injected into the context. Ideal for automatic linting, auditing, and usage metrics.

🛑 Stop

Runs when the agent exits. Ideal for cleanup, completion notifications, session reports, and memory persistence.

⚙️ Configure in settings.json

Hooks are configured in the key hooks of the settings.json. Each entry defines the event, an optional tool matcher, and the shell command to run.

// .claude/settings.json — example with 4 hooks
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/session-start.sh"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/lint.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/session-end.sh"
          }
        ]
      }
    ]
  }
}

💡 Global vs. local

~/.claude/settings.json

Applies to all projects. Good for personal policies: blocking destructive commands, lint preferences.

.claude/settings.json

Project-specific. Committed to the repository—the whole team automatically inherits the policies.

🛠️ Practical Use Cases

🧹

Automatic linting after every save

PostToolUse on Write/Edit runs eslint/prettier. The output is injected into the context — the agent sees the errors and fixes them automatically.

#!/bin/bash # .claude/hooks/lint.sh
FILE=$(cat /dev/stdin | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" == *.ts || "$FILE" == *.tsx ]]; then
  npx eslint --fix "$FILE" 2>&1
fi
🔒

Block dangerous commands

PreToolUse on Bash inspects the command. If it detects prohibited patterns, it returns exit 1 with a message explaining the block.

#!/bin/bash # .claude/hooks/block-dangerous.sh
CMD=$(cat /dev/stdin | jq -r '.tool_input.command // empty')
DANGEROUS="rm -rf|git push --force|DROP TABLE|truncate"
if echo "$CMD" | grep -qE "$DANGEROUS"; then
  echo "❌ Comando bloqueado por política de segurança" >&2
  exit 1
fi
💉

Inject context in SessionStart

At the start of each session, it automatically injects the current state: branch, recent commits, open issues. The agent starts with context already in place.

#!/bin/bash # .claude/hooks/session-start.sh
echo "=== CONTEXTO DO PROJETO ==="
echo "Branch: $(git branch --show-current)"
echo "Último commit: $(git log -1 --oneline)"
echo "Arquivos modificados:"
git status --short | head -10

⚖️ What to do and what to avoid

✓ Best practices

  • ✓ Quick scripts — hooks with short timeouts don’t block the agent
  • ✓ Clear messages in stderr when blocking
  • ✓ Use a matcher to apply only where it makes sense
  • ✓ Version scripts in .claude/hooks/

✗ Common pitfalls

  • ✗ Hooks that take > 5s — they stall agent execution
  • ✗ PreToolUse on all tools without a specific matcher
  • ✗ Injecting very large output in SessionStart (context pressure)
  • ✗ Rely only on fail-open for critical security hooks

🛡️ Security and Fail-Open

Fail-open is the default behavior: if a hook fails (timeout, script error, crash), the harness continues execution normally. The tool the hook should intercept isn't blocked.

This is intentional for enrichment hooks (lint, context)—you don't want a broken lint to interrupt all work. But for security hooks, fail-open is dangerous.

🔑 Rule of thumb

Audit hooks: fail-open is fine. If logging fails, the work continues.
Security hooks: make the script extremely simple and fast — the risk of failure drops to zero.
// Best practices for robust hooks
#!/bin/bash
# Sempre: timeout explícito para não travar o agente
# Leitura do stdin com timeout
INPUT=$(timeout 2 cat /dev/stdin 2>/dev/null || echo '{}')

# Parse seguro com fallback
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)

# Se nada para verificar, sair com sucesso (fail-open explícito)
[ -z "$CMD" ] && exit 0

# Verificação principal
if echo "$CMD" | grep -qE "rm -rf /|DROP DATABASE"; then
  echo "Comando bloqueado: risco de perda de dados irreversível" >&2
  exit 1
fi

exit 0

📊 Data available in each hook

Event Data on stdin Output impact
SessionStart Session metadata Stdout injected as the initial message
PreToolUse Tool name + tool_input (JSON) Exit 0=allows, 1=blocks; stderr→agent
PostToolUse tool_input + tool_response (JSON) Stdout injected into the current context
Stop Reason for termination Side effects only (log, notification)

⚡ Most-used hooks

  1. 1. PostToolUse Write → automatic linting
  2. 2. PreToolUse Bash → block commands
  3. 3. SessionStart → context injection
  4. 4. Stop → completion notification

🔧 Skill /update-config

The skill /update-config of Claude Code makes it easy to add hooks to settings.json using a natural language prompt: "add a lint hook for TypeScript".

🎯 Module 4.2 Summary

✓
4 events — SessionStart, PreToolUse, PostToolUse, Stop
✓
settings.json — declarative, versionable configuration inherited by the team
✓
Automatic linting — PostToolUse on Write/Edit, output injected into context
✓
Blocking — PreToolUse with exit 1 + message on stderr
✓
Fail-open — a hook that fails does not block execution by default

Next Module:

4.3 — ⌨️ Slash commands and deterministic workflows