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
|
# 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
|
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
|
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», «отбой», «разбегаемся»."
|
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`)
|
- prefix НЕ в (`meta:`|`docs:`|`style:`|`chore:`|`fix typo`)
|
||||||
- AND (body length > 200 символов OR files changed > 3)
|
- AND (body length > 200 символов OR files changed > 3)
|
||||||
- Плюс: **первый** non-trivial commit сессии — всегда триггерит, даже если ниже порога (старт работы = context shift).
|
- Плюс: **первый** 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):**
|
**Skip (false-positive guards):**
|
||||||
- «закрываем эту таску» — task close, не session. Это zone `using-tasks`.
|
- «закрываем эту таску» — 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