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:
2026-05-24 23:36:04 +03:00
parent 1c0d040347
commit cc6c321b57
6 changed files with 274 additions and 2 deletions

View 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,
}))
"