PostToolUse hook in `~/.claude/settings.json` was using `$env:USERPROFILE` (PowerShell syntax), but Claude Code on Windows runs hook commands through git-bash. Bash treats `$env` as an empty variable, leaving `":USERPROFILE\..."` as the literal `-File` argument — PowerShell fails with "invalid filename format" and the hook never fires. Install snippet in hooks/README.md now uses literal absolute path `C:\Users\<you>\.claude\...` with a "Why literal path" section explaining why `$env:VAR` / `%VAR%` / `~` all break through the bash-harness chain on Windows. Retracts the v0.3.0 "live-hook e2e smoke done" closure — that result was from synthetic replay through the PowerShell tool, which bypassed the broken harness chain. Real e2e verification requires this fix plus a CC restart, then a substantive commit to observe `additionalContext` surface.
108 lines
5.3 KiB
Markdown
108 lines
5.3 KiB
Markdown
# 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`. Use `powershell` for stock Windows (PS 5.1, always present); use `pwsh` if you have PowerShell 7+ installed. The hook script runs cleanly under both.
|
|
|
|
**Substitute `C:\\Users\\<you>` with your actual home path before pasting** — see "Why literal path" below.
|
|
|
|
```jsonc
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Bash",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"C:\\Users\\<you>\\.claude\\skills\\session-handoff\\hooks\\commit-detector.ps1\""
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Swap `powershell` for `pwsh` if you prefer PS 7+. To check which you have: `Get-Command pwsh -ErrorAction SilentlyContinue` (empty → PS 7 not installed → use `powershell`).
|
|
|
|
If `hooks.PostToolUse` already exists — append the matcher block to the array. Don't overwrite existing entries.
|
|
|
|
**Restart Claude Code after editing `settings.json`** — hooks are loaded at session start. A running session won't pick up the new hook until it's restarted (close + reopen the CC instance). Verify the hook is active by making a substantive commit and checking for a `Substantive commit detected on ...` system reminder in the next turn.
|
|
|
|
### Why literal path (no `$env:USERPROFILE` / `%USERPROFILE%` / `~`)
|
|
|
|
Claude Code on Windows invokes hook commands through **git-bash**, not PowerShell or cmd.exe directly. The outer-shell layer mangles shell-specific variable references before PowerShell ever sees the args:
|
|
|
|
- `$env:USERPROFILE` (PowerShell syntax) → bash treats `$env` as an empty variable and the rest `:USERPROFILE\...` becomes a literal, so PowerShell receives `-File ":USERPROFILE\..."` and fails with `неверный формат имени` / "invalid filename format".
|
|
- `%USERPROFILE%` (cmd syntax) → bash passes through literally, PowerShell doesn't expand it, same failure.
|
|
- `~/.claude/...` → bash expands `~` to git-bash-style `/c/Users/<you>/...`, which PowerShell's `-File` can't resolve to a real Windows path.
|
|
|
|
Literal absolute path with `C:\\Users\\<you>\\...` (escaped backslashes for JSON) survives every outer shell unchanged. `settings.json` is per-user anyway — portability across machines isn't a concern at this layer.
|
|
|
|
## 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`.
|