claude-handoff runs hooks automatically and spawns a background process, so it
is designed to fail safe and minimize the data it handles. This document
describes the threat model and the mitigations in place.
Please open a private GitHub security advisory rather than a public issue. We aim to respond within a few days.
The plugin runs on a developer's own machine. The trust boundaries:
- Trusted: Claude Code itself and the hook inputs it provides (e.g.
transcript_path,cwd), and theclaudeCLI onPATH. - Untrusted: the contents of the transcript (which can include text from
files or web pages the user opened), the existing
HANDOFF.md, the project.gitignore, and environment variables.
A fully compromised shell (attacker can set arbitrary env vars and run processes) is out of scope — at that point the machine is already lost.
| Risk | Mitigation |
|---|---|
Command injection via CLAUDE_HANDOFF_MODEL reaching the spawned claude call |
The model name is validated against a strict whitelist (/^[A-Za-z0-9._-]+$/); anything else falls back to the default. The prompt is passed via stdin (never parsed by a shell), so the only non-constant argv entry is the validated model. |
Secret leakage into HANDOFF.md (and later re-injection) |
High-confidence secret shapes (AWS/Stripe/GitHub/Slack/Google/OpenAI-Anthropic keys, JWTs, PEM private keys, bearer tokens, URL credentials) are redacted from the digest and git block before the model call. The distiller is also instructed never to emit credentials. tool_result blocks (API responses, file dumps) are excluded from the digest entirely. |
| Secrets committed by accident | .claude/HANDOFF.md is added to .gitignore automatically. If that write fails (restricted permissions), it is logged; verify with git status and add the entry manually if needed. |
Path traversal via CLAUDE_PROJECT_DIR |
The project directory is normalized with path.resolve(), collapsing .. segments. |
Second-order prompt injection via a tampered HANDOFF.md |
The re-injected content is explicitly framed as reference data, not instructions to obey, and is clearly labeled as coming from a previous session. |
| Resource exhaustion / hangs in the background worker | The claude -p call has a timeout; only the tail of pathologically large transcripts is read into memory; the digest is capped well under the stdin limit. |
| Recursion (the distiller triggering the plugin again) | The headless call uses --bare (no plugin auto-discovery) and a CLAUDE_HANDOFF_NESTED env guard. |
| A failing hook breaking the session | Every hook is wrapped in try/catch and always exits 0 with safe/empty output. |
- Secret redaction is best-effort pattern matching, not a guarantee. Do not
rely on it for high-value secrets; treat
.claude/HANDOFF.mdas sensitive. - The background safety net sends a redacted transcript digest to the model. If
you do not want any automatic model call, set
CLAUDE_HANDOFF_AUTO=0and use/handoffonly. - Uncommitted filenames (not contents) may appear in the git state section.