feat(session-handoff): add PostToolUse commit-detector hook [v0.3.0]
Hooks live in skills/session-handoff/hooks/:
- commit-detector.ps1 Windows/PowerShell
- commit-detector.sh POSIX (python3 for JSON parsing)
- README.md opt-in instructions, cross-platform settings.json
snippets, smoke procedure
Hook reads PostToolUse stdin, detects substantive `git commit` (prefix
not in meta/docs/style/chore + fix typo, AND body >200 chars OR files >3).
On hit, emits hookSpecificOutput.additionalContext so Claude Code
surfaces a system reminder next iteration. Silent skip on --amend, failed
commits, non-git cwd, trivial prefix, below thresholds.
install.sh deliberately does NOT mutate settings.json — opt-in via the
README hook config snippet, applied once per machine. SKILL.md body
mentions the hook as an optional alternative to the agent-side
behavioral heuristic.
Bump 0.2.1 -> 0.3.0 MINOR (new opt-in capability, backward-compatible —
SKILL keeps working without the hook).
Deferred (separate follow-ups if needed):
- live-hook e2e smoke (would interfere with current session commits)
- rebase/cherry-pick batch deduplication
Closes [session-handoff-posttooluse-hook] (partial: stdin smoke,
live-hook deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
73
skills/session-handoff/hooks/commit-detector.sh
Normal file
73
skills/session-handoff/hooks/commit-detector.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# session-handoff PostToolUse hook (POSIX). See commit-detector.ps1 for prose.
|
||||
set -euo pipefail
|
||||
|
||||
raw=$(cat)
|
||||
[[ -z "$raw" ]] && exit 0
|
||||
|
||||
# Helper: extract a JSON path via python3
|
||||
jget() {
|
||||
python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.loads(sys.argv[1])
|
||||
out = d
|
||||
for k in sys.argv[2].split('.'):
|
||||
if isinstance(out, dict):
|
||||
out = out.get(k)
|
||||
else:
|
||||
out = None
|
||||
break
|
||||
print('' if out is None else out)
|
||||
" "$raw" "$1" 2>/dev/null || echo ''
|
||||
}
|
||||
|
||||
cmd=$(jget tool_input.command)
|
||||
[[ -z "$cmd" ]] && exit 0
|
||||
|
||||
# Only `git commit`, not `--amend`
|
||||
if ! echo "$cmd" | grep -qE '(^|[^[:alnum:]_-])git[[:space:]]+commit($|[^[:alnum:]_-])'; then exit 0; fi
|
||||
if echo "$cmd" | grep -qE '(^|[^[:alnum:]_-])git[[:space:]]+commit\b.*--amend'; then exit 0; fi
|
||||
|
||||
# Only on successful commit (if exit_code present and non-zero, skip)
|
||||
exit_code=$(jget tool_response.exit_code)
|
||||
if [[ -n "$exit_code" && "$exit_code" != "0" ]]; then exit 0; fi
|
||||
|
||||
cwd=$(jget cwd)
|
||||
[[ -z "$cwd" ]] && cwd=$(pwd)
|
||||
|
||||
# Require git work-tree
|
||||
git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
||||
|
||||
subject=$(git -C "$cwd" log -1 --format='%s')
|
||||
body=$(git -C "$cwd" log -1 --format='%b')
|
||||
files=$(git -C "$cwd" diff-tree --no-commit-id --name-only -r HEAD | wc -l | tr -d ' ')
|
||||
|
||||
# Trivial-prefix check
|
||||
prefix=$(echo "$subject" | sed -E 's/^([a-z]+)(\([^)]+\))?:.*$/\1/')
|
||||
case "$prefix" in
|
||||
meta|docs|style|chore) exit 0 ;;
|
||||
esac
|
||||
echo "$subject" | grep -qE 'fix[[:space:]]+typo' && exit 0
|
||||
|
||||
# Threshold
|
||||
body_len=${#body}
|
||||
if [[ "$body_len" -le 200 && "$files" -le 3 ]]; then exit 0; fi
|
||||
|
||||
# Substantive — emit JSON via python3 to handle quoting safely
|
||||
python3 -c "
|
||||
import json, sys
|
||||
msg = (
|
||||
'Substantive commit detected on $cwd: \`' + '''$subject''' + '\` '
|
||||
+ '($files files changed, body $body_len chars). '
|
||||
+ 'Consider invoking session-handoff write-mode to update .tasks/NEXT_SESSION.md.'
|
||||
)
|
||||
print(json.dumps({
|
||||
'hookSpecificOutput': {
|
||||
'hookEventName': 'PostToolUse',
|
||||
'additionalContext': msg,
|
||||
},
|
||||
'systemMessage': 'session-handoff: substantive commit detected',
|
||||
'suppressOutput': False,
|
||||
}))
|
||||
"
|
||||
Reference in New Issue
Block a user