52a0ba0da0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
154 lines
4.1 KiB
Markdown
154 lines
4.1 KiB
Markdown
# 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**
|
|
|
|
```bash
|
|
mkdir -p /workspace/gravl/.claude/hooks
|
|
```
|
|
|
|
**Step 2: Write the script**
|
|
|
|
Create `.claude/hooks/git-clean-check.sh`:
|
|
|
|
```bash
|
|
#!/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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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**
|
|
|
|
```bash
|
|
jq . /workspace/gravl/.claude/settings.json
|
|
```
|
|
|
|
Expected: JSON reprinted with no errors.
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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.
|
|
|
|
---
|