Dead man's switch for Claude Code: writes the handoff before your prompt cache dies.
Claude Code keeps your conversation in a 1-hour prompt cache. Reading the full context at minute 50 costs ~10% of the token price; at minute 61 it's full price again. So the moment you walk away from a long session is exactly the moment its state gets expensive to save.
deadman arms a switch every time Claude stops:
- You come back → the next turn disarms it. Nothing happens, ever.
- You stay away → at T-15m before cache death it pings your phone.
At T-10m it wakes the idle session — one last cheap read of everything —
and has Claude write a dated handoff to
.claude/handoffs/, then stop.
Next time you start a session in that workspace, deadman offers the handoff back as context. Walk away mid-task, come back tomorrow, resume in minutes.
(The cache math is real money on API billing. On subscription plans you aren't billed per token — the win there is the loop itself: a phone warning before anything happens, and a written handoff waiting instead of a cold session.)
Needs bash and jq. One file, no daemon.
curl -fsSL https://raw.githubusercontent.com/thevibeworks/deadman/main/bin/deadman -o /tmp/deadman
bash /tmp/deadman install # or: install --project (this repo only)
bash /tmp/deadman doctor # everything wired?
bash /tmp/deadman drill # 10-second live end-to-end self-testinstall copies the script to ~/.claude/deadman/bin/deadman and wires
three hook entries into settings.json (idempotent, with a backup):
two Stop hooks (the "armed" announcement + the sleeper) and one
SessionStart hook (the resume offer). Restart running Claude sessions —
hooks load at session start.
From then on, every turn ends with:
deadman armed: auto-handoff in 50m, warn at 45m if idle (5f3759df; deadman status)
45midle — your phone pings: "claude idle 45m: myproject — auto-handoff in 5m unless you return". Touch the session and nothing more happens.50midle — the session wakes itself, writes.claude/handoffs/2026-07-30-1036-5f3759df.md(goal, verified state, decisions, what not to repeat, next action), refreshes.claude/handoffs/latest.md, and stops. Newest 10 are kept.- Next
claudein that workspace: "an auto-handoff was written 2h ago — read it before starting."
Your repo-root HANDOFF.md (if you keep one by hand) is never touched —
the robot writes only under .claude/handoffs/.
deadman install [--project] wire the hooks into settings.json (idempotent)
deadman doctor verify dependencies, wiring, notifier
deadman drill ~10s live end-to-end self-test
deadman status [n] armed sleepers on this host + last n events
deadman events [n] tail the shared event log
deadman uninstall [--project] remove hook entries (keeps binary + logs)
status shows what's armed right now — session, phase, seconds until
fire, last tick. events is the append-only record of every decision the
switch made (scheduled, tick, notified, fired, and every abort
with its reason).
Optional and pluggable — with no notifier the wake still fires; you just don't get the ping. Auto-detected in order:
DEADMAN_NOTIFY_CMD— any command; getsTITLE BODYappended as args- Bark via
barkme.sh, whenBARK_KEYis set - ntfy when
NTFY_TOPICis set terminal-notifier(macOS)
deadman doctor tells you which one it picked.
Environment variables always win; ~/.claude/deadman/config.env (written
by install, all defaults commented) is for persistent overrides. Times
are offsets from the 1h cache death:
| variable | default | meaning |
|---|---|---|
DEADMAN_AFTER_SEC |
3000 |
fire at 50m idle (T-10m) |
DEADMAN_LEAD_SEC |
300 |
warn 5m before firing (T-15m) |
DEADMAN_ANNOUNCE |
1 |
0 = no "armed" line at each Stop |
DEADMAN_SKILL |
(empty) | delegate the wake to a skill you own |
DEADMAN_NOTIFY_CMD |
(auto-detect) | custom notifier command |
DEADMAN_HANDOFF_DIR |
.claude/handoffs |
where dated handoffs land |
DEADMAN_KEEP |
10 |
dated handoffs kept per workspace |
DEADMAN_TICK_SEC |
30 |
gate re-check interval |
DEADMAN_GRACE_SEC |
5 |
late-flush tolerance (see below) |
Every Stop, an asyncRewake hook forks a sleeper and kills the
session's previous one — so the idle timer resets on any activity, and a
sleeper that survives the full window is the proof you were away. It
re-checks its gates every 30s and dies the moment any of them fail:
- a newer sleeper owns the session (you came back)
- the Claude process exited
- a real
user/assistantentry landed after schedule + grace - this activity position already fired (dedup)
- a sibling session in the project went active (
/clear, session switch)
The subtle part is the idle signal. The transcript file is not
append-only while you're away: Claude writes housekeeping into it
(titles, mode records, away-summaries), and the final message of a turn
can flush to disk after the Stop hook already started. File size and
mtime are lies. deadman reads the timestamp of the last real
user/assistant entry and only counts it as activity if it's later
than the sleeper's start plus a small grace window. Each of those rules
exists because we shipped the naive version first and watched it fail;
the test suite pins all of them.
State that involves PIDs stays host-local ($TMPDIR/deadman — PIDs
don't cross container namespaces). The event log is shared and
host-tagged, so deadman events shows every machine writing to the same
~/.claude.
Caveat: the wake rides on
asyncRewake, an undocumented Claude Code hook field (≥ 2.1.220). If a future release removes it, deadman degrades to doing nothing — it can never break your session.
MIT