Skip to content

Commit cfe633b

Browse files
fix: track mv/git mv/cp commands in session edit log
Add track-moves.sh PostToolUse hook for Bash that detects mv, git mv, and cp commands, extracts all source and destination paths, and logs them to .claude/session-edits.log. This prevents guard-git.sh from blocking commits that include moved or copied files.
1 parent 088b797 commit cfe633b

2 files changed

Lines changed: 103 additions & 0 deletions

File tree

.claude/hooks/track-moves.sh

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env bash
2+
# track-moves.sh — PostToolUse hook for Bash tool calls
3+
# Detects mv/git mv/cp commands and logs all affected paths
4+
# (both source and destination) to .claude/session-edits.log so that
5+
# guard-git.sh can validate commits that include moved/copied files.
6+
# Always exits 0 (informational only, never blocks).
7+
8+
set -euo pipefail
9+
10+
INPUT=$(cat)
11+
12+
# Extract the command from tool_input JSON
13+
COMMAND=$(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?.command||'';
18+
if(p)process.stdout.write(p);
19+
});
20+
" 2>/dev/null) || true
21+
22+
if [ -z "$COMMAND" ]; then
23+
exit 0
24+
fi
25+
26+
# Only care about mv / git mv / cp commands
27+
if ! echo "$COMMAND" | grep -qE '(^|\s|&&\s*)(mv|git\s+mv|cp)\s+'; then
28+
exit 0
29+
fi
30+
31+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
32+
LOG_FILE="$PROJECT_DIR/.claude/session-edits.log"
33+
34+
# Use node to parse the command and extract all file paths involved
35+
PATHS=$(echo "$COMMAND" | node -e "
36+
const path = require('path');
37+
let d = '';
38+
process.stdin.on('data', c => d += c);
39+
process.stdin.on('end', () => {
40+
const base = path.resolve(process.argv[1]);
41+
const results = new Set();
42+
43+
// Split on && or ; to handle chained commands
44+
const parts = d.split(/\s*(?:&&|;)\s*/);
45+
46+
for (const part of parts) {
47+
// Match: mv / cp / git mv followed by arguments
48+
const m = part.match(/(?:git\s+mv|mv|cp)\s+(.+)/);
49+
if (!m) continue;
50+
51+
// Simple arg splitting that respects quotes
52+
const raw = m[1];
53+
const args = [];
54+
let cur = '';
55+
let q = null;
56+
for (let i = 0; i < raw.length; i++) {
57+
const c = raw[i];
58+
if (q) { if (c === q) q = null; else cur += c; }
59+
else if (c === '\"' || c === \"'\") { q = c; }
60+
else if (c === ' ' || c === '\\t') { if (cur) { args.push(cur); cur = ''; } }
61+
else { cur += c; }
62+
}
63+
if (cur) args.push(cur);
64+
65+
// Filter out flags (-f, -v, --force, etc.)
66+
const paths = args.filter(a => !a.startsWith('-'));
67+
68+
// Resolve each path relative to project root
69+
for (const p of paths) {
70+
const abs = path.resolve(p);
71+
const rel = path.relative(base, abs).split(path.sep).join('/');
72+
if (!rel.startsWith('..')) results.add(rel);
73+
}
74+
}
75+
76+
process.stdout.write([...results].join('\\n'));
77+
});
78+
" "$PROJECT_DIR" 2>/dev/null) || true
79+
80+
if [ -z "$PATHS" ]; then
81+
exit 0
82+
fi
83+
84+
# Append timestamped entries for every affected path
85+
mkdir -p "$(dirname "$LOG_FILE")"
86+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
87+
while IFS= read -r rel_path; do
88+
if [ -n "$rel_path" ]; then
89+
echo "$TS $rel_path" >> "$LOG_FILE"
90+
fi
91+
done <<< "$PATHS"
92+
93+
exit 0

.claude/settings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@
6262
"timeout": 5
6363
}
6464
]
65+
},
66+
{
67+
"matcher": "Bash",
68+
"hooks": [
69+
{
70+
"type": "command",
71+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/track-moves.sh\"",
72+
"timeout": 5
73+
}
74+
]
6575
}
6676
]
6777
}

0 commit comments

Comments
 (0)