Skip to content

Commit e16dfeb

Browse files
feat: add worktree workflow hooks for parallel session safety
Add guard-git.sh (PreToolUse) to block dangerous git commands that interfere with parallel sessions, and track-edits.sh (PostToolUse) to log edited files so commits can be validated against the session log. Update CLAUDE.md with worktree-first workflow and fix the Claude Code hooks example in recommended-practices.md to use the correct schema.
1 parent 0166103 commit e16dfeb

6 files changed

Lines changed: 211 additions & 10 deletions

File tree

.claude/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
session-edits.log

.claude/hooks/guard-git.sh

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env bash
2+
# guard-git.sh — PreToolUse hook for Bash tool calls
3+
# Blocks dangerous git commands that interfere with parallel sessions
4+
# and validates commits against the session edit log.
5+
6+
set -euo pipefail
7+
8+
INPUT=$(cat)
9+
10+
# Extract the command from tool_input JSON
11+
COMMAND=$(echo "$INPUT" | node -e "
12+
let d='';
13+
process.stdin.on('data',c=>d+=c);
14+
process.stdin.on('end',()=>{
15+
const p=JSON.parse(d).tool_input?.command||'';
16+
if(p)process.stdout.write(p);
17+
});
18+
" 2>/dev/null) || true
19+
20+
if [ -z "$COMMAND" ]; then
21+
exit 0
22+
fi
23+
24+
# Only act on git commands
25+
if ! echo "$COMMAND" | grep -qE '^\s*git\s+'; then
26+
exit 0
27+
fi
28+
29+
deny() {
30+
local reason="$1"
31+
node -e "
32+
console.log(JSON.stringify({
33+
hookSpecificOutput: {
34+
hookEventName: 'PreToolUse',
35+
permissionDecision: 'deny',
36+
permissionDecisionReason: process.argv[1]
37+
}
38+
}));
39+
" "$reason"
40+
exit 0
41+
}
42+
43+
# --- Block dangerous commands ---
44+
45+
# git add . / git add -A / git add --all (broad staging)
46+
if echo "$COMMAND" | grep -qE '^\s*git\s+add\s+(\.\s*$|-A|--all)'; then
47+
deny "BLOCKED: 'git add .' / 'git add -A' stages ALL changes including other sessions' work. Stage specific files instead: git add <file1> <file2>"
48+
fi
49+
50+
# git reset (unstaging / hard reset)
51+
if echo "$COMMAND" | grep -qE '^\s*git\s+reset'; then
52+
deny "BLOCKED: 'git reset' can unstage or destroy other sessions' work. To unstage your own files, use: git restore --staged <file>"
53+
fi
54+
55+
# git checkout -- <file> (reverting files)
56+
if echo "$COMMAND" | grep -qE '^\s*git\s+checkout\s+--'; then
57+
deny "BLOCKED: 'git checkout -- <file>' reverts working tree changes and may destroy other sessions' edits. If you need to discard your own changes, be explicit about which files."
58+
fi
59+
60+
# git restore (reverting) — EXCEPT git restore --staged (safe unstaging)
61+
if echo "$COMMAND" | grep -qE '^\s*git\s+restore'; then
62+
if ! echo "$COMMAND" | grep -qE '^\s*git\s+restore\s+--staged'; then
63+
deny "BLOCKED: 'git restore <file>' reverts working tree changes and may destroy other sessions' edits. To unstage files safely, use: git restore --staged <file>"
64+
fi
65+
fi
66+
67+
# git clean (delete untracked files)
68+
if echo "$COMMAND" | grep -qE '^\s*git\s+clean'; then
69+
deny "BLOCKED: 'git clean' deletes untracked files that may belong to other sessions."
70+
fi
71+
72+
# git stash (hides all changes)
73+
if echo "$COMMAND" | grep -qE '^\s*git\s+stash'; then
74+
deny "BLOCKED: 'git stash' hides all working tree changes including other sessions' work. In worktree mode, commit your changes directly instead."
75+
fi
76+
77+
# --- Commit validation against edit log ---
78+
79+
if echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
80+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
81+
LOG_FILE="$PROJECT_DIR/.claude/session-edits.log"
82+
83+
# If no edit log exists, allow (backward compat for sessions without tracking)
84+
if [ ! -f "$LOG_FILE" ] || [ ! -s "$LOG_FILE" ]; then
85+
exit 0
86+
fi
87+
88+
# Get unique edited files from log
89+
EDITED_FILES=$(awk '{print $2}' "$LOG_FILE" | sort -u)
90+
91+
# Get staged files
92+
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null) || true
93+
94+
if [ -z "$STAGED_FILES" ]; then
95+
exit 0
96+
fi
97+
98+
# Find staged files that weren't edited in this session
99+
UNEXPECTED=""
100+
while IFS= read -r staged_file; do
101+
if ! echo "$EDITED_FILES" | grep -qxF "$staged_file"; then
102+
UNEXPECTED="${UNEXPECTED:+$UNEXPECTED, }$staged_file"
103+
fi
104+
done <<< "$STAGED_FILES"
105+
106+
if [ -n "$UNEXPECTED" ]; then
107+
deny "BLOCKED: These staged files were NOT edited in this session: $UNEXPECTED. They may belong to another session. Commit only your files: git commit <your-files> -m \"msg\""
108+
fi
109+
fi
110+
111+
exit 0

.claude/hooks/track-edits.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bash
2+
# track-edits.sh — PostToolUse hook for Edit and Write tools
3+
# Logs each edited file path to .claude/session-edits.log so that
4+
# guard-git.sh can validate commits against actually-edited files.
5+
# In worktrees each session gets its own log automatically.
6+
# Always exits 0 (informational only, never blocks).
7+
8+
set -euo pipefail
9+
10+
INPUT=$(cat)
11+
12+
# Extract file_path from tool_input JSON
13+
FILE_PATH=$(echo "$INPUT" | node -e "
14+
let d='';
15+
process.stdin.on('data',c=>d+=c);
16+
process.stdin.on('end',()=>{
17+
const p=JSON.parse(d).tool_input?.file_path||'';
18+
if(p)process.stdout.write(p);
19+
});
20+
" 2>/dev/null) || true
21+
22+
if [ -z "$FILE_PATH" ]; then
23+
exit 0
24+
fi
25+
26+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
27+
LOG_FILE="$PROJECT_DIR/.claude/session-edits.log"
28+
29+
# Normalize to relative path with forward slashes
30+
REL_PATH=$(node -e "
31+
const path = require('path');
32+
const abs = path.resolve(process.argv[1]);
33+
const base = path.resolve(process.argv[2]);
34+
const rel = path.relative(base, abs).split(path.sep).join('/');
35+
process.stdout.write(rel);
36+
" "$FILE_PATH" "$PROJECT_DIR" 2>/dev/null) || true
37+
38+
if [ -z "$REL_PATH" ]; then
39+
exit 0
40+
fi
41+
42+
# Append timestamped entry
43+
mkdir -p "$(dirname "$LOG_FILE")"
44+
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $REL_PATH" >> "$LOG_FILE"
45+
46+
exit 0

.claude/settings.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"type": "command",
99
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-readme.sh\"",
1010
"timeout": 10
11+
},
12+
{
13+
"type": "command",
14+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-git.sh\"",
15+
"timeout": 10
1116
}
1217
]
1318
},
@@ -31,6 +36,23 @@
3136
}
3237
]
3338
}
39+
],
40+
"PostToolUse": [
41+
{
42+
"matcher": "Edit|Write",
43+
"hooks": [
44+
{
45+
"type": "command",
46+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/rebuild-graph.sh\"",
47+
"timeout": 30
48+
},
49+
{
50+
"type": "command",
51+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/track-edits.sh\"",
52+
"timeout": 5
53+
}
54+
]
55+
}
3456
]
3557
}
3658
}

CLAUDE.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,20 @@ If codegraph reports an error, crashes, or produces wrong results when analyzing
112112

113113
## Parallel Sessions
114114

115-
Multiple Claude Code instances run concurrently in this repo. To avoid breaking each other's work:
116-
117-
- **Never unstage files** (`git reset`, `git restore --staged`) — another session may have staged them intentionally.
118-
- **Never delete or revert files** you didn't create or modify in this session.
119-
- **Never run `git checkout -- <file>`** or `git restore <file>` on files outside your task scope.
120-
- **Never run `git add .` or `git add -A`** — only stage files you explicitly changed.
121-
- **Ignore "unexpected" dirty files** — if `git status` shows changes you didn't make, leave them alone. They belong to another session.
122-
- **Do not "clean up" lint/format issues** in files you aren't working on. Another session may be mid-edit.
115+
Multiple Claude Code instances run concurrently in this repo. **Every session must start with `/worktree`** to get an isolated copy of the repo before making any changes. This prevents cross-session interference entirely.
116+
117+
**Safety hooks** (`.claude/hooks/guard-git.sh` and `track-edits.sh`) enforce these rules automatically:
118+
119+
- `guard-git.sh` (PreToolUse on Bash) **blocks**: `git add .`, `git add -A`, `git reset`, `git checkout -- <file>`, `git restore <file>`, `git clean`, `git stash`. It allows `git restore --staged <file>` for safe unstaging.
120+
- `guard-git.sh` also **validates commits**: compares staged files against the session edit log and blocks commits that include files you didn't edit.
121+
- `track-edits.sh` (PostToolUse on Edit/Write) logs every file you touch to `.claude/session-edits.log` (gitignored, per-worktree).
122+
123+
**Rules:**
124+
- Run `/worktree` before starting work
125+
- Stage only files you explicitly changed
126+
- Commit with specific file paths: `git commit <files> -m "msg"`
127+
- Ignore unexpected dirty files — they belong to another session
128+
- Do not clean up lint/format issues in files you aren't working on
123129

124130
## Git Conventions
125131

docs/recommended-practices.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,29 @@ You can configure [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-
197197
"PostToolUse": [
198198
{
199199
"matcher": "Edit|Write",
200-
"command": "codegraph build --incremental"
200+
"hooks": [
201+
{
202+
"type": "command",
203+
"command": "codegraph build",
204+
"timeout": 30
205+
}
206+
]
201207
}
202208
]
203209
}
204210
}
205211
```
206212

207-
This ensures the graph stays fresh as the AI agent modifies files.
213+
This ensures the graph stays fresh as the AI agent modifies files. Incremental builds are automatic — only changed files are re-parsed.
214+
215+
#### Parallel session safety hooks
216+
217+
When multiple AI agents work on the same repo concurrently, add hooks to prevent cross-session interference:
218+
219+
- **Edit tracker** (PostToolUse on Edit|Write): log every file path touched to `.claude/session-edits.log`
220+
- **Git guard** (PreToolUse on Bash): block `git add .`, `git reset`, `git restore`, `git clean`, `git stash`, and validate that `git commit` only includes files from the session edit log
221+
222+
See this repo's `.claude/hooks/track-edits.sh` and `guard-git.sh` for a working implementation. Pair with the `/worktree` command so each session gets an isolated copy of the repo.
208223

209224
---
210225

0 commit comments

Comments
 (0)