Files
gravl/docs/plans/2026-02-21-stop-hook.md
T
2026-02-21 18:47:13 +01:00

4.1 KiB

Stop Hook Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add a Stop hook to .claude/settings.json that blocks Claude from stopping if there are uncommitted tracked-file changes or unresolved conversational TODOs.

Architecture: Two parallel hooks registered under the Stop event in .claude/settings.json. Hook 1 is a bash command that checks git diff --name-only HEAD; Hook 2 is a prompt hook that reads the conversation transcript for unresolved work.

Tech Stack: Bash, Claude Code hooks API (settings.json), git


Task 1: Create the git clean check script

Files:

  • Create: .claude/hooks/git-clean-check.sh

Step 1: Create the hooks directory

mkdir -p /workspace/gravl/.claude/hooks

Step 2: Write the script

Create .claude/hooks/git-clean-check.sh:

#!/bin/bash
set -euo pipefail

dirty=$(git -C "${CLAUDE_PROJECT_DIR:-.}" diff --name-only HEAD 2>/dev/null)

if [ -n "$dirty" ]; then
  # Format list for the reason message
  file_list=$(echo "$dirty" | tr '\n' ', ' | sed 's/, $//')
  printf '{"decision": "block", "reason": "Uncommitted changes in tracked files: %s"}' "$file_list" >&2
  exit 2
fi

echo '{"decision": "approve"}'

Step 3: Make it executable

chmod +x /workspace/gravl/.claude/hooks/git-clean-check.sh

Step 4: Manually test the script

With the current dirty working tree it should block:

cd /workspace/gravl && bash .claude/hooks/git-clean-check.sh

Expected: exits 2, stderr contains {"decision": "block", ...} listing modified files.

Step 5: Test the approve path

cd /workspace/gravl && git stash && bash .claude/hooks/git-clean-check.sh; git stash pop

Expected: exits 0, stdout is {"decision": "approve"}.

Step 6: Commit

git -C /workspace/gravl add .claude/hooks/git-clean-check.sh
git -C /workspace/gravl commit -m "feat(hooks): add git clean check script for Stop hook"

Task 2: Register hooks in settings.json

Files:

  • Create: .claude/settings.json

Note: .claude/settings.local.json already exists with permissions. The new settings.json is for hooks (tracked by git; settings.local.json is not).

Step 1: Write settings.json

Create .claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/git-clean-check.sh",
            "timeout": 10
          },
          {
            "type": "prompt",
            "prompt": "Review the conversation transcript. Look for any tasks, TODOs, follow-up items, or promises Claude made (e.g. \"I'll fix X\", \"TODO:\", \"we should also…\", \"I'll address that later\") that were mentioned but not completed in this session. If any are unresolved, return {\"decision\": \"block\", \"reason\": \"Unresolved items: <comma-separated list>\"}. If everything mentioned was completed, return {\"decision\": \"approve\"}.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Step 2: Validate JSON

jq . /workspace/gravl/.claude/settings.json

Expected: JSON reprinted with no errors.

Step 3: Commit

git -C /workspace/gravl add .claude/settings.json
git -C /workspace/gravl commit -m "feat(hooks): register Stop hook in settings.json"

Task 3: Verify hooks load in Claude Code

Step 1: Restart Claude Code

Hooks load at session start. Exit and reopen claude in /workspace/gravl.

Step 2: Check hooks loaded

Run /hooks inside Claude Code.

Expected: Stop hook appears with two entries (command + prompt).

Step 3: Trigger the hook manually

Ask Claude to stop (or say "ok thanks, bye"). With current dirty state, the git hook should block and report uncommitted files.

Step 4: Commit everything clean and verify approve path

git -C /workspace/gravl add -A && git -C /workspace/gravl commit -m "chore: clean working tree to test Stop hook approve path"

Then attempt to stop Claude — both hooks should approve.