| date | 2026-06-13 00:00:00 PT | ||||||
|---|---|---|---|---|---|---|---|
| ver | 2.1.0 | ||||||
| author | Ice-ninja | ||||||
| model | claude-opus-4-8 | ||||||
| tags |
|
Restores CLI coding tool and agentic sessions following a system crash.
_____ _____ _____ _____ _____ _____ _____ _____
| _ | _ | _ | _ | | _ | _ | _ | _ |
| |_| | |_| | |_| | |_| | | |_| | |_| | |_| | |_| |
|_____|_____|_____|_____| |_____|_____|_____|_____|
|_____|_____|_____|_____| |_____|_____|_____|_____|
Crash-guard keeps a durable recovery ledger for agentic CLI / TUI sessions and restores them after WSL restarts, terminal crashes, or failed restore attempts.
It is not a PTY recorder. It stores enough metadata to relaunch the right continue/resume command in the right working directory, through the same captured non-secret proxy-routing environment.
The two halves cover different sets, and the difference matters when you are trying to work out why a session you remember having open was not reported.
Sentinels (cg_run, the ledger) work for anything launched through the
wrapper: claude, codex, opencode, hermes, pi, kilo, ante, rtk,
and wrapper-launched commands such as rtk/xx. Anything started outside
cg_run is invisible to this half, however it was installed.
Detection (crash-guard recover) reads harness session stores directly and
needs no wrapper, but only where a scanner exists:
| harness | how it is decided |
|---|---|
| omp | definitive -- session_exit record |
| hermes | definitive -- ended_at / end_reason in state.db |
| claude code | definitive once cg-session-end is installed; heuristic before that |
| codex | heuristic -- unfinished turn + write in the pre-boot gap |
pi (legacy ~/.pi) |
heuristic -- predates the session_exit marker |
| qwen | heuristic -- writes claude's schema, so the same scanner reads it |
Not yet scanned: gemini, opencode, kilo, jcode, agy. Their session
files exist and are readable; the scanners are simply not written, so those
harnesses are covered only by the sentinel half.
Recovery needs two different facts, and no single source has both.
Which sessions were open — markers. omp ends a clean session with a final
{"type":"custom","customType":"session_exit"} record, and hermes sets
ended_at + end_reason in ~/.hermes/state.db. For those the answer is a
fact: the marker is there or it isn't. Claude Code gains the same tell once
bin/cg-session-end is installed as a SessionEnd hook (see below). Codex and
legacy ~/.pi write no terminal record at all, so they fall back to requiring
two signals — a last write inside the pre-boot gap and an unfinished final
turn (a tool_use with no tool_result, or a user turn with no reply).
This half is complete and needs no instrumentation: it reads what the harness already wrote, so it sees sessions crash-guard never launched.
How to bring one back — sentinels. cg_run records the argv a session was
launched with, including any wrapper (rtk hermes), and the proxy-routing
environment the alias injected (OPENAI_BASE_URL=...). None of that is
recoverable from a transcript. Resuming hermes --resume <id> without it
reconnects to a different backend than the session was using and drops the
wrapper's output filtering — the session comes back, but not the session you
had.
So they are not two implementations of the same thing, and not an A/B test. Markers find the sessions; sentinels say how to relaunch them faithfully. One command uses both:
crash-guard recover # every open session, from the markers
crash-guard recover --restore # reopen chosen ones in tmux, in their own dirs
crash-guard recover --compare # cross-check against the inferential detectorA session with a matching sentinel is restored with its environment replayed
and is labelled launch sentinel matched. One without still restores — just
with the plain resume command, which is what the harness itself would do.
crash-guard restore (the cgr alias) drives the sentinel ledger directly.
When that ledger is empty — nothing launched through cg_run this boot — it
falls back to the markers rather than reporting nothing, because "no sentinels"
and "nothing was running" are different facts.
A session that carries a session_exit record closed cleanly no matter how
close to the crash it was last written — which is exactly the case where
mtime-proximity scoring gets it wrong.
bin/crash-guard the command. Session ledger, restore, and `recover`
bin/cg-recover marker-based detection; `crash-guard recover` runs this
bin/cg-forensic inferential detection, for the harnesses with no marker
bin/cg-session-end SessionEnd hook that gives Claude Code a close marker
bin/cg-snapshot periodic liveness snapshots of running sessions
bin/cg-correlate reconciles shell history, session logs and snapshots
bin/cg-harness-monitor samples memory use per harness over time
shell/crash-guard.sh shell integration: cg_run, cgr, cgs, cgh
scripts/install.sh install files only; does not edit shell rc files
README.md setup and operating guide
CHANGELOG.md project changes
LICENSE MIT license
Claude writes no terminal record, so a clean /exit and a power cut look
identical. It does fire a SessionEnd hook on clean shutdown, and a crash
fires nothing — so one record from that hook is enough. Add to
~/.claude/settings.json:
"hooks": {
"SessionEnd": [
{ "hooks": [ { "type": "command",
"command": "/path/to/crash-guard/bin/cg-session-end" } ] }
]
}The marker only ever rules a session out: transcripts written before the hook existed have no marker either, so its absence still has to earn its verdict through the two-signal heuristic.
If you are an agent setting this up on a new machine, do this from WSL/bash or a Linux shell:
git clone https://github.com/aaaronmiller/crash-guard ~/code/crash-guard
cd ~/code/crash-guard
bash scripts/install.shAdd this shell integration block to ~/.zshrc or ~/.bashrc. The PATH
export MUST come BEFORE the source line, and both should be BEFORE any
tool aliases that use cg_run:
# --- crash-guard session recovery: insert BEFORE your tool aliases ---
export PATH="$HOME/.local/bin:$PATH" # MUST be before source line
source "$HOME/.config/crash-guard/crash-guard.sh" # defines cg_run()
# Aliases using cg_run go AFTER the source line:
alias cgr='crash-guard restore' # crash-guard restore tracked sessions
alias cgs='crash-guard status' # crash-guard status/preview
alias cgr-archive='crash-guard restore --from-archive --terminal ghostty' # recover archived restore via Ghostty/tmux tabs
alias cgh='crash-guard history' # crash-guard OS boot history browserYou can tag a session with a human-readable name at launch. Names appear in restore groups and the detailed plan, helping distinguish multiple sessions in the same directory:
alias my-pi='cg_run --name "pi-on-crash-guard" rtk -- pi -c'Then add tracked launcher aliases. On this machine, the active proxy aliases use
xx, so the tracked aliases are:
alias cc='cg_run xx cip' # Claude init, proxy
alias ccc='cg_run xx ccf' # Claude continue, proxy, free tier
alias hsi='cg_run xx hip' # Hermes init, proxy
alias hsr='cg_run xx hcf' # Hermes continue, proxy, free tier
alias psi='cg_run xx pip' # Pi init, proxy
alias psi-c='cg_run xx pcf' # Pi continue, proxy, free tier
alias qw='cg_run xx qip' # Qwen init, proxy
alias qw-c='cg_run xx qcf' # Qwen continue, proxy, free tier
alias codex-run='cg_run xx xip' # Codex init, proxy
alias codex-res='cg_run xx xcf' # Codex continue, proxy, free tier
alias oc='cg_run xx oip' # OpenCode init, proxy
alias ante='cg_run xx aip' # Ante init, proxyIf the machine does not use xx, wrap the real command directly:
alias cc='cg_run claude -- rtk claude --dangerously-skip-permissions'
alias ccc='cg_run claude -- rtk claude --continue --dangerously-skip-permissions'
alias codex-run='cg_run codex -- rtk codex --dangerously-bypass-approvals-and-sandbox'
alias codex-res='cg_run codex -- rtk codex resume'
alias oc='cg_run opencode -- rtk opencode'
alias hsi='cg_run hermes -- rtk hermes'
alias hsr='cg_run hermes -- rtk hermes --resume'System-specific notes:
- Ghostty on Linux/Fedora: install
tmux; crash-guard uses tmux windows as tab-equivalents because Ghostty exposesnew_tabas a keybind, not a+new-tabCLI action. - WezTerm: install
weztermorwezterm.exe; existing-window tab spawning is preserved. - Kitty: enable remote control if you want native Kitty tabs; crash-guard falls back to a new Kitty OS window if remote launch fails.
- Windows Terminal from WSL:
wt.exeandwsl.exemust be on PATH.
Open a new shell or run:
source ~/.zshrcRun tracked aliases normally. Each launcher writes a live sentinel before the tool starts and appends to durable history on start, stop, archive, and restore.
# On any machine where crash-guard is installed from a git clone:
crash-guard self-update # auto-detect repo
crash-guard self-update --repo ~/code/crash-guard # explicit pathThe first time you install, the installer writes a repo marker file. After that,
crash-guard self-update handles stash, pull, reinstall, and version verification.
No manual git pull or re-install needed.
If you didn't install from a git clone, just clone and install once:
git clone https://github.com/aaaronmiller/crash-guard ~/code/crash-guard
cd ~/code/crash-guard && bash scripts/install.shFuture updates: crash-guard self-update.
Common commands:
crash-guard # restore picker; Enter restores all visible sessions
cgr # same as crash-guard restore
cgs # current live/stale/crashed sentinel status
cgh # OS boot-period history browser
cgr-archive # retry records already moved to archiveThe restore picker shows recovery groups, then lets you use arrow keys or
numbers to restore all visible sessions, select a group, or select one
individual session inside a group. Pressing Enter on the default restores all
visible recoverable sessions. Use --group N when you intentionally want only
one group.
Restore groups show:
- Session duration (e.g.
(8h35m),(51s)) — sessions that ran < 5 seconds are automatically filtered out (they're test launches or missing commands, not worth restoring). - Git context — branch name and dirty✓/dirty✗ indicator for each session's working directory.
- Session name — if set via
cg_run --name "my-label", shown in[brackets]. - Ephemeral filtering — sessions that ran < 5 seconds are never shown.
Useful non-interactive forms:
crash-guard --dry-run
crash-guard restore --yes
crash-guard restore --group 2
crash-guard restore --group 2 --item 3
crash-guard restore --boot <boot-id> --group 1
crash-guard restore --from-archive --list-groups
crash-guard restore --from-archive --group 1
crash-guard restore --from-archive --group 1 --item 2
crash-guard restore --from-archive --terminal ghostty
crash-guard restore --no-spawn
crash-guard restore --include-closed # also show cleanly closed sessions
crash-guard restore --include-closed --list-groups # preview onlyCrash-guard keeps append-only history at:
~/.local/share/crash-guard/history.jsonl
It also folds in live sentinels and archived sentinels, so a failed restore does
not erase the only recovery record. When you restore a visible group,
crash-guard saves that group membership before archiving old sentinels; later,
crash-guard restore --from-archive lets you select the saved group again or
choose individual sessions inside it.
crash-guard history first lists OS boot periods with record counts. In an
interactive terminal, choose a period with arrow keys or a number; crash-guard
then shows that period's recovery groups.
Recovery grouping:
- Clean exits are kept as individual closed-session records.
- Crashed, stale, or archived sessions that ended together are grouped.
- The newest recoverable group is group
1for plaincrash-guard. - Older OS boot periods are restored with
--boot <boot-id>.
Cleanup is explicit and dry-run by default:
crash-guard excise --older-than 365
crash-guard excise --older-than 365 --apply--apply requires typing Jettison the ghosts before old history is removed.
| backend | behavior |
|---|---|
tmux |
new tmux windows, meaning full-screen tab-like workspaces, not panes |
ghostty |
resolves to tmux on Linux for tab-like restore behavior |
wezterm |
new tabs in the current/sole WezTerm window when resolvable |
kitty |
new tabs through Kitty remote control, with OS-window fallback |
windows-terminal |
new Windows Terminal tabs through wt.exe + wsl.exe |
Force a backend:
crash-guard restore --terminal tmux
crash-guard restore --terminal ghostty
crash-guard restore --terminal weztermDefault config lives at:
~/.config/crash-guard/programs.json
It controls terminal backend selection, per-tool continue/resume commands, session-store locations, captured environment patterns, the restore pre-hook, and restore spawn delay.
Key config options:
terminal.backend—auto(default),tmux,wezterm,kitty,ghostty,windows-terminalterminal.order— preference order for auto-detectionrestore.pre— shell command to run before each restore (default:_proxy_stack_auto_start)restore.spawn_delay— seconds between session restores (default:0.5, prevents OOM)programs.<name>.continue— command to continue a session in a directoryprograms.<name>.resume_by_id— command to resume by session ID (with{id}placeholder)programs.<name>.store— session store location for Tier 2 resume (claude,codex)programs.<name>.use_sentinel_argv— if true, replay recorded argv exactly (for wrappers likertk)env_patterns— glob patterns for environment variables to capture per-session
crash-guard: no wezterm CLI found
- Install WezTerm (
weztermorwezterm.exeon PATH) or configurewezterm.cliin config - Or use
--terminal tmux/--terminal ghosttyto force a different backend
crash-guard: no supported terminal CLI found
- Ensure at least one terminal backend is installed:
tmux,wezterm,kitty,ghostty, or Windows Terminal (wt.exe) - Check
crash-guard statusto see current boot's tracked sessions
Restore spawns too many sessions / system OOM
- Reduce
restore.spawn_delayin config (default 0.5s) - Use
crash-guard restore --selectto pick specific sessions - Use
crash-guard restore --dry-runto preview without spawning
Sessions show as rtk instead of actual tool
- Ensure
rtkprogram config hasuse_sentinel_argv: true(default in v2.0+) - Run
crash-guard initto update config with latest defaults
cg_run alias not found
- Ensure
source ~/.config/crash-guard/crash-guard.shis in your~/.zshrcor~/.bashrc - Verify
~/.local/binis in PATH before sourcing
History grows too large
- Run
crash-guard excise --older-than 90 --applyto remove old records - Adjust retention period as needed
Security: what crash-guard stores
cwd(working directory) is recorded in every sentinel — full filesystem paths appear in analytics and metrics. If your path contains sensitive information (e.g./home/me/code/client-acme-corp), it will be visible in the history log and analytics output.argv(command arguments) is recorded in every sentinel. Do not pass secrets as command-line arguments (e.g.--api-key sk-xxx). Use environment variables or config files instead.envcaptures only proxy-routing environment variables (URLs, base endpoints, non-secret placeholders). Real API keys and auth tokens are explicitly dropped byenv_snapshot().- History files are stored in plaintext under
~/.local/share/crash-guard/. They are not encrypted. Protect your home directory accordingly.
Session naming
- Use
cg_run --name "my-label" key -- cmdto tag sessions with a human-readable name - Names appear in restore groups in
[brackets]and in the detailed plan - Helps distinguish multiple sessions running in the same directory
Crash diagnostics
- After a system crash mid-restore, the next login shows a one-line diagnostic:
restored 2/3 via ghostty (1 failed) — critically low memory: ~500MB available - This reads
history.jsonlevents which are fsynced to disk before each spawn - The
restore_startevent includes a system snapshot (memory, process count), the full plan summary, and the terminal backend being used - If you see memory warnings, reduce
restore.spawn_delayor restore fewer sessions
Diagnostic history events (advanced)
The append-only history.jsonl now records detailed restore lifecycle:
restore_start: plan summary, backend, system memory/process snapshot
restore_group: saved selected group membership for archive retry
restore_attempt: per-item, logged BEFORE spawn (survives OOM kill)
restore_ok / restore_fail: per-item result
restore_done: final counts, system snapshot (compare before/after)
All restore events use flush=True + os.fsync() so they survive process kill.
- Runtime state lives under
~/.local/share/crash-guard/. - User config lives under
~/.config/crash-guard/. - The repository has no build step and no third-party Python dependencies.
- Generated caches and local state are ignored by
.gitignore. - License: MIT.
