Summon
Claude Desktop session toolbox. Four jobs, one store:
| Mode | Invocation | Job |
|------|-----------|-----|
| Transfer (default) | summon [flags] | Copy/move sessions across accounts so they're visible from the account you switch to next |
| Pick / Recover | summon pick · summon recover <id> | Find a past session, resolve its transcript, distill a handover brief, emit a paste-ready handover for a new session |
| Rebind | summon rebind <id> --cwd <newpath> | Fix a session's recorded cwd after the project folder moved |
| Doctor | summon doctor [--json] | Scan every session for broken cwd bindings; report which need rebinding |
Transfer touches no transcripts and makes no API calls. Recover/pick make exactly one optional, gated LLM call (the distillation) and degrade gracefully without it. Transfer is documented first; the toolbox modes follow under Toolbox modes.
When to run it
Before you switch accounts, not after. The natural workflow:
- Notice you're approaching usage limit on the account you're currently using
- Run
summon --to <next-account>— sessions get copied (default) into the next account's dir - Logout from current account in Desktop → Login to the new account
- All your mid-flight sessions appear in the new account's left-hand session picker (the sidebar on the left side of Desktop's Code tab). The Logout/Login is the natural switch you were going to do anyway.
Running summon after hitting the usage limit also works — the file moves are pure local ops, no API needed — but you'll still need to Logout/Login on the destination to see the sessions, since Desktop's session list is cached at login. Doing it proactively just means the Logout/Login is no longer "extra friction," it's the same step you'd be doing anyway.
Mental model
Each Desktop session has two halves:
| Half | Location | Account-bound? |
|------|----------|----------------|
| Metadata JSON | %APPDATA%/Claude/claude-code-sessions/<account>/<workspace>/local_<uuid>.json | Yes — lives under <account> |
| Transcript JSONL | ~/.claude/projects/<encoded-cwd>/<cli-uuid>.jsonl | No — global, shared |
Summon copies (or with --move, relocates) the metadata wrapper into the destination account's dir. The transcript stays put — both wrappers point at the same conversation. After Logout/Login on the destination, the new entries appear in the left-hand session picker (Desktop's Code-tab sidebar).
The uuid-mismatch trap. The wrapper filename uuid (local_<uuid>.json / sessionId) does not name the transcript — the transcript file is named by the wrapper's cliSessionId, a different uuid (e.g. wrapper local_6577b24c-… → transcript e640a2a8-….jsonl). And the transcript's parent dir is the munged cwd (X:\Roam\LCMap\.claude\worktrees\funny-hypatia-5e54f7 → X--Roam-LCMap--claude-worktrees-funny-hypatia-5e54f7), which occasionally doesn't derive from the wrapper's recorded cwd at all. All toolbox modes resolve via cliSessionId at the expected munged path first, then fall back to scanning every project dir for <cliSessionId>.jsonl.
Run
# Wrapper (after install — see below)
summon [flags]
# Or direct
python ~/.claude/skills/summon/scripts/summon.py [flags]
Default behaviour: list candidate sessions across all non-destination accounts, grouped Account → Project → Session, then prompt to copy them into the destination account. Copy semantics by default — sessions remain visible in the source account too. Last 3 days; remote-VM sessions auto-skipped.
Two natural framings of the same operation:
- Push (proactive): you're approaching usage limit on your current account. Run
summon --to <next-account>while still on the current one. Pick which sessions to push. Then Logout/Login is the account switch you were going to do anyway. - Pull (rescue): you've already switched accounts and want to bring earlier sessions over. Run
summon(no--to); destination defaults to your now-current account.
Mechanically identical — the file moves are the same regardless of which framing you have in mind. Push is the recommended workflow because the Logout/Login becomes invisible.
Flags
| Flag | Default | Effect |
|------|---------|--------|
| --to <account> | most-recently-active account | Destination — where the sessions land. Specify when pushing to a different account; omit when pulling into your current account. UUID prefix or email substring |
| --from <account> | all non-destination accounts | Restrict source to one account |
| --days N | 3 | Time window |
| --all | | Disable time filter |
| --cwd <pattern> | | Substring match against session cwd |
| --title <pattern> | | Substring match against session title |
| --pick | | Interactive multi-select by number |
| --move | | Move instead of copy — delete source after copying (lean cleanup) |
| --dry-run | | Preview without touching files |
| --list-accounts | | Show all accounts and exit |
| --peek <id> | | Preview a session's last messages and exit (id prefix or full) |
| --flat | | Flat list instead of grouped hierarchy |
| --select <picks> | | Non-interactive selection: --select "1,2,4" or --select all. Replaces the picker prompt for scripted callers |
| --yes | | Skip the final confirmation prompt only — selection is still required (picker prompt, piped stdin, or --select) |
Toolbox modes (pick / recover / rebind / doctor)
Semantic exit codes across all modes: 0 ok, 2 usage/ambiguous id, 3 session or path not found, 10 doctor found broken sessions.
summon pick — session picker → distilled handover
Interactive picker over the whole session store (all accounts, default last 30 days — --days N/--all to widen, --cwd/--title to narrow). Uses fzf when it's on PATH and the terminal is interactive; falls back to a numbered list (--select N answers it non-interactively). A ● marks sessions active in the last 10 minutes — don't recover a session that's still running.
Selecting a session emits a paste-ready handover on stdout (context panel and progress on stderr, so summon pick | clip stays clean). Same output as recover, below.
summon pick --json skips the picker entirely and emits the filtered inventory as a claude-mods.summon.pick/v1 envelope on stdout — JSON only, no panel glyphs (an empty inventory is "data": [] with exit 0, not an error). Each session row carries: id (short) + sessionId (full) + cliSessionId, title, cwd, projectRoot + worktree (the cwd with any \.claude\worktrees\<name> suffix split out), branch, model + effort, turns, isArchived, isRunning (active in the last 10m), brokenCwd (doctor's check — recorded cwd missing on disk), lastActivityAt (ISO-8601 Z), account + accountEmail, and transcriptPath (resolved via the same wrapper→transcript logic as recover, scan fallback included; null when missing). This feeds the in-chat visual card picker and any scripted caller:
summon pick --json | jq -r '.data[] | "\(.id) \(.title) \(.projectRoot)"'
summon pick --json --rich advances the schema to claude-mods.summon.pick/v2 and adds transcript-derived display metrics to every row — one linear transcript read each, so it's opt-in (the plain --json inventory stays metadata-only and instant). Extra keys: events (transcript line count), toolCalls, densityBuckets (24-bucket activity histogram over the session's lifetime), durationMin, sizeKB (on-disk transcript size), ctxTokens (last-turn context occupancy — input + cache + output, matching Claude Code's live meter), ctxPeak (max before any auto-compaction), ctxWindow (200000, or 1000000 when peak exceeds 200k), ctxPct / ctxPeakPct, and firstAsk (the session's opening ask, boilerplate-stripped). This is the feed for the card picker.
summon recover <id> — distilled handover brief
summon recover 6577b24c — id is a sessionId or cliSessionId, prefix ok. Four-stage flow:
- Extract (in-script, no LLM): parses the transcript JSONL and pulls conversational content only — user/assistant text turns, skipping
tool_resultblobs andtool_useinputs (they are most of the bytes). The final ~15 turns are included verbatim; earlier turns fill the remaining budget from the start (so the goal statement survives), middle elided when too long. Total capped at a char budget (--budget, default 120k). - Distill (cheap, tool-less): pipes the extraction to a single
claude -p --model sonnet --permission-mode dontAskcall — one-shot stdin summarisation, no tools, no agentic loop, neverbypassPermissions(perrules/loop-engineering.md). Produces a brief with fixed sections: Goal / What landed (branch + commits if mentioned) / Unfinished / Open decisions / Key context, ~1k-word cap.--modeloverrides sonnet. - Cache: the brief is written to
<transcript-path>.handover.mdnext to the JSONL and reused while it's newer than the transcript's mtime.--refreshforces re-distillation. - Emit (stdout = the data product): the brief inline plus a pointer clause:
Continue a previous Claude session: 'Fix overlapping photo pins with gentle displacement'.
Branch: claude/funny-hypatia-5e54f7
## Goal
…
## What landed
…
## Unfinished
…
## Open decisions
…
## Key context
…
Full transcript at C:\Users\Mack\.claude\projects\X--Roam-LCMap-…\e640a2a8-….jsonl (session 6577b24c-…, branch claude/funny-hypatia-5e54f7); consult it only if something specific is missing.
Degrade, never hard-fail: if the claude CLI is absent from PATH, or the call fails/times out (60s), recover falls back to the classic non-distilled pointer prompt (Title/Branch/Orig cwd/Transcript + tail-reading instruction) with a stderr warning and exit 0 — worker unavailability is advisory, not an error. --no-distill forces the fallback (no LLM call at all).
| Flag | Default | Effect |
|------|---------|--------|
| --no-distill | | Skip the LLM distillation; emit the plain pointer prompt |
| --refresh | | Ignore a cached <transcript>.handover.md and re-distill |
| --model <m> | sonnet | Model for the distillation call |
| --budget <n> | 120000 | Char budget for the transcript extraction fed to the distiller |
summon rebind <id> --cwd <newpath> — fix cwd after a folder move
When a project folder moves (e.g. X:\Roam\LCMap → X:\Maplab\LCMap), sessions bound to the old cwd fail to restart in the Desktop UI. Rebind repairs the binding:
summon rebind 6577b24c --cwd "X:\Maplab\LCMap\.claude\worktrees\funny-hypatia-5e54f7"
- Backs up every matching wrapper to
~/.claude/summon-backups/<timestamp>/(outside the live store) before touching anything - Atomically rewrites
cwd, and rebasesoriginCwd/worktreePath(worktree sessions record the project root inoriginCwd— the suffix math is handled) - Bridges the transcript: Desktop resolves the transcript via the munged new cwd, so the
<cliSessionId>.jsonlis copied (never moved) into the new munged project dir.--no-transcriptskips this - Verifies by re-reading the wrapper; on mismatch it restores from the backup
- If the same session was transfer-copied into several accounts, all copies are rebound
- When the new cwd is inside a
.claude\worktrees\path, prints a reminder that git worktree links break on folder moves — rungit worktree repair <new-worktree-path>from the repo root (verified fix 2026-07-03 on X:\Maplab\LCMap)
--dry-run previews; --force allows a --cwd that doesn't exist yet. The new cwd must normally exist on disk. After a rebind, restart Desktop (or Logout/Login) so the sidebar re-reads the wrapper.
Wrapper edit + backup + transcript bridge are verified against the live store (throwaway-session test, 2026-07-03). End-to-end "session reopens in the Desktop UI after rebind" — confirm on your first real rebind before bulk-rebinding.
summon doctor — find broken sessions
Scans every wrapper (all accounts, all time) and reports sessions whose recorded cwd no longer exists on disk, with a ready-made summon rebind <id> --cwd <new-location> line per finding. Also counts transcript-missing and found-by-scan sessions. Exit 10 when anything is broken; --json emits a claude-mods.summon.doctor/v1 envelope for scripted use:
summon doctor --json | jq -r '.data[] | "\(.sessionId) \(.cwd)"'
Broken-cwd findings are mostly pruned worktrees (the session ended, the worktree was cleaned — nothing to fix unless you want to recover it, which needs no rebind: summon recover works regardless of cwd) and moved project folders (the real rebind case).
In-chat mode (visual card picker) — the default for picking sessions
When summon is invoked from inside a Claude chat session (Desktop chat, claude.ai), the terminal picker can't run interactively — stdin isn't a TTY, so fzf and the numbered prompt are out. This card picker is the default way to present sessions in chat — reach for it whenever the user asks to see, pick, recover, or summon sessions, not just when they say "picker".
- Run
summon widget --days 30(add--cwd/--titlefilters as asked). It prints the finished, self-contained card-picker HTML on stdout — the rich inventory already trimmed and injected into the template. - Pass that stdout straight to the
show_widgettool aswidget_code. That's the whole job: no manual injection, no key-trimming, no reading a file back. The builder also writes the same HTML to%TEMP%\claude\summon-widget.html(override with--out), so you canReadit if you'd rather not re-run.
Why one command, not hand-assembly (don't "simplify" this away).
show_widgetaccepts only inlinewidget_code— no file path — and its CSP blocks any fetch, so the session data must be inlined. The old manual flow (pick --json --rich→ hand-merge data into the template →Readthe assembled file to inline it) was expensive and broke:--richfor ~75 sessions is >100 KB (it spools to a tool-results file), and once injected as one long JSON line the assembled file trips the 25k-tokenReadcap and can't be paginated (Read is line-based; the megaline is indivisible).summon widgetfixes all of it in-process: it drops 0-turn stubs, caps to the most-recently-active--limit(default 24), keeps only the keys the template consumes, downsamples the density strip, injects one session object per line (soReadcan paginate the mirror file), and holds the assembled HTML under a hard--max-kbbyte budget (default 28 KB ≈ <15k tokens) so it never spools and never trips the Read cap — capping the session count with a stderr note if it must. Flags:--include-stubs,--full-density,--limit N,--max-kb N,--days N/--all,--out PATH.
The widget itself needs no further setup: archived sessions are hidden by default (a "show archived" toggle reveals them); cross-account copies dedupe by sessionId; it has a client-side age filter (24h/3d/7d/30d/all — summon widget pre-selects the one matching --days) so the user narrows in-widget without a CLI re-run. Each card shows a colour-coded context-usage gauge chip (token count + % of the session's window — 200k or 1M, auto-detected from peak; green/amber/red by fill) in the stats row, an activity-density strip, and chips for model/effort, age, turns, messages, tool calls, on-disk size, and duration, plus grid/list and sort controls. The header stays light — just project, tags, and the action icons (grouped right, wrap-safe) — so nothing overruns the card border. firstAsk is the mechanical opening ask; you MAY replace it with a distilled one-liner by setting a summary field on a row (edit the mirror file or post-process the JSON) before rendering.
Manual fallback (only if summon widget is unavailable): run summon pick --json --rich --days 30, parse the claude-mods.summon.pick/v2 envelope, drop its data array into the <script id="D"> block of assets/picker-widget.html (the widget consumes pick/v2 objects verbatim — keys documented in the template header), and render via show_widget. Watch the Read-cap trap above: keep the injected array one-object-per-line and trim to a couple dozen sessions.
- Act on the
sendPromptcallbacks the widget fires. Per-card↗ summonand⟳ recover(and the footer's "Recover/Summon selected") are worded to be spawned as background chips — when one arrives, callspawn_task(one chip per session) rather than doing the work inline, so the user's current turn keeps flowing:- "Recover … as a background chip" → one
spawn_taskper session (the batch button sends a single prompt listing all selected — fan it into one chip per session, not one mega-chip, so each recovers independently in its own project folder). For each chip:- Title = the original session name, verbatim (e.g.
revoicing) — never aRecover "…" sessionlabel. The chip should look like a continuation of the original in the sidebar, not a new errand. cwd= the session's project root (strip any\.claude\worktrees\<name>suffix).- Word the prompt so the chip is the recovered session: it reads the original transcript (resolve via
summon recover <id>/ the wrapper→transcript logic), writes a hand-off brief, and resumes the work in place in the project folder. The chip must not spawn a further chip and must not open the original worktree path as a separate session — that path is reference-only, for locating the branch and any in-progress changes. (The failure mode this prevents: a chip prompt that says "start a fresh session there" plus a worktree path makes the recovering chip spawn a second chip into the worktree. The chip already is the fresh session — tell it to continue, not to spawn.)
- Title = the original session name, verbatim (e.g.
- "Summon (copy) these…" → transfer flow:
summonwith--selectfor exactly those sessions,--dry-runpreview first, then the real run once the user confirms. - "Peek session…" →
summon --peek <id>.
- "Recover … as a background chip" → one
The template is deliberately self-contained: host CSS variables + the host's Tabler ti webfont (both available in the show_widget context, light/dark safe), no external assets, and the host-provided sendPrompt(text) bridge for the buttons. Chat contexts only — terminal users keep the fzf/numbered picker; don't route a TTY user through the widget.
Auto-detect rules
- Destination: account with the most recent filesystem activity (mtime of any session JSON). This reliably tracks the active Desktop account.
- Source: by default, all accounts except destination. Use
--from <account>to restrict to one. - Workspace dir under destination: most-recently-active existing workspace. New UUID is created if the destination has no workspaces yet.
Display
Output follows the Terminal Panel Design System (panel header, body with │ rail, footer, ASCII fallback when stdout isn't UTF-8). The candidate hierarchy is Account → Project → Session, with sessions globally numbered for picker selection (3, 5, 7).
╭── 🪄 summon ──────────────────────────────────────────────── → mknv74 ───●
│
├── 4 sessions · from 1 account · last 3d
│
├── dev@example.com (4)
│ ├── X:\Projects\Axiom (2)
│ │ ├── 1. train-fasttext 30t 16h
│ │ └── 2. make-doom-for-mips 64t 16h
│ └── X:\Work\client-site (2)
│ ├── 3. timekeeper 35t 16h
│ └── 4. agency-os 17t 16h
│
│ 💡 best run BEFORE switching accounts: copy sessions to the next
│ account first, then Logout/Login (the switch you were doing anyway)
│
╰── # select · a all · blank cancel ───────────────────────────────────●
Header shows → destination. Summary line shows count, source breadth, and active filter window. Body shows Account → Project → Session hierarchy with global numbering for picker selection (3,5,7). A rotating hint tile sits above the footer; the footer shows the active hotkeys.
Edge cases handled
| Case | Behaviour |
|------|-----------|
| Session cwd is /sessions/<vm>/mnt (remote) | Skipped — no local transcript to bridge |
| Transcript JSONL missing on disk | Skipped with warning (orphan metadata) |
| Same sessionId already in destination | Skipped (idempotent) |
| Destination has no workspace dirs | New workspace UUID created |
| Stdout is not UTF-8 (Windows cp1252) | ASCII fallback for all panel glyphs |
| Stdout is not a TTY or NO_COLOR set | Plain text, no ANSI escapes |
Sidebar refresh
Desktop loads sessions into its left-hand session picker on login and doesn't watch the filesystem afterwards (verified via bundle inspection — no chokidar, no relevant fs.watch on the session dir, only direct fs.readdir calls). Summon throws a best-effort nudge at fs.watch (sentinel pings, mtime touches, rename ping-pong) but don't rely on it — assume Logout/Login is required to populate the sidebar with new sessions.
This is why summon is best run before switching accounts: the Logout/Login is what you'd do anyway. Running summon as a "rescue" after the fact still works mechanically, but the Logout/Login still has to happen.
If sessions still don't appear:
- Try View → Reload (rarely helps; Ctrl+R only re-renders)
- Logout → Login triggers a full filesystem rescan and always works
Wrapper install
Symlink (or copy) the wrapper into a directory on PATH:
# Linux/macOS/Git Bash
ln -s ~/.claude/skills/summon/bin/summon ~/.local/bin/summon
# Windows (PowerShell)
copy "$env:USERPROFILE\.claude\skills\summon\bin\summon.cmd" "$env:USERPROFILE\bin\summon.cmd"
Then summon pick, summon doctor, etc. work directly from any shell.
Architecture reference
Full file system layout, session schemas, account binding, and the validated cross-account transfer procedure live in docs/references/claude-desktop-internals.md (claude-mods). That document is canonical; this skill is the operating manual.
Anti-patterns
- Waiting until you've already hit the limit — the file moves still work, but you've burned the chance to wrap up your current message before switching. Run summon proactively while you still have usage on the source.
- Expecting sessions to appear in the sidebar without Logout/Login — Desktop's session list is loaded on login; the kitchen-sink fs.watch nudge is best-effort and shouldn't be relied on. The Logout/Login becomes painless if you've timed summon as a push before switching.
- Running while Desktop is mid-write to a session JSON — quit Desktop first if you've literally just closed the session you want to push.
- Trying to summon remote sessions — they have no local transcript and can't be transferred.
- Hardcoding account UUIDs — use
--list-accountsfirst, then email substring (more readable, less brittle). - Treating this as a transfer for archived sessions — it's for mid-flight work; archived sessions belong in the source account's archive view.
- Using
--movefor sessions you might want to access from both accounts — copy is default precisely because multi-account workflows are the common case. - Rebinding without checking the new path —
rebindrefuses a nonexistent--cwdfor a reason; a typo'd rebind is two edits instead of one.--forceis for pre-creating bindings, not for skipping the check. - Recovering by pasting the whole transcript — the handover brief exists so the new session starts from a distilled summary and consults the JSONL only for specifics. Feeding a full multi-MB transcript into a fresh session burns the context you were trying to save.
- Re-distilling on every recover — the brief is cached at
<transcript>.handover.mdand reused while the transcript is unchanged; reach for--refreshonly when the session has genuinely moved on since the cache was written.