Layer 3 — Hooks (The Guardrail Layer)

The question: How do I make safety rules that always fire — regardless of what Claude decides, regardless of what I accidentally approve?

The critical distinction

This layer carries three words that matter: Deterministic. Not AI.

CLAUDE.md tells Claude "don't apply config without approval." Claude follows this most of the time. But "most of the time" is not acceptable when the consequence of violation is a network outage. Hooks are deterministic scripts that fire on specific events. They are not AI. They are not probabilistic. They are shell scripts with exit codes.

Hook events for networking

EventWhenNetworking use case
PreToolUseBefore any tool executesBlock config-mode commands, validate syntax
PostToolUseAfter a tool completesLint generated files, post notifications
SessionStartSession beginsPull latest configs, check VPN connectivity
PostCompactAfter context compactionPreserve critical device state
PreCompactBefore compactionSave troubleshooting notes

Block configuration commands — deterministically

Add to .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo $CLAUDE_TOOL_INPUT | jq -r '.command' | grep -qiE 'configure|conf t|edit exclusive|set system|commit|write mem|copy run|reload|reboot' && echo 'BLOCKED: Configuration command intercepted' && exit 2 || exit 0",
            "timeout": 5000
          }
        ]
      }
    ]
  }
}

Exit code 2 means "block this tool call." Any bash command containing configure, conf t, edit exclusive, set system, commit, write mem, copy run, reload, or reboot is intercepted and stopped. This covers Cisco IOS/IOS-XE, Arista EOS, Juniper Junos, and PAN-OS configuration entry points.

This hook does not care about Claude's reasoning. It does not care about your permission settings. It does not care if you typed "yes" at a permission prompt. The command is blocked. Period. This is the safety net below the safety net.

Auto-lint every generated YAML and Terraform file

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write(*.yml)",
        "hooks": [
          {
            "type": "command",
            "command": "ansible-lint $CLAUDE_FILE_PATH --profile production 2>&1 || true",
            "timeout": 15000
          }
        ]
      },
      {
        "matcher": "Write(*.tf)",
        "hooks": [
          {
            "type": "command",
            "command": "terraform fmt -check $CLAUDE_FILE_PATH && terraform validate 2>&1 || true",
            "timeout": 10000
          }
        ]
      }
    ]
  }
}

Every time Claude writes a YAML file, ansible-lint runs automatically. Every time it writes a Terraform file, terraform fmt and terraform validate run automatically. If the linter finds issues, the output feeds back into Claude's context and it fixes the violations without you asking. The quality enforcement loop is closed, permanently, without human intervention.

Pull latest configs on session start

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "cd ~/network-automation && git pull origin main --quiet 2>&1 && echo 'Config repo synced to HEAD'",
            "timeout": 30000
          }
        ]
      }
    ]
  }
}

Every session starts with the latest configs from your repo. No stale data. No "I was looking at last Tuesday's version" errors. This is the equivalent of the runbook instruction "always pull latest before starting work" — except it's enforced, not documented.

Post to Slack after any change is written

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write(configs/*)",
        "hooks": [
          {
            "type": "command",
            "command": "curl -sS -X POST $SLACK_WEBHOOK -H 'Content-Type: application/json' -d '{\"text\": \"Claude Code modified: '$CLAUDE_FILE_PATH' in session '$CLAUDE_SESSION_ID'\"}'",
            "timeout": 5000
          }
        ]
      }
    ]
  }
}

Any file written under configs/ triggers a Slack notification with the file path and session ID. Your team knows when generated configs exist. Nobody commits a Claude-generated config without at least one other person seeing the notification.

Knowledge check

Try it yourself