Skip to content

Latest commit

 

History

History
930 lines (755 loc) · 44.7 KB

File metadata and controls

930 lines (755 loc) · 44.7 KB

Use Session Kit

The picker

SSH opens an ordinary shell. Type kit to open the picker. With shell integration enabled, Linux Bash may also print a short kit: open sessions hint; neither Bash nor macOS zsh starts the picker until you ask.

The picker is one list. Sessions ready for this window come before sessions open elsewhere. Inside each group the state order is question, needs you, working, then idle; machine sessions sit behind one counted row. New session, Projects, Closed sessions, and Help follow the session rows.

Enter           open the top or highlighted row; on an empty list, start new
b               go back; from the home screen, quit to an ordinary shell
digits          mark sessions: 5 · 1,4 · 7-9
letters         filter the list as you type
q               quit from the home screen when no filter text is typed
Esc             clear text, step back, then quit when nothing else remains
Ctrl-C          abandon the line being typed
Ctrl-D          quit the picker from anywhere
↑ / ↓           move the highlight on the cursor-driven screen
mouse           scroll, highlight, and open on the cursor-driven screen

Nothing on the screen asks you to confirm anything. An action runs and then says what it did, Closed 3 sessions., and a close is recoverable from Closed sessions. Picker navigation covers the list, marking, filtering, the mouse, and the action panel in full.

Opening a session that is open elsewhere defaults to moving it here after a fresh identity check. The earlier window returns to its picker and the conversation is never duplicated. New session defaults to Claude Code. The action panel for a Claude or Codex session also offers account, model, history, name, colour, close, and, where applicable, restore actions.

Picker rows and ordinary action lines hide shpool IDs and conversation UUIDs. sp detail and the JSON modes expose exact identity for diagnosis. Commands accept an ID wherever they accept a number, so a script that already holds one keeps working.

Keys in the key-driven picker

The installed kit command starts this key-driven surface directly. An interactive Bash shell that loaded Session Kit's shell integration can instead route its kit function through the cursor-driven launcher when that feature is enabled; see Troubleshoot. The key-driven surface accepts:

Enter           open the top row, or start new when the list is empty
number          open a session, or show what can be done with one open elsewhere
h number        read a session's history without opening it
n               new session
k numbers       close sessions: k 5 · k5 · k 5, 6, 8 · k 4-7 · k all
name number     give a Claude or Codex conversation a name of your own
name reset number    drop that name and show the provider's again
fork number     start a separate fork of a Claude or Codex conversation
model number    move an idle Claude or Codex conversation to another configured model
x               show or hide machine sessions behind their counted row
a               everything that needs you
g               move to the next session that needs you
d number        dismiss a repair that failed, from the Needs you screen
/text           filter names, providers, accounts, models, and projects as you type
group           group by state, provider, or project
c               compact rows: no headings, more sessions on screen
next / prev     move between pages; > and < do the same
r               refresh the list and clear the filter
m               more: sessions outside the kit, closed sessions, projects
p               projects: add, review, hide, or restore new-session directories
o               show sessions outside the kit, read-only
u               restore a closed conversation
?               show this table from the picker itself
b / q           quit the home screen for a regular shell
quit / exit     the same, by name

A key takes its number with or without the space: k5 and k 5 are the same close. Closing keeps the typed number through the result and refresh. ? prints the same table from the picker itself, so the two cannot drift apart.

Why the key-driven picker's screen clears while you type

An idle picker keeps itself current. It collects a snapshot in the background every few seconds and repaints only when something visible changes, so it never runs in front of your prompt. Session numbers are stable, so a repaint never changes what a number means, and the active search and page survive one.

A repaint clears the screen. Characters you typed without pressing Enter live in the terminal's own input queue, which this process cannot inspect: in canonical mode the kernel reports an incomplete line as no input at all, on both Linux and macOS. So a repaint landing mid-command erases the echo of what you typed while the characters themselves stay queued and intact. Press Enter and the command you started still runs.

Set SESSION_KIT_PICKER_REFRESH_SECONDS=0 to switch this off, or to a number of seconds (minimum 2) to change the pace.

When activation selects a newer release, a running picker reloads the new launcher at a safe refresh point and restores its filter, page, and selection. If the new launcher cannot start, the current picker stays in place and reports the degraded target.

Command help

sp help              every command, grouped, with a line each
sp help <topic>      one area in detail
sp --help, sp -h     the same reference

sp help answers before anything else is required, including on a machine where shpool is missing or half-installed, that is the moment the instructions are most needed. The topics are sessions, names, accounts, history, selectors, unavailable, exit-codes, completion, and machine.

Every Session Kit command follows the same rule: -h and --help print their reference to standard output and exit 0, and an argument mistake prints usage to standard error and exits 2. session-kit help covers installation and maintenance; sp help covers day-to-day session work.

The verbs beginning picker-, plus sp restore-exact, are not part of that human list. The picker and the managed shell call them with proof arguments a person does not hold; sp help machine names them for anyone reading the code.

shpool_status --help separates the modes that never touch disk from the ones that refresh the cached inventory, fill in pending provider titles, or change recovery state, several of them look like reads and are not.

Tab completion

Session Kit installs one Bash completion file under each command name that takes arguments:

~/.local/share/bash-completion/completions/sp
~/.local/share/bash-completion/completions/session-kit
~/.local/share/bash-completion/completions/shpool_status
~/.local/share/bash-completion/completions/shpool_reaper
~/.local/share/bash-completion/completions/codex_resume_here

With the bash-completion package present, a new shell loads the right file on demand. Without it, source one copy from your own startup file:

source ~/.local/share/bash-completion/completions/sp

It completes verbs, help topics, flags, provider names, color names, and the aliases in your projects file. It never runs a Session Kit command and never asks the manager for state, so a Tab press cannot change anything or stall on a snapshot. That is also why it does not complete session numbers: the only honest source for those is a live inventory, and building one on Tab would be a state change nobody asked for.

Set SESSION_KIT_COMPLETION_DIR to install the copies elsewhere, or to none to skip them. Uninstall removes only files that still carry the kit's own marker line, so an edited copy is left alone.

Exit codes

Every Session Kit command uses the same statuses.

Status Meaning
0 the command did what it was asked to do
1 refused or failed; the reason is on standard error
2 wrong arguments, or a selector that matched no session
3 live evidence was refused as stale, partial, or ambiguous (shpool_status --strict-json, --guard-json), or shpool_reaper found its disable sentinel
64 the launcher was invoked under a name that is not a Session Kit command
69 something required is unavailable: the current-release link, a helper missing from the pinned release, or Bash 4 or newer on macOS
74 the picker refused the action it was asked to take
75 the picker's attach failed after the action was allowed
76 a reopen ran after a provider crash and the reopened provider exited non-zero; the session closes if its conversation can be brought back, and otherwise stays open with the reason on screen
78 the launcher found a release or manager link outside the install root
127 codex_resume_here could not find the codex command
143 sp was terminated (SIGTERM or SIGHUP)

A refusal is a status, not a suggestion: nothing partial has happened. Session Kit disables a whole action rather than performing part of it.

Start a session

sp new claude [project-alias]
sp new codex [project-alias]
sp new shell [project-alias]
sp new <project-alias>
sp new claude [project-alias] --account <alias>
sp new codex [project-alias] --model <model-id>
sp new codex [project-alias] --origin machine

The last form uses the provider stored for that project alias. When the project has a committed session-kit.toml and is on your project list, its provider, account, and model are used unless a flag here overrides them; see Projects.

Guided New asks for the provider, project, and account before launch. When Matrix has current, healthy advice that names the same provider, its recommended account is preselected and Enter confirms it. Missing, stale, unhealthy, logged-out, or provider-mismatched advice leaves the account unselected. Codex currently has no preselection because the available Matrix advice is not provider-qualified for Codex; choose its account manually. This fail-closed behavior prevents advice for one provider from selecting an account for the other.

--model starts the provider on that model; the identifier is validated for the provider before anything is launched. --origin machine stamps the session as a machine session, so it stays out of the picker's default list and lives behind the counted row instead; SESSION_KIT_ORIGIN=machine in the launching environment does the same.

A session started from inside another session is a machine session even without either of those, because that is how agents start sessions and none of them says so. If you run sp new in one of your own windows and want the new session in your own list, say --origin human. Everything the picker starts is yours; the picker runs in the login shell, which is inside no session.

That rule is about creating. sp restore-exact and sp restore bring back a conversation that already had an owner, so they carry the origin it was closed with, whichever way it goes and wherever you run them, and a conversation with no record is yours. See picker-navigation for the full rule, including the one case where an unstamped session is listed as a machine's.

Sessions use modern Bash on macOS even when the account's normal SSH shell is zsh.

A new Claude session starts with an explicit UUID, so native macOS process evidence can bind it immediately. A new Codex TUI has no conversation UUID until its first message; before then the picker labels it Codex started, no messages yet. Opening and closing that terminal work normally, while recovery, fork, name, and color actions wait for Codex to publish its conversation identity, because each depends on that UUID. With the optional Codex App Server integration active, Session Kit also accepts an exact conversation rollout held open by the managed app-server process, and refuses the association when that server has zero or multiple candidate conversations.

Accounts

sp account list
sp account enroll <claude|codex> <alias> <email>
sp account verify <claude|codex> <alias>
sp new <claude|codex> [project-alias] --account <alias>

Enrollment creates a separate provider profile. Claude Code profiles use separate CLAUDE_CONFIG_DIR roots and Codex profiles use separate CODEX_HOME roots. Complete the provider's normal sign-in, then run verify. Session Kit does not ask for, copy, or show the provider token.

The home screen shows a short account alias in an aligned column beside CLD or CDX. Session detail shows the bound email and provider-supplied plan, and sp account list includes the last verification time. The account-choice screen shows current Matrix health, usage, and any provider-qualified recommendation. Missing details remain unknown; Session Kit does not estimate them.

To change the account for an existing Claude or Codex conversation, open the session's actions in the picker and choose Change account. It does not take over a session that is open in another window. Choose a verified account; the switch runs on Enter and the line says what changed. Nothing is asked first, the evidence below is taken before the switch, not after a question.

Session Kit proceeds only when fresh evidence proves the exact conversation has no active turn, tool, hook, subagent, child agent, or background provider work. A provider waiting for your reply is idle enough to switch. Session Kit transfers only the exact conversation artifacts, resumes the same provider UUID under the selected profile, and verifies it before changing the displayed alias. The conversation history, title, project, color, and session number stay the same. If the target resume or account check fails, Session Kit restores the private checkpoint and original binding, then tries to resume the exact conversation on the original profile. It reports the exact boundary if rollback cannot be proved.

A session created before account profiles existed needs a one-time managed-shell recreation for its first account change. Session Kit explains this, then proves the conversation has no active work before it starts. The original window briefly reconnects, but the exact conversation UUID, history, title, project, color, and session number remain. Later changes use the account-aware shell and do not repeat that migration.

One account change is automatic. When the usage feed shows the account a live conversation is signed in to has no weekly quota left, Session Kit carries that conversation to another enrolled account and tells you afterwards. It happens at most once per conversation: a session that spends a second account stops and reports rather than walking to a third. It never moves into an account with less than a quarter of its weekly window left, and it never moves at all while the usage feed is stale or unreadable. If nothing qualifies, it stands down and waits for you.

Four things it will never do on its own: switch an account back on that you switched off, move a session whose live account disagrees with its record, silently drop a model you asked for by name (the handoff carries the same model record), or leave a failed move needing recovery, if the handoff does not prove itself, the conversation stays where it was.

If a message about a move cannot be delivered, it stays owed and is retried rather than being marked as sent.

sp account-auto-switch <session> says what would happen to one session and changes nothing. Add --apply to actually move it. The periodic check is part of the watchdog, which passes --apply itself; that only happens when the watchdog is in SESSION_KIT_WATCHDOG_MODE=repair, the same setting that lets it act on a live session at all. Only one pass at a time may run on a machine.

When the owner-only $XDG_STATE_HOME/session-kit/account-switching-off sentinel exists, Session Kit disables enrollment, new account selection, manual switching, and automatic switching, without deleting profiles, changing provider-owned login data, or stopping running providers. Its default path is ~/.local/state/session-kit/account-switching-off. Nothing in Session Kit creates that file for you.

Find and inspect

sp list
sp detail <session-number|shpool-id>
sp find <text>
sp health

sp detail also lists each live child shell or worker that has been running for at least one hour. Its age appears on the same row. This reuses the normal session process inventory; a recorded child conversation with no exact live process is not given an estimated age.

Sessions outside the kit appear in a separate read-only view. They have no number and cannot be opened here.

Open or move

sp go <session-number|shpool-id>
sp takeover <session-number|shpool-id>

sp go opens a detached exact managed session. sp takeover moves an attached session to the current terminal after a fresh identity check; the earlier window returns to its picker, and the provider conversation is not duplicated.

Names

sp name <session-number|shpool-id> <title>
sp name reset <session-number|shpool-id>

Title priority is:

  1. local manual name;
  2. explicit provider rename;
  3. retained automatic title;
  4. deterministic provider and project description;
  5. shortened identity fallback;
  6. shell command or Idle shell.

Reset removes only the local manual name. It does not hand the session back to automatic naming: once you have named a session, nothing renames it for you.

Renaming inside the provider counts too. /rename in Codex or Claude is read back, becomes the session's name everywhere Session Kit shows it, and is kept as a name you chose, so no automatic pass restores what it replaced. It also wins over an earlier sp name: Session Kit knows the last title it pushed into the provider itself, so a provider title that differs from it was typed after that push, and the newer of your two names is the one you meant. Your capitals are yours; a name is stored and shown exactly as you typed it. A session renamed this way before you upgraded settles itself the first time the kit reconciles; you do not have to rename anything twice.

Display setup

Recommended terminal: Ghostty

Session Kit is developed and tested against Ghostty. Stock Ghostty already supports the title pushes and truecolour sequences the kit uses, so it needs no display-related setting. Keep your own font, theme, shell-integration, and window settings.

Any truecolour terminal can run Session Kit. The key-driven picker writes 24-bit colour and uses synchronized terminal output during repaint. The cursor-driven picker falls back to a smaller palette when the terminal reports fewer than 256 colours. Set NO_COLOR=1 or SESSION_KIT_NO_COLOR=1 for plain text; state words and labels keep the same meaning.

Claude Code status line

Installation places config/claude/statusline.sh at ~/.claude/statusline.sh and registers it in every managed Claude profile with a two-second refresh interval. Line 1 is:

<session name>, [MODEL] user@host:/directory | N% context

The session name uses its kit colour. The inventory lookup for that tint uses the default ~/.local/state/session-kit/inventory.json; a custom SESSION_KIT_STATE_DIR or XDG_STATE_HOME therefore gets the safe bold-name fallback. The rest of the line still works.

Line 2 is an extension point:

5h N% <time left> · 7d N% <time left> · account@example.invalid

Without a valid cache it reads quota -- and leaves line 1 intact. An executable ~/.claude/statusline-quota-refresh.sh may fill the cache. Session Kit calls it in the active profile, no more than once every 180 seconds and in the background. It must write $HOME/.claude/cache/quota_headers as key: value lines containing:

  • x-probe-account, matching the profile email;
  • anthropic-ratelimit-unified-5h-utilization and anthropic-ratelimit-unified-5h-reset;
  • optionally the matching 7d utilization and reset fields.

The installed example is ~/.local/lib/session-kit/current/extras/statusline-quota-refresh.example. It deliberately leaves the vendor endpoint as a TODO and makes no network call until you supply one. Copy it beside the status line only after filling that TODO; the core kit never handles credentials or makes this request for you.

The status-line quota segment uses Linux utilities, including GNU stat; the name, title, and colour integrations do not depend on it.

Claude attention evidence

The optional attention_hook.sh makes needs you react to Claude events between polls. It is hand-deployed: install, update, rollback, and uninstall do not copy, register, or remove it. Register the installed canonical copy for Claude's Notification, UserPromptSubmit, and SessionEnd events, as shown in the Claude integration contract. SESSION_KIT_ATTENTION_SOURCE=auto merges hook and polling evidence; poll ignores hook records, and hook is intended for drills.

Codex status bar

Session Kit does not configure or replace the Codex status bar. Codex owns that surface. For each managed launch, the kit passes two command-line overrides: the installed tui.terminal_title items (activity and thread by default) and the session's sk-<colour> theme. These options apply to that process only and never edit ~/.codex/config.toml.

A new Codex process can start before the conversation has a name, so its own bar may temporarily show an identifier. Opening a detached, proven-idle provider can refresh it automatically. For an attached provider, the key-driven picker offers the guarded refresh action only when the exact process is idle, has no subagents, and has a stored title. That maintenance action is not exposed by the cursor-driven action panel. An App Server refresh restarts only the generation-bound remote TUI, not the server or conversation.

Terminal window and tab titles

sp go, sp takeover, sp new, and picker open or move push the session name with the standard OSC 0 terminal-title sequence. The kit strips control bytes, collapses whitespace, and caps the title at 64 characters. Returning to the picker restores session kit. A restore does not claim the tab before the session is actually opened.

Claude may update its own title at SessionStart or after the next submitted prompt. Codex receives activity and thread as per-launch title items. Set SESSION_KIT_TAB_TITLE=off to disable both the kit's title pushes and the Codex override; Session Kit deliberately does not set Claude's vendor title-disable switch because that also suppresses hook-provided titles.

Session colours

Every Claude Code and Codex session carries an identity-derived colour on its picker row and in provider chrome. Shell sessions have none. Collisions walk the provider's palette until every colour is in use:

  • Claude: red, blue, green, yellow, purple, orange, pink, cyan;
  • Codex: lime, magenta, silver, sand, sky, sea.

The palettes are separate, so a Claude and Codex session never share a kit colour. Codex receives an installed theme with a per-launch override. Claude receives name and colour transcript records before its first visible frame; an explicit provider /rename or /color remains authoritative.

To settle existing live assignments after a palette or authority change:

sp color reconcile

The command moves only sessions that need to move and is safe to repeat. A running Claude window applies a changed provider colour at its next start or resume. Picker semantics never depend on colour alone.

When a provider exits

A clean exit closes the session. /exit inside Claude, the provider ending with status 0, ends the shell with it, returns the session number to the ordinary quarantine, and lands you at the picker. The conversation is in Closed sessions, and one Restore brings it back. There is no opt-in marker: one policy, on every machine.

A crash is different. On a non-zero status the conversation reopens itself once, with a one-line notice saying so. A second crash within a minute stops the loop, so a conversation that cannot start cannot spin. What happens then depends on one question, and the session is told which way it went:

  • if the conversation can be brought back, the session closes and lands in Closed sessions under its own name, exactly as a clean exit does;
  • if it cannot, no conversation was ever recorded, its transcript is gone or unreadable, the Closed list could not be written, or you had marked the session keep, the session STAYS OPEN and says which of those it was. Closing it would be the one move that loses the conversation for good, so the kit leaves it for you to close from the picker.

A reopen that is REFUSED rather than run never closes the session, whatever the reason: a refusal is not evidence that a conversation is finished, and one of the reasons is that a provider is still running in that terminal.

Nothing asks you to choose.

Two keys end your time in a session, and the difference between them is the whole point.

Ctrl-Q leaves. The work keeps running and you land back at the picker. The session's number brings you back to the same live conversation later. It is the same outcome as closing the terminal window without typing anything, without having to close a window to get it. Ctrl-Q works in every session, whichever provider is running, because the session manager handles it before the provider ever sees the key — including in a window where the provider has stopped answering the keyboard at all.

Ctrl-D closes. The provider exits, the session ends, its number returns to the ordinary quarantine, and the conversation lands in Closed sessions where one Restore brings it back. Codex and a shell session close on a single press. Claude Code answers the first press with Press Ctrl-D again to exit. That confirmation belongs to Claude's exit action rather than to the key, so the kit cannot configure it away, and binding the same action to a different key keeps the identical prompt.

In Claude each key has a typed twin: /kit does what Ctrl-Q does, and /exit does what Ctrl-D does, closing on the first press with nothing to confirm. Codex has neither, so in Codex it is the keys.

Session Kit puts the Ctrl-Q binding in the shpool config it writes for a fresh setup; shpool's own name for the action is detach. It is not shpool's default, stock shpool detaches on the two-key chord Ctrl-Space Ctrl-q, so if you already had a shpool config before installing Session Kit, yours was left alone and Ctrl-Q may do nothing there. Add this to ~/.config/shpool/config.toml. shpool compiles keybindings once per attachment, so it needs no daemon restart, but it reaches only windows you open or REOPEN after the edit. A window you are already sitting in keeps the binding it started with, which is why Ctrl-q can work in one of your terminals and do nothing in the one next to it:

[[keybinding]]
binding = "Ctrl-q"
action = "detach"

Why there is no /kit in Codex: in codex-cli 0.145.0 the slash namespace is built-in commands only. Its app-server protocol has no channel for a custom command, ~/.codex/prompts/ is not read at all, and a file installed as a skill is discovered but never appears under /. An older Codex did honour ~/.codex/prompts/, which is why this page used to say so. If a future Codex brings custom slash commands back, measure it before believing it, tests/test_install.py pins what was measured.

If you installed an earlier release, ~/.codex/prompts/kit.md is still on disk. Nothing reads it and nothing runs it, so it is clutter rather than a risk, and the installer deliberately does not delete it: an upgrade that removes files from a provider's own home is a worse habit than an inert leftover. Delete it yourself whenever you like.

The kit installs the Claude verb into ~/.claude/commands/kit.md AND into every enrolled Claude account profile under ~/.local/share/session-kit/accounts/, because a session launched on an account reads that account's commands and nothing else. It is kept with the release. It runs one line, shpool detach, which detaches the session named in $SHPOOL_SESSION_NAME. A provider that is not installed on the machine gets nothing, an existing kit.md that is not a plain file is left alone, and the account profile a session happens to be using is never written to.

Only a typed /kit ever leaves a session. Claude Code treats a file in commands/ as a skill it may invoke on its own when the conversation looks relevant, so the verb carries disable-model-invocation: true: the description is not placed in the model's context at all, and nothing but the typed command can fire it. Without that, typing the bare word kit as an ordinary message was enough to empty a terminal mid-turn.

Do not use a generic latest-conversation command to come back. If exact identity cannot be proved, reopen is disabled rather than guessed.

From an ordinary managed shell, keep_session disables automatic cleanup, unkeep_session allows it to resume once all safety conditions are met, and bye closes the session immediately, no question, and recoverable from Closed sessions like any other close. A directly typed Bash exit keeps its normal shell behavior.

Close a session

In the picker, type the numbers you want, press Enter, and choose Close. From a shell:

sp close <session-number|shpool-id>

Before closing, Session Kit refreshes live evidence and resolves every number to one exact shpool ID. Cached inventory, changed generations, hidden numbers, or ambiguous identity disables the whole action rather than part of it. Nothing is confirmed first, and the line afterwards says what happened: Closed 3 sessions.

Every deliberate close is written to a ledger, provider, conversation, title, directory, and the time, and the conversation is listed under Closed sessions whenever it is not already open. The retained row is hidden while the exact conversation is live and reappears if that live session is later lost. That is what makes a confirmation unnecessary: a close you did not mean is one Restore away.

Closing ends the session. It does not delete provider-native conversation history or optional journal files.

Change the model

sp change-model <session-number|shpool-id> <model-id>

The exact conversation comes back on the model you named, through the same launch path sp new --model uses. In the picker it is the Change model action, which offers the models you have configured: one provider<TAB>model line each in ~/.config/session-kit/models.tsv. Session Kit ships no model catalog and never guesses a model name. The picker shows only identifiers the same provider-specific launch gate accepts (claude-... for Claude; gpt-..., o3..., o4..., or codex-... for Codex), so every offered choice can reach the model-change safety checks.

The model you chose is the model you get

Session Kit never changes a model by itself, and says so before a session starts rather than after it has run on something else. Both sp new --model and sp change-model ask one more question before anything launches: does this machine really serve that model? Two local answers exist, and the first that applies wins:

  1. What was served last time. When something reads the model a session is really on, it records the pair with session-kit's model-served verb. A recorded answer that differs from the request is a silent downgrade, and the next launch refuses it by name.
  2. Your models.tsv. A model that is not on this machine's list is refused, and the refusal prints the list.

With neither, the answer is unknown, said as unknown, never as verified, and the session starts exactly as asked.

A refusal names the model you asked for and what would serve it instead, and nothing starts. Nothing is ever substituted: --model <what-serves-it> takes the other model, and --model-anyway repeats the request as it was.

sp new claude myproject --model claude-fable-5
You asked for claude-fable-5.
The last session that asked for it actually ran on claude-haiku-4-5 instead.
Start it on claude-haiku-4-5 with --model claude-haiku-4-5, or repeat the
command with --model-anyway to ask for claude-fable-5 anyway.
session-kit: no session was created

A session that is already running is never restarted by this check, it applies to a launch and to a model change, not to recovery.

Isolated workers

Delegated work gets its own copy of the code. A machine session, one started with --origin machine, or with SESSION_KIT_ORIGIN=machine in the environment, is given a git worktree of its own without asking, on a branch the kit cuts for it, so two workers on one repository never edit the same files. A directory that is not a git repository has nothing to isolate: the session runs in it directly and says so. An automation that means the checkout itself says --no-worktree.

A person's own session is never moved out of the directory they chose. A worktree is theirs to ask for, by branch name:

sp new claude --worktree <branch>
sp new codex myproject --worktree <branch>

The branch is materialized as a worktree under the state directory (worktrees/trees/<project>-<8 hex>/<branch>, with any / in the branch name written as -; the hex tells two projects of the same name apart, and sp worktree list prints the exact path). The session starts there. The call is idempotent per repository and branch: starting a second session on the same branch reuses the same directory rather than forking a second one. A branch already checked out somewhere else, or a project that is not a git repository, refuses the launch, no session is created.

Rows for isolated sessions carry their branch in the picker and in sp list, and sp detail shows it as Worktree.

sp worktree list
sp worktree materialize --repo <directory> --branch <branch>
sp worktree teardown --branch <branch> [--merged-into <ref>] [--force]
sp worktree sweep [--active <shpool-id>]... | --none-alive | --active-stdin

Closing the session gives the copy back. Every close does it, sp close, the picker's k, bye or a clean provider exit, and the reaper's scheduled pass, so nobody has to remember a teardown.

git status is not the question. Before a copy goes, everything work can be is checked, and any one of these keeps it, with the reason said out loud:

  • uncommitted, staged or untracked files;
  • files the repository ignores, a log, a screenshot, a report, a .env, a one-off script. A fresh copy starts with none, so every one of them was made by whoever worked there;
  • the commit the copy is actually standing on not being in the reference (HEAD by default), its real HEAD, so a commit made on a detached HEAD, which lives nowhere but that copy, is never removed with it;
  • a stash made from it;
  • a merge, rebase, cherry-pick, revert or bisect left half-finished;
  • a submodule with anything of its own in it;
  • anybody still working in the directory, named by pid;
  • the checkout there not being the one recorded, a directory made by hand where a copy used to be is somebody else's;
  • a copy the kit did not cut for that session: a worktree asked for by branch name is the operator's, and so is every worktree that was on the machine before this shipped. Those are removed by sp teardown and nothing else;
  • a question git could not answer. Every check above exists to prove the work is safe, so one that could not run has not passed. An unreadable HEAD, a git stash list that failed, a repository that has gone missing, a comparison that errored, a process table that could not be read: each is named and each keeps the copy.

--force covers uncommitted, staged, untracked and ignored files, a stash, a submodule, an unmerged branch, and a check that could not run. It does not cover:

  • a commit on no branch that is not proved to be in the reference, the copy's own reflog is the only thing that reaches it, and removing the directory takes that reflog with it;
  • a half-finished merge, rebase, cherry-pick, revert or bisect, the replay state lives only in the copy. Finish it or undo it there first; the refusal names the command;
  • a checkout that is not the recorded one;
  • a repository the kit may not write into (below);
  • a live session standing in the directory.

A copy kept because a worker was still writing is not kept forever: the reaper runs the sweep against the live session list on every scheduled pass, including the ones with no session to close, which are most of them. Run it yourself with sp worktree sweep --active <shpool-id>…; with no --active and no --none-alive it refuses, because a sweep that is told nothing must never read that as "nothing is alive". --active-stdin is the machine form: it reads a shpool list --json payload and refuses outright on anything it cannot fully account for, so a reader that broke can never look like an idle machine. A copy whose session was never recorded is reported and left alone however old it is, age is not ownership.

A checkout that is the running thing is never copied. Some directories are served live, and a copy of one is work that never takes effect, the session edits the copy, the site keeps serving the original, and the check that says "done" reads the old page.

Three ways a repository is recognised as one of those, and the first needs nobody to have configured anything:

  • it is under a system root, /srv, /var, /opt, /usr, /etc, /root, /run, /bin, /sbin, /lib*, /boot, /dev, /proc, /sys, or / itself. That is where a machine keeps the software it runs. This is on by default, deliberately: a guarantee that has to be switched on is not one;
  • it carries a .session-kit-shared file at its root;
  • the host lists it in SESSION_KIT_SHARED_REPOS (colon-separated absolute paths).

SESSION_KIT_COPYABLE_REPOS (same format) is the way to say a particular directory is yours to copy, and it overrules all three.

A delegated session in one of these runs in the checkout itself and says why. Nothing is written into such a repository, no branch, no worktree administrative directory, no removal, and no session ever edits a copy of it by accident. The same rule applies to a linked worktree of one: a worktree resolves to the repository it belongs to, so it is covered by whatever covers that repository.

No repository-wide prune, ever. Giving a copy back unregisters exactly the one entry that names that directory. git worktree prune is repository-wide and drops the administrative directory, index, HEAD, reflog, half-finished rebase, of every worktree of that repository it cannot reach at that moment, including one on a volume that simply is not mounted right now.

When you want the close and the prune in one verb:

sp teardown <session-number|shpool-id> [--merged-into <ref>] [--force]

--force removes a directory that is dirty or unmerged anyway. It does not override a live session standing in it, and neither form ever deletes the branch, the commits outlive the worktree.

Automatic cleanup

This is a BACKSTOP, not the ordinary route. A session whose provider exits closes itself at once (see "When a provider exits" above), so a terminal that reaches the 72-hour window is one the shell could not close: a session started before this release landed, a close that failed, or a session the manager still lists after its shell is gone. Two shapes are deliberately outside it and stay visible until you close them from the picker, a terminal with no provider-exit record, and one with no exact conversation, because a row whose state cannot be proven is never cleaned up quietly.

The scheduled observer considers only disconnected provider-exited terminals, and automatic close begins only after the cleanup timer is enabled. A candidate then needs 72 continuous hours with every exact safety predicate unchanged.

Attachment, a live provider, child work, a recovery conflict, an identity change, missing evidence, or a new terminal generation resets or blocks eligibility.

sp prune runs fresh checks for a manual cleanup.

History and recovery

sp history <session-number|shpool-id>
sp find <text>
sp recover
sp recover --allow-large-ledger
sp restore --allow-large-ledger <number>

History is the readable record of a session, and it is an action on every row in the picker. Where a recording exists it is replayed as settled text in a pager; a session with no recording, recording is off unless you turned it on, and a session started without it can never be recorded after the fact, falls back to the conversation transcript and says so on the line above the text. Claude and Codex transcripts use the same layout for operator messages, model messages, tool calls, and tool output.

sp find searches stored recordings case-insensitively. Each recording keeps its own result group and example-line allowance. A current or recoverable session uses its recorded name; older recordings without trustworthy title metadata use their start date and first readable request instead. Storage paths and internal session identifiers are never printed. When recordings have the same display name, a recording ordinal keeps their result headers distinct. With no result it prints Matches: none. A damaged recording is skipped with a one-line notice while the rest of the search continues.

Clean recordings are prepared incrementally in the background. If the clean copy trails its capture by more than the two-second live-write grace period, or an on-demand refresh fails, sp history says that the last readable version is being shown. If no clean version can be produced, it says that the recording as captured is being shown. These are successful recall notices, not errors; redirected stdout still contains only the recalled text, with the notice on stderr.

Restoring a closed conversation is the Closed sessions row in the picker, or sp restore at the command line. Both read one list, so they show the same conversations in the same order under the same names, and a row is acted on by what that row shows: its session number, the one it has on every other screen , or, when its number has been retired, the name beside it. Two rows that answer to one name (everything nobody named reads "unnamed") each show the time of their own event where the number would be, and that is what you type. A word belongs to one row on the whole list or to none of it: a conversation somebody named 2 does not answer to 2 while session 2 exists, and shows the time of its own event instead.

A name and an event time belong to the list rather than to the conversation, so they do move to another row when the list is rebuilt, that is what they are for, while a name is shared. What cannot happen is one moving between the screen you read and the restore you type. Each screen writes down what it printed beside each row; sp restore checks the word you hand it against that and refuses in a sentence, restoring nothing, if it now means a different conversation. The picker draws and acts out of one list, so a word typed there always answers for the row in front of you. If a screen cannot be written down it says so, because the check is missing rather than passing.

A restore still resolves an exact Claude or Codex conversation identity and never falls back to recency, directory, or "latest". One conversation cannot be open in two writable sessions at once: open the session that has it, or fork it deliberately. A live conversation is never offered. A shell session cannot be resumed at all, its history is all that remains of it, and neither can a conversation whose transcript this machine can no longer read; both are listed, and both say so in a sentence rather than offering a restore that cannot work.

Normal refreshes stream and completely validate the durable Closed sessions ledger. A pathological file above the normal safety ceiling is never shown as an empty or partial list: recovery exits non-zero, names the exact size and ceiling, and prints the command above. The explicit large-ledger form applies the same complete row validation without the unattended-refresh ceiling; its footer gives the matching restore command for the numbers it prints.

Repair

sp repair <session-number|shpool-id>

Repair is reserved for direct evidence of an unrecoverable shpool handoff failure. Silence alone does not qualify.

The installed watchdog reports evidence and does not repair in its default mode. Its advanced repair mode is a separate Linux-only opt-in that carries terminal-state risk. The macOS watchdog is report-only.

If every session becomes unreachable at once rather than one, that is a different problem and repair will not help. See Troubleshoot.

Automation

Noninteractive close requires an explicit opt-in and the exact shpool ID:

SESSION_KIT_NONINTERACTIVE=1 \
SESSION_KIT_CONFIRM_ID=<exact-shpool-id> \
sp close <exact-shpool-id>

Never automate moves, kills, repairs, or cleanup using display numbers. They are stable within a boot, but they describe what is on screen, not which conversation you mean.