Running many Claude Code sessions? Know which ones died, and bring them back.
A reboot or an OOM kill takes down a dozen sessions at once, and Claude Code leaves no record of which ones were mid-task. session-sentinel records every session start and clean exit, lists the ones that never finished, and gives you the resume command for each, or reopens them all as terminal tabs (claude-resume-tabs).
/plugin marketplace add cycanics/session-sentinel
/plugin install session-sentinel@session-sentinel
Hooks apply to sessions started after you install.
Requires Linux and Python 3. journalctl is optional (it supplies the reboot
time shown in the listing); claude-resume-tabs also needs python3-gi and
GNOME Terminal.
$ claude-orphans
2 session(s) started but never closed cleanly (tracking since 2026-07-01)
Killed by reboot at 2026-07-16 01:25
ff306034 2026-07-16 01:21 3.9M 61 turns ~/work/api-server
Died mid-boot (crash / kill -9 / OOM / terminal closed)
5ca8a8a6 2026-07-20 19:09 5.3M 48 turns ~/work/infra
Resume: cd <cwd> && claude --resume <session-id>
Flags: --this-boot (only sessions that died since the last reboot),
--cwd DIR, --all (include sessions that never wrote a transcript), --json,
--rescan (search again for transcripts not found earlier),
--compact [--keep-days N].
claude-resume-tabs reopens the same orphans as GNOME Terminal tabs, each
running claude --resume <id> in the session's original directory:
$ claude-resume-tabs --dry-run
skip 45620d2e working directory is gone: /tmp/exp2/stream
2 tab(s) in a new window:
54162fe3 23 turns ~/work/notes
2adf8ea2 8 turns ~/work/infra
All tabs go into one new window; --window ID adds them to an existing one
(--list-windows prints the ids). Sessions with zero user turns and sessions
whose directory is gone are skipped; --min-turns 0 and --force override
that. A tab stays open after claude exits so a failed resume stays visible;
--close-on-exit turns that off. Other flags: --limit N, --session ID
(repeatable, prefix match), --this-boot, --cwd DIR, --all, -y.
The plugin's bin/ directory is on the PATH of Claude's Bash tool, so inside
a session you can ask Claude to run claude-orphans. To use
the commands from a normal shell, point an alias at the installed copy:
alias claude-orphans='"$(ls -d ~/.claude/plugins/cache/session-sentinel/session-sentinel/*/ | sort -V | tail -1)bin/claude-orphans"'
alias claude-resume-tabs='"$(ls -d ~/.claude/plugins/cache/session-sentinel/session-sentinel/*/ | sort -V | tail -1)bin/claude-resume-tabs"'Vanilla Claude Code leaves no durable record of an unclean exit. Transcripts
(~/.claude/projects/<slug>/<id>.jsonl) have no end marker, so a clean exit
and a kill -9 look the same. The live registry (~/.claude/sessions/<pid>.json)
does keep stale entries after a crash, but Claude Code prunes them the next time
a session starts, which is exactly when you would go looking.
So the plugin records the signal as it happens. Its SessionStart and
SessionEnd hooks append to ~/.claude/session-sentinel/lifecycle.jsonl. A clean
exit fires SessionEnd; a reboot or a kill never does. Started, no clean end,
not running now = died unclean.
- What counts as clean: the end reasons
prompt_input_exit,logout,clearandresume(the last two hand the work to another session). Reasonother, which Claude reports when a SIGHUP/SIGTERM tears it down (for example when the terminal crashes), is unclean, except for headlessclaude -pruns, which always end withother. - Resume, then killed:
--resumereuses the session id, so only an end newer than the latest start closes a session. - Reboot vs crash: the kernel
boot_idis recorded at start. An orphan from another boot died in a reboot; one from this boot crashed on its own.journalctl --list-bootssupplies the reboot time for display only. - PID reuse: a session counts as running only if the PID in Claude Code's
live registry still has the process start time recorded there (field 22 of
/proc/<pid>/stat), so a PID recycled after a reboot is never taken for a running session.
- The start record carries
transcript_pathandcwd, so a transcript is found by path, not by searching. - Only orphan candidates (started, not closed, not running) are looked at. Their
path, size, mtime and turn count are cached in
~/.claude/session-sentinel/index.json; a repeat run is astatper candidate, and a transcript that grew is only read from where it was last counted. - A transcript that is not at its recorded path (resuming from another directory
makes the hook report a path under the new directory while Claude keeps writing
the original file) is looked for under
projects/<slug-of-cwd>/and then with onestatper project directory. That search runs at most once a day per candidate (--rescanforces it). - Once a day,
SessionStartcompacts the log in the background: sessions that closed cleanly more than 30 days ago are dropped; open and orphaned ones are kept. SetSESSION_SENTINEL_KEEP_DAYSto change the window (0turns automatic compaction off).
With a synthetic ~/.claude of 300 orphans, a repeat run takes about 0.2 s at
5,000 transcripts and about 1 s at 20,000 (tests/bench.sh).
All state is in ~/.claude/session-sentinel/: lifecycle.jsonl (the log),
index.json (transcript cache), lifecycle.lock and last-compact. Uninstalling
the plugin stops the hooks; delete that directory to remove the history.
python3 tests/test_sentinel.py # clean exit, kill, reboot/stale PID, moved transcript, compaction
tests/bench.sh 5000 300 # timings against a synthetic ~/.claude in a scratch HOMEMIT