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>
74 lines
2.1 KiB
Bash
74 lines
2.1 KiB
Bash
#!/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,
|
|
}))
|
|
"
|