/recover: rebuild a crashed Claude Code session from disk
Add a manually-invoked /recover skill that, when the user opens Claude in a repo whose previous session crashed (computer died, terminal closed, Claude broke mid-task) and the session ID is unknown, finds the most-recently-active prior session for that folder, reconstructs its working state from the raw on-disk transcript, and injects a budgeted digest into the current session so work continues right where it left off. It also surfaces the native claude --resume <id> command for a true thread continuation.
Claude Code already ships claude --continue/-c (resume the most recent session in the cwd) and claude --resume (interactive picker). Those are excellent — but they have two gaps /recover fills:
claude --resume (the picker needs a TTY) and spawning a nested claude is nonsense — the child's restored thread can't be piped back into the parent's context. So /recover reads the transcript itself and injects a digest into the current session, and merely prints claude --resume <id> as a suggestion the user runs from a real terminal./recover produces a budgeted, noise-stripped digest (what was the goal, what was the plan, what was in flight) — useful when you want orientation fast, or the full transcript is too large, or you just want to keep going in the session you already have open.The neighboring jacked skills stay in their lanes. The dividing line is the source of truth and the precondition:
| Skill | Precondition | Source of truth | Mechanism |
|---|---|---|---|
retry | Transient API/rate-limit blip, mid-turn | Live context window (session still alive) | Prompt only — no file I/O |
redo | Approach went sideways, want a clean restart | git + intent | Stash + rebranch (throws work away) |
checkpoint | A /checkpoint was deliberately run earlier | Self-authored .claude/checkpoints/*.html | Reload the saved artifact |
jacked | Want a specific past session by topic, any machine | Qdrant semantic index over all sessions | Search → retrieve → inject |
recover (new) | Session crashed before any save; ID unknown | Raw ~/.claude/projects/<slug>/*.jsonl for THIS cwd | Newest prior transcript → reconstruct → inject |
Three components with clean boundaries. The hard parts (JSONL parsing, repo-path mapping, noise-stripping, summary extraction) already exist in transcript.py and config.py — both verified Qdrant-free (stdlib imports only). The new logic is a thin, unit-testable layer plus a Click subcommand plus a Markdown skill.
jacked/recover.py — pure logic. Resolves the project dir for the cwd, ranks candidate sessions (excluding the live one), and builds a budgeted digest. Imports transcript.py + config.py only.jacked recover — a Click subcommand in cli.py (matches the existing Click pattern; not _require_search-gated). Two phases so we confirm before injecting: --json (rank candidates, cheap metadata) and --session <id> --digest (the actual working-state digest).jacked/data/skills/recover/SKILL.md — thin orchestrator. Runs the CLI, presents the auto-pick, confirms, injects the digest, and re-anchors. Installs to ~/.claude/skills/recover/SKILL.md automatically via the existing jacked install glob.
flowchart TD
A[User runs /recover] --> B["jacked recover --json --exclude $CLAUDE_CODE_SESSION_ID"]
B --> C[recover.py: resolve project dir for cwd]
C --> D[Rank candidates by last timestamped line; exclude live session]
D --> E{Any candidates?}
E -->|no| F[Tell user: no prior session for this folder — stop]
E -->|yes| G[Skill shows auto-pick: title · last prompt · when · branch + alternates]
G --> H{User confirms pick?}
H -->|wrong one| I[User names an alternate]
I --> J
H -->|yes| J["jacked recover --session ID --digest"]
J --> K[recover.py build_digest: budgeted, noise-stripped, TodoWrite, incomplete-turn flag]
K --> L[Digest printed into current context = injection + print 'claude --resume ID']
L --> M[Re-anchor: you were doing X, next step was Y — ready to continue]
flowchart TD
A[User runs /recover] --> B["jacked recover --json --exclude $CLAUDE_CODE_SESSION_ID"]
B --> C[recover.py: resolve project dir for cwd]
C --> D[Rank candidates by last timestamped line; exclude live session]
D --> E{Any candidates?}
E -->|no| F[Tell user: no prior session for this folder — stop]
E -->|yes| G[Skill shows auto-pick: title, last prompt, when, branch + alternates]
G --> H{User confirms pick?}
H -->|wrong one| I[User names an alternate]
I --> J
H -->|yes| J["jacked recover --session ID --digest"]
J --> K[recover.py build_digest: budgeted, noise-stripped, TodoWrite, incomplete-turn flag]
K --> L[Digest printed into current context = injection + print 'claude --resume ID']
L --> M[Re-anchor: you were doing X, next step was Y — ready to continue]
recover.py public functions| Function | Responsibility |
|---|---|
resolve_project_dir(cwd, projects_root=None) -> Path | None | Map this folder → its ~/.claude/projects/<slug> dir. Try config.get_session_dir_for_repo(); verify by reading the cwd field inside the dir's newest .jsonl; if mismatch or missing, fall back to enumerating projects_root/* and matching each folder's first cwd field against the real cwd. Defeats the dot-slug bug (jack.neil → jack-neil) and stale dot-preserving folders. |
list_candidates(project_dir, exclude_session_id, now) -> list[SessionCandidate] | List flat *.jsonl (main sessions; subagent transcripts live in <uuid>/subagents/ and are skipped). Exclude exclude_session_id. Rank by last timestamped line (the literal last lines are untimestamped metadata). Tolerate a half-written final line. Each candidate carries session_id, path, ai_title, last_prompt, last_ts, git_branch, msg_count, truncated. |
build_digest(session_path, budget=DEFAULT) -> Digest | Reuse parse_jsonl_file_enriched() + the noise-strippers. Extract the working state (see Digest section). Respect budget; when truncating, emit a visible note pointing to claude --resume <id> — never a silent drop. |
resume_command(session_id) -> str | Return claude --resume <session_id>. Trivial; reimplemented here rather than importing the Qdrant-bound retriever.get_resume_command. |
Two subtleties, both verified empirically against real files on this machine, drive correctness here.
A cwd maps to a projects folder by replacing both / and . with -. /Users/jack.neil/Github/claude-jacked → -Users-jack-neil-Github-claude-jacked. A naïve /-only slug misses it, and a stale dot-preserving folder from an older build also exists. Resolution therefore never trusts the slug alone — it verifies (or discovers) the folder by reading the cwd field stored inside the transcripts.
Robust exclusion strategy, in order:
--exclude "$CLAUDE_CODE_SESSION_ID" at the shell level. This captures the value where it's known to be set, independent of the Python subprocess environment — the primary mechanism.--exclude is empty, read CLAUDE_CODE_SESSION_ID, then CLAUDE_SESSION_ID, from the environment.now and whose tail looks live (ends in fresh assistant/system traffic rather than the untimestamped last-prompt/ai-title/mode/permission-mode tail a finished session leaves). This guards the rare case where the env var is unavailable.
There is no summary line type in the transcript format — recap state lives in lightweight metadata lines. build_digest walks the chosen transcript (skipping isSidechain subagent branches) and assembles, in priority order:
ai-title line (one-line "what this was about").last-prompt line (lastPrompt), the fastest "what was I doing" signal.user messages with isMeta:true and <command-name>… wrappers stripped.assistant text blocks (tool-output noise removed).TodoWrite tool_use block's input.todos (content + status: in-progress / pending / completed), if any. Not every session has one.tool_use names + key inputs (Bash commands, Edit/Write file paths) to show the last concrete operations.Edit/Write/NotebookEdit input.file_path and file-history-snapshot entries.gitBranch, plus mode/permission-mode to re-establish posture.tool_use with no matching tool_result, or a half-written final line, mark "last turn may be incomplete — work was in progress on X" rather than presenting it as finished.# Phase 1 — rank candidates (cheap metadata, machine-readable)
jacked recover --json [--cwd PATH] [--exclude SESSION_ID] [--limit N]
# Phase 2 — build the digest for the confirmed session (the injection payload)
jacked recover --session <id> --digest [--budget N] [--cwd PATH]
| Flag | Meaning |
|---|---|
--json | Emit ranked candidates as JSON: {chosen, candidates:[…]}, each with id/title/last_prompt/last_ts/branch/msg_count/age/truncated. Default phase-1 output. |
--session <id> | Target a specific session (the auto-pick, or an alternate the user named). |
--digest | Print the budgeted working-state digest for --session + the claude --resume <id> line. |
--exclude <id> | Session to exclude (the live one). Skill passes "$CLAUDE_CODE_SESSION_ID"; CLI falls back to env if empty. |
--cwd PATH | Override the working directory to resolve (default: process cwd). |
--limit N | How many candidates to return in phase 1 (default 3). |
--budget N | Digest size cap (chars or lines). Generous default; truncation is logged in-output. |
/recover skill flow
A thin SKILL.md (frontmatter: name: recover + a trigger-tight description anchored on "session/computer crashed mid-work," "didn't run /checkpoint," "session ID unknown," "rebuild this folder's last session from the transcript" — and explicitly not for transient API errors, deliberately-saved state, or topic search across old projects). The body:
jacked recover --json --exclude "$CLAUDE_CODE_SESSION_ID".jacked recover --session <id> --digest; that output is the injected working state. Then surface claude --resume <id> as "for a true native continuation (preserves Claude's internal state), run this in a fresh terminal."MEMORY.md for standing project conventions, and continue the work.--session <alt>.| File | Change |
|---|---|
jacked/recover.py | New. The four public functions above + SessionCandidate/Digest dataclasses. Qdrant-free; imports transcript + config only. |
jacked/cli.py | Add the recover Click subcommand (phase-1 --json + phase-2 --digest). Not _require_search-gated. Tolerant error output (never a stack trace at the user). |
jacked/data/skills/recover/SKILL.md | New. The thin orchestrator skill. Installs via the existing jacked install skills glob. |
tests/test_recover.py | New. Unit tests over recover.py with JSONL fixtures (run with uv run python -m pytest). |
jacked/__init__.py | Version bump. |
README.md | One-paragraph mention of /recover under the skills/commands section. |
resolve_project_dir: dot-slug folder, enumerate-and-match fallback, stale dot-preserving folder ignored, missing folder → None.list_candidates: excludes the live session id; ranks by last timestamped line (not mtime); ignores subagent <uuid>/ dirs; multiple-candidate ordering; tolerates a malformed/half-written final line without aborting the parse.build_digest: extracts ai-title/last-prompt/recent asks/last assistant text; pulls the latest TodoWrite; flags an incomplete final turn; respects --budget and emits the truncation note (no silent drop); strips tool-output noise.resume_command: exact string.
This is a technique skill, so test by application. RED: give a fresh agent "my session in this repo crashed, recover it" without the skill and document the flailing (won't know the env var to exclude, won't handle the dot-slug, may reach for Qdrant/jacked, may dump the whole transcript). GREEN: with the skill, the agent runs the two-phase CLI, confirms the pick, injects a budgeted digest, and offers claude --resume. REFACTOR: close any gaps surfaced.
[search] dependency — works on a bare install, by design.claude --resume — TTY + nested-session problems make it unworkable; we print the command for the user to run./recover only, per the user's ask./jacked's job. /recover is strictly "this folder's last session."/checkpoint.| Scenario | Behavior |
|---|---|
| No project folder resolves for this cwd | Report "no recorded sessions for this folder" and stop. Suggest checking the folder is right. |
| Folder exists but only the live session is present | After excluding the live id, zero candidates → "nothing to recover here yet." |
CLAUDE_CODE_SESSION_ID unavailable | Fall back to env names, then the live-tail heuristic; if still ambiguous, present candidates and let the user exclude the current one explicitly. |
| Multiple very-recent candidates | Auto-pick the newest, but the skill shows the alternates and confirms before injecting (never silently grabs). |
| Crash-truncated final line | Parser skips/repairs the bad last line; digest flags "last turn may be incomplete." |
| Chosen transcript exceeds budget | Digest is truncated with a visible note + the claude --resume <id> pointer to the full thread. |
jacked CLI not on PATH (skill running where the package isn't installed) | Skill detects the failure and tells the user to install/repair jacked, rather than half-recovering. |
--budget units & value — characters vs lines, and the exact cap. Leaning lines (~3000) to mirror /checkpoint; finalize during implementation against a real large transcript.--json ever auto-skip confirmation when there is exactly one candidate and it's unambiguous? Current spec: always confirm (cheap, and wrong-session is the headline risk). Revisit only if it feels heavy in practice.Generated with the jacked HTML artifact template.