Skip to content

Hooks System

mark7766 edited this page Jul 14, 2026 · 2 revisions

Hooks System

How Claude Code hooks enforce the PDCA loop at the mechanical level — the fourth and fifth layers of the defense system.


Why hooks?

Text instructions in AGENTS.md say "must read memory before coding." But in long sessions, under heavy tool use, AI models can skip text instructions. Hooks are mechanical constraints — they run as shell commands at specific lifecycle events, and can block the session with non-zero exit codes.


The four hooks

graph TD
    START[Session Start] --> SSH[SessionStart Hook]
    SSH --> PROMPT[User enters prompt]
    PROMPT --> UPSH[UserPromptSubmit Hook]
    UPSH --> TOOL[AI calls a tool]
    TOOL --> PTUH[PreToolUse Hook]
    PTUH --> RESP[AI generates response]
    RESP --> STOP[Session ends]
    STOP --> STOPH[Stop Hook]
    STOPH -->|exit 0| END[Session ends normally]
    STOPH -->|exit 2| WAKE[Wake AI: complete Act phase]
    WAKE --> RESP
Loading

Hook 1: SessionStart

Trigger: Claude Code session starts.

Purpose: Verify the project environment is ready and remind about PDCA.

Actions:

  • Check that CLAUDE.md and AGENTS.md exist
  • Verify .github/agent/memory/ directory exists
  • Verify .claude/settings.local.json exists and has valid hooks

If missing: Log a warning. This hook doesn't block — it alerts.

Hook 2: UserPromptSubmit

Trigger: User submits a prompt.

Purpose: Detect if the prompt is a coding task and remind about Plan phase.

Actions:

  • Check if the prompt matches a coding task pattern (feat, fix, refactor, etc.)
  • If yes, remind about the 7-file Plan phase reading

Matcher pattern: Only fires for prompts that look like coding tasks — not for conversational questions.

Hook 3: PreToolUse

Trigger: AI is about to use a tool (Write, Edit, Bash, etc.).

Purpose: Block destructive operations unless explicitly authorized.

Actions:

  • Block git push without explicit user request
  • Block SSH connections to unknown hosts
  • Block docker compose commands that modify production
  • Block file deletion outside the project directory

If blocked: The tool call is denied. The AI must explain why and ask for explicit authorization.

Hook 4: Stop (most critical)

Trigger: Claude Code session is about to end.

Purpose: Verify that the Act phase was completed — memory files were updated.

Actions:

  • Check if task-history.md was modified during this session
  • If NOT modified → exit 2 (blocks session end)
  • asyncRewake: true → wakes the AI with a prompt to run Act

Exit codes:

Code Meaning
0 Memory updated — session can end
2 Memory NOT updated — session blocked, AI wakes to complete Act

Configuration file

Hooks are configured in .claude/settings.local.json:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [{
          "type": "command",
          "command": "bash -c '... verification script ...'"
        }]
      }
    ],
    "UserPromptSubmit": [
      {
        "matcher": "feat|fix|refactor|feature|bug|implement|add|新功能|修复|重构",
        "hooks": [{
          "type": "command",
          "command": "bash -c '... reminder script ...'"
        }]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": "bash -c '... safety check script ...'"
        }]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [{
          "type": "command",
          "command": "bash -c '... check if task-history.md modified ...'"
        }]
      }
    ]
  }
}

{{SOURCE_DIR_PATTERN}}

To avoid hooks firing on every file change (README.md, package.json, etc.), hooks are scoped to source directories:

User says: "src/ tests/"
→ Becomes: ^src/\\|^tests/

This regex pattern is embedded in the hook scripts. Only changes to files matching this pattern trigger the hook logic.

Why ask the user?

Different projects have different source layouts:

  • src/ tests/ — typical monorepo
  • lib/ app/ — Ruby/Rails
  • pkg/ internal/ — Go
  • src/ — simple project

The user knows their layout best. The default is src/ tests/.


settings.json vs settings.local.json

File Purpose Commit to git?
settings.json Project-wide settings ✅ Yes
settings.local.json Local overrides (hooks, personal prefs) ❌ No (gitignored)

Conflict resolution

If both files have hooks, settings.local.json takes precedence. If both exist, the installer warns:

⚠️ Both settings.json and settings.local.json exist.
Hooks in settings.json may conflict with settings.local.json.
Recommendation: merge hooks into settings.local.json only.

Hook scripts

The actual hook logic is in the template files. During install, the {{SOURCE_DIR_PATTERN}} is replaced:

SessionStart script (template)

#!/usr/bin/env bash
# Verify ai-coding-ok environment
set -e

REQUIRED_FILES=(
  "AGENTS.md"
  "CLAUDE.md"
  ".github/agent/memory/project-memory.md"
  ".github/agent/memory/task-history.md"
)

for file in "${REQUIRED_FILES[@]}"; do
  if [[ ! -f "$file" ]]; then
    echo "⚠️ [ai-coding-ok] Missing: $file"
  fi
done

echo "✅ [ai-coding-ok] Environment ready. PDCA active."

Stop script (template)

#!/usr/bin/env bash
# Check if memory was updated this session
# {{SOURCE_DIR_PATTERN}} is replaced during install

SOURCE_PATTERN="{{SOURCE_DIR_PATTERN}}"

# Check if task-history.md was modified
if git diff --name-only HEAD | grep -q ".github/agent/memory/task-history.md"; then
  echo "✅ [ai-coding-ok] Memory updated. Session complete."
  exit 0
else
  echo "⛔ [ai-coding-ok] task-history.md was NOT updated!"
  echo "   Complete the Act phase before ending the session."
  exit 2
fi

Platform availability

Hook Claude Code Copilot Cursor
SessionStart
UserPromptSubmit
PreToolUse
Stop

Hooks are Claude Code-specific (Layer 4-5). Copilot and Cursor rely on Layers 1-3 for PDCA enforcement.


Troubleshooting hooks

"Stop hook always exits 2 even when I updated memory"

The hook checks git diff. If task-history.md was modified but not staged, the diff won't show it. Make sure the file is saved.

"PreToolUse blocks legitimate git operations"

Adjust the matcher pattern in settings.local.json to be more specific, or temporarily disable the hook:

{
  "matcher": "Bash",
  "hooks": []  // Disable for this session
}

"Hooks slow down Claude Code"

Each hook runs a shell command. Keep scripts lightweight (<1 second). Avoid network calls or heavy computation.


Next steps

Clone this wiki locally