Files
claude-skills/skills/session-handoff/hooks
vitya f1be677b0a fix(session-handoff): hook command literal path [v0.3.3]
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.
2026-05-25 06:54:15 +03:00
..

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.

{
  "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)

{
  "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:

$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.