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:
@@ -1,5 +1,5 @@
|
||||
# Task Board
|
||||
_Updated: 2026-05-24 (session-handoff: install + bootstrap-template-extend + hermes-mapping 🟢; 3 из 7 кластера закрыто.)_
|
||||
_Updated: 2026-05-24 (session-handoff: 3 baseline 🟢, posttooluse-hook + existing-projects-upgrade → 🔴.)_
|
||||
|
||||
<!--
|
||||
Canonical layout. One block per task. Per-task deep context lives in
|
||||
|
||||
38
.tasks/session-handoff-posttooluse-hook.md
Normal file
38
.tasks/session-handoff-posttooluse-hook.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# session-handoff-posttooluse-hook
|
||||
|
||||
## Goal
|
||||
Автоматизировать substantive-commit detection в `session-handoff` через PostToolUse hook на `Bash` matcher'е (settings.json уровне), вместо поведенческой памяти агента. Hook parses `git log -1`, применяет ту же substantive-эвристику что и в SKILL.md, и на hit emits `hookSpecificOutput.additionalContext` чтобы Claude Code surface'ил system reminder в следующей итерации агента.
|
||||
|
||||
## Key files
|
||||
- `skills/session-handoff/hooks/commit-detector.ps1` — Windows/PowerShell hook script
|
||||
- `skills/session-handoff/hooks/commit-detector.sh` — Linux/macOS POSIX hook script (требует python3 для JSON parsing)
|
||||
- `skills/session-handoff/hooks/README.md` — opt-in инструкции, cross-platform settings.json snippets, smoke procedure
|
||||
- `skills/session-handoff/SKILL.md` — body When-to-use updated: substantive-commit пункт получил «**Optional**: harness-side hook см. hooks/README.md»
|
||||
- `skills/session-handoff/SKILL.md` frontmatter — bump 0.2.1 → 0.3.0 (MINOR: new opt-in capability)
|
||||
|
||||
## Decisions log
|
||||
- 2026-05-24: **install.sh НЕ мутирует ~/.claude/settings.json**. Auto-rewriting user hook config — неправильная shape для install скрипта. Hooks ship as files; user enables once per machine. SKILL.md и hooks/README.md документируют opt-in step (раз сделал — работает на все sessions).
|
||||
- 2026-05-24: **JSON output protocol**: hook возвращает `hookSpecificOutput.additionalContext` (per Claude Code PostToolUse hook protocol). На hit — JSON; на miss — silent exit 0 без output. Confirmed via claude-code-guide subagent (https://code.claude.com/docs/en/hooks.md § JSON Output Format).
|
||||
- 2026-05-24: **--amend skip** (recommended в task design questions). Amend обычно правит prev session коммит, не новый work artifact.
|
||||
- 2026-05-24: **rebase/cherry-pick noise — deferred**. Hook fires per commit, batch operations spam. Trade-off acceptable for opt-in v0.3.0; defer "только original commit-event (HEAD@{1} != HEAD)" к follow-up если actually annoys.
|
||||
- 2026-05-24: **first-non-trivial-commit-of-session special case — NOT in hook**. Session boundaries are agent-state, не accessible from hook side. Hook uses only body/file thresholds. Под-detection on small first commits acceptable; agent-side эвристика остаётся как backup.
|
||||
- 2026-05-24: **POSIX requires python3** for safe JSON parsing of PostToolUse stdin. Alternatives (sed/awk JSON parsing) fragile. Documented as dep in README.
|
||||
|
||||
## Open questions
|
||||
- [ ] Live-hook smoke test — отдельной сессией (enable hook → substantive commit → see additionalContext surface). Не делалось в этой сессии чтобы не interfere с current commits.
|
||||
|
||||
## Completed steps
|
||||
- [x] Research PostToolUse hook output protocol (claude-code-guide subagent)
|
||||
- [x] Inspect existing ~/.claude/settings.json (no hooks currently configured)
|
||||
- [x] Write commit-detector.ps1 (Windows)
|
||||
- [x] Write commit-detector.sh (POSIX)
|
||||
- [x] Write hooks/README.md (opt-in instructions cross-platform + smoke procedure)
|
||||
- [x] Update SKILL.md body — When-to-use mentions hook as opt-in alternative
|
||||
- [x] Bump SKILL.md 0.2.1 → 0.3.0 (MINOR — new capability)
|
||||
- [x] Reinstall via scripts/install.ps1
|
||||
- [ ] stdin-pipe smoke (substantive HEAD + trivial HEAD)
|
||||
- [ ] STATUS.md → 🟢 (partial: stdin smoke ✓, live-hook deferred)
|
||||
- [ ] commit
|
||||
|
||||
## Notes
|
||||
Live-hook enable + e2e validation = separate task / separate session. Adding hook to settings.json in this active session would fire on every git commit done here, including the closure commit itself — meta-feedback loop best avoided.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: session-handoff
|
||||
version: 0.2.1
|
||||
version: 0.3.0
|
||||
description: "Sliding handoff between CC sessions via .tasks/NEXT_SESSION.md. Read on session start: orient agent, ask user before action. Write on session-end phrase or substantive commit. Session-end phrases: «завершаем сессию», «сворачиваемся», «закругляемся», «wrap up session», «end session», «we're done for now». Trigger-line in CLAUDE.md: `session handoff: read on start, write on end`. Skip task-zone phrases: «закрываем эту таску», «pause», «отбой», «разбегаемся»."
|
||||
---
|
||||
|
||||
@@ -26,6 +26,7 @@ Forward-looking, не timeline: handoff = связка новых вещей к
|
||||
- prefix НЕ в (`meta:`|`docs:`|`style:`|`chore:`|`fix typo`)
|
||||
- AND (body length > 200 символов OR files changed > 3)
|
||||
- Плюс: **первый** non-trivial commit сессии — всегда триггерит, даже если ниже порога (старт работы = context shift).
|
||||
- **Optional**: substantive-commit detection может быть автоматизирован harness-side через PostToolUse hook — см. `hooks/README.md` для opt-in инструкций. С enabled hook'ом первая часть becomes deterministic (parser-side, не behavioral memory).
|
||||
|
||||
**Skip (false-positive guards):**
|
||||
- «закрываем эту таску» — task close, не session. Это zone `using-tasks`.
|
||||
|
||||
91
skills/session-handoff/hooks/README.md
Normal file
91
skills/session-handoff/hooks/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# session-handoff hooks
|
||||
|
||||
Opt-in PostToolUse hook that detects substantive `git commit` invocations and signals the agent ("consider running session-handoff write-mode") via a system reminder. Replaces the agent-side commit-detection heuristic in the SKILL.md `When to use` section with a deterministic harness-side trigger.
|
||||
|
||||
## Why opt-in (not auto-installed)
|
||||
|
||||
`install.sh` deliberately does **not** mutate `~/.claude/settings.json`. Auto-rewriting the user's hook config on every skill install is the wrong shape — user expects `install.sh` to copy files, nothing more. The hook is shipped as scripts; user enables it once per machine.
|
||||
|
||||
## Enable on Windows (PowerShell)
|
||||
|
||||
Add to `~/.claude/settings.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "pwsh -NoProfile -ExecutionPolicy Bypass -File \"$env:USERPROFILE\\.claude\\skills\\session-handoff\\hooks\\commit-detector.ps1\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `hooks.PostToolUse` already exists — append the matcher block to the array. Don't overwrite existing entries.
|
||||
|
||||
## Enable on Linux / macOS (bash)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash ~/.claude/skills/session-handoff/hooks/commit-detector.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The bash variant needs `python3` on PATH (used to parse the PostToolUse JSON stdin).
|
||||
|
||||
## What gets signalled
|
||||
|
||||
On a successful `git commit` whose subject prefix is not in {`meta`, `docs`, `style`, `chore`} or `fix typo`, **and** whose body exceeds 200 characters or which touches more than 3 files — the hook emits a `hookSpecificOutput.additionalContext` system reminder of shape:
|
||||
|
||||
> Substantive commit detected on `<cwd>`: `<subject>` (N files changed, body M chars). Consider invoking session-handoff write-mode to update `.tasks/NEXT_SESSION.md`.
|
||||
|
||||
On any of these → silent skip (exit 0, no JSON):
|
||||
|
||||
- malformed PostToolUse stdin
|
||||
- Bash command isn't `git commit`
|
||||
- command is `git commit --amend`
|
||||
- commit returned non-zero exit
|
||||
- cwd isn't a git work-tree
|
||||
- subject prefix is in trivial set
|
||||
- body ≤ 200 chars AND files ≤ 3
|
||||
|
||||
## Smoke test (without enabling the hook)
|
||||
|
||||
Pipe a synthetic PostToolUse JSON to the script. On Windows:
|
||||
|
||||
```powershell
|
||||
$payload = @{
|
||||
tool_input = @{ command = 'git commit -m "subject"' }
|
||||
tool_response = @{ exit_code = 0 }
|
||||
cwd = (Get-Location).Path
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
$payload | pwsh -NoProfile -File .\skills\session-handoff\hooks\commit-detector.ps1
|
||||
```
|
||||
|
||||
If your `HEAD` is a non-trivial commit (e.g. recent `feat:` with > 200-char body or > 3 files), output is JSON containing `additionalContext`. Otherwise — empty stdout (silent skip).
|
||||
|
||||
## Caveats
|
||||
|
||||
- **Rebase / cherry-pick noise.** Every commit in a rebase or cherry-pick batch will re-fire the hook. Deferred to a follow-up if it actually annoys in practice; the hook is opt-in so the cost is bounded.
|
||||
- **First-non-trivial commit of session.** The agent-side heuristic in SKILL.md treats the *first* non-trivial commit of a session as "always substantive" regardless of thresholds. The hook can't see session boundaries — uses only body/file thresholds. Slight under-detection on small first commits; acceptable trade-off for harness-side determinism.
|
||||
- **No automatic write-mode invocation.** Hook only signals. The agent still decides whether to run session-handoff write-mode in response — keeps the user-agency invariant from SKILL.md `What NOT to do`.
|
||||
69
skills/session-handoff/hooks/commit-detector.ps1
Normal file
69
skills/session-handoff/hooks/commit-detector.ps1
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# session-handoff PostToolUse hook (PowerShell).
|
||||
#
|
||||
# Reads PostToolUse JSON from stdin, detects whether the just-completed
|
||||
# Bash tool call was a substantive `git commit`, and on hit emits JSON to
|
||||
# stdout with `additionalContext` so Claude Code surfaces a system reminder
|
||||
# in the next agent iteration ("substantive commit — consider session-handoff
|
||||
# write-mode").
|
||||
#
|
||||
# Substantive heuristic (mirrors session-handoff SKILL.md):
|
||||
# prefix NOT in (meta:|docs:|style:|chore:|fix typo) AND
|
||||
# (body > 200 chars OR files > 3)
|
||||
#
|
||||
# Silent skip on: malformed JSON, no command, --amend, failed commit,
|
||||
# non-git cwd, trivial prefix, below thresholds. Never blocks the tool call
|
||||
# (PostToolUse cannot, by design).
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Read stdin
|
||||
try {
|
||||
$raw = [Console]::In.ReadToEnd()
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) { exit 0 }
|
||||
$hook = $raw | ConvertFrom-Json -ErrorAction Stop
|
||||
} catch {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Only Bash tool, only git commit (not --amend)
|
||||
$cmd = $hook.tool_input.command
|
||||
if (-not $cmd) { exit 0 }
|
||||
if ($cmd -notmatch '(?<![\w-])git\s+commit(?![\w-])') { exit 0 }
|
||||
if ($cmd -match '(?<![\w-])git\s+commit\b.*--amend') { exit 0 }
|
||||
|
||||
# Only on successful commit
|
||||
if ($null -ne $hook.tool_response.exit_code -and $hook.tool_response.exit_code -ne 0) { exit 0 }
|
||||
|
||||
# Resolve cwd; require a git work-tree
|
||||
$cwd = $hook.cwd
|
||||
if (-not $cwd) { $cwd = (Get-Location).Path }
|
||||
$inside = & git -C $cwd rev-parse --is-inside-work-tree 2>$null
|
||||
if ($inside -ne 'true') { exit 0 }
|
||||
|
||||
# Parse last commit
|
||||
$subject = (& git -C $cwd log -1 --format='%s').Trim()
|
||||
$body = (& git -C $cwd log -1 --format='%b')
|
||||
$files = ((& git -C $cwd diff-tree --no-commit-id --name-only -r HEAD) | Measure-Object).Count
|
||||
|
||||
# Trivial-prefix check (Conventional Commits prefix before optional scope + colon)
|
||||
$prefix = $subject -replace '^([a-z]+)(\([^)]+\))?:.*$','$1'
|
||||
$trivial = @('meta','docs','style','chore')
|
||||
if ($trivial -contains $prefix) { exit 0 }
|
||||
if ($subject -match 'fix\s+typo') { exit 0 }
|
||||
|
||||
# Threshold check
|
||||
$bodyLen = if ($body) { $body.Length } else { 0 }
|
||||
if ($bodyLen -le 200 -and $files -le 3) { exit 0 }
|
||||
|
||||
# Substantive — emit JSON
|
||||
$msg = "Substantive commit detected on " + $cwd + ": ``" + $subject + "`` (" + $files + " files changed, body " + $bodyLen + " chars). Consider invoking session-handoff write-mode to update .tasks/NEXT_SESSION.md."
|
||||
|
||||
@{
|
||||
hookSpecificOutput = @{
|
||||
hookEventName = 'PostToolUse'
|
||||
additionalContext = $msg
|
||||
}
|
||||
systemMessage = 'session-handoff: substantive commit detected'
|
||||
suppressOutput = $false
|
||||
} | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
||||
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