One AuthTool per supported CLI. An adapter knows where a tool keeps its
login on disk and how to capture/apply/inspect it. Secrets live only inside
Snapshot (as Secret); Account is the redacted, serializable identity.
All credential writes go through here: 0600 at creation, temp in the dest’s
own directory, atomic same-fs rename, symlink/ownership refusal. A write that
cannot be done atomically fails loudly rather than falling back to a
non-atomic (and mode-racy) copy.
The ASCII wordmark the CLI prints when run with no subcommand. It is the
same art shown in the README banner, so the banner is literally what you see
in your own terminal. ANSI colour is used only when stdout is a TTY and
NO_COLOR is unset, so a piped invocation stays plain text.
The subcommand handlers. Each returns an exit code; a hard error propagates
and main prints a redacted message + exits 1. Output is identity-based and
never prints a credential byte (the A11 egress guarantee) - the only reader
of a Secret is inside the adapters/store.
A read-only stdio MCP server: an agent can SEE which account is active but
can NEVER switch it. Hand-rolled newline-delimited JSON-RPC 2.0 (mirrors the
sessionwiki MCP). Two tools, both readOnlyHint: whoami, list_accounts.
Field allowlist + secret-free errors (A13); no switch/add/use tool exists.
Recent sessions read STRAIGHT from each tool’s own on-disk store - no
sessionwiki required. Claude Code: ~/.claude/projects/**/<uuid>.jsonl
(line objects carry cwd and the user messages). Codex:
~/.codex/sessions/**/rollout-<ts>-<uuid>.jsonl. Resume uses each tool’s
native mechanism (claude --resume <id> in the session’s cwd,
codex resume <id>), the same semantics sessionwiki uses.
One override-first path resolver per tool. Every canonical credential path
goes through here so tests can redirect to a temp tree and never touch a
real login. Precedence: explicit root (tests) > tool env var > home dir.
Best-effort detection of a running claude / codex process, used to warn
before a switch that could disrupt a live session (a running session holds
the old token and can overwrite the just-switched login on its next refresh).
Proxy mode: a loopback HTTP server that forwards Claude Code’s API traffic
upstream, choosing the account per request so a running conversation can
change accounts. Credentials are read from slots and never copied, and
neither prompt content nor any token value is ever logged.
Credential bytes wrapper: never prints, zeroizes on drop. The ONLY way to
read the bytes is expose(), which is easy to grep for in review. No
#[derive(Debug)] in the credential path relies on this type.
Attribute sessions to the account that was active when they ran, by joining
the switch timeline with session start times. Attribution is best-effort:
a session with no prior switch event is unattributed (a first-class
bucket), and a missing/older sessionwiki degrades gracefully (A14).
Persisted preferences: <store_dir>/settings.json. Deliberately tiny - one
flat file, every field optional, an unreadable or half-written file falling
back to defaults rather than failing a command. Nothing here is a credential.
The claude shim: a tiny launcher placed on the user’s PATH (ahead of the
real claude) that reads swapdex’s default-account pointer and runs the real
claude in that account’s slot. This is what makes a plain claude follow
swapdex use. No credential is ever moved - the shim only sets
CLAUDE_CONFIG_DIR.
The permanent-slot registry: a name -> slot mapping persisted to
<store_dir>/slots.json. Each slot is a directory under
<store_dir>/slots/<id>/ handed to the tool as its own home - Claude’s
CLAUDE_CONFIG_DIR, Codex’s CODEX_HOME. swapdex never writes a credential
into a slot; the tool’s own login does. The id is opaque and
name-independent so a rename never changes the directory (and therefore never
changes the Keychain service, which is derived from the dir string).
The profile store at ~/.local/share/swapdex: named snapshots, a switch
timeline, an active-name hint, a cross-process lock, and bounded backups.
Everything is 0600, the store dir 0700; it holds plaintext refresh tokens and
is single-machine, single-user - never sync it.
The persistent full-screen UI (swapdex ui on a real terminal), ccusage-
style by user request: the screen clears, the UI stays up, and everything
happens inside it. Switching shows its result in the status line and
REFRESHES the list in place; landing in a conversation (resume or new) is
the one action that leaves - by design, that is the goal of a switch.
Local, no-network usage read. Sums tokens from each tool’s on-disk session
transcripts over recent windows (5h / 7d) so you can tell how heavily you
have been using an account and when to switch. Reads ~/.claude/projects
and ~/.codex/sessions only - never the network.