Tools for managing remote Claude Code sessions across projects and machines
Project description
xa
Manage Claude Code sessions across projects and machines.
Claude Code's built-in /resume picker is per-project, per-machine — sessions
on your laptop, your Mac, your server, and your phone all live in separate
silos. xa treats sessions as first-class records you can list, search,
spawn, resume, and kill across every machine you use, from one CLI or one
HTTPS endpoint.
Works great on a headless server (tmux-hosted claude, phone URL for
remote control via claude.ai/code/...). Works fine on a laptop too.
Quick install
pip install xa # library + CLI + SSH transport
pip install 'xa[service]' # also HTTP service (FastAPI + uvicorn)
Runtime requirements (install via your package manager):
| Tool | Why |
|---|---|
tmux (≥3.0) |
backs every live Claude Code session as a detached pane |
claude |
Anthropic's Claude Code CLI — xa spawns and resumes it |
rsync + ssh |
only for SSHHost (pull remote session trees) |
xa checks for these at runtime and reports a clear error if they're missing.
Quickstart
Already have some Claude Code sessions on this machine? List them:
$ xa list
ID HOST STATE MOD TURNS CWD URL / FIRST_MSG
-------- ----- --------------- ------- ----- ----------------------- ----------------------------------------------------
7b875f44 local live 2s ago 494 /root https://claude.ai/code/session_01ALoSc...
b3366c60 local live 20h ago 725 /root/py/proj https://claude.ai/code/session_012W14x...
4e738fb5 local transcript_only 18h ago 15 /root my apps.thorwhalen.com has an admin level app…
Dig into one of them:
xa info 7b875f44
xa archive forensics 7b875f44 # what was it doing when it last ran a tool?
xa history --search "bridge URL" # cross-project prompt search
Or walk through them interactively:
xa pick
Core CLI commands
Sessions
| Command | What it does |
|---|---|
xa list [--host H] [--project P] [--state S] [--limit N] |
Table of sessions across configured hosts |
xa info <id> |
Full record + transcript forensics for one session |
xa history [--search PHRASE] |
Grep ~/.claude/history.jsonl |
xa pick |
Interactive picker (list → number → action) |
xa spawn <cwd> |
Start claude in tmux at cwd, print phone URL |
xa resume <id> |
claude --resume <id> in a new tmux pane |
xa kill <id-or-tmux-name> |
End a live session |
Archive
| Command | What it does |
|---|---|
xa archive list |
Postmortem of every session xa spawned |
xa archive log <id> |
Raw pane log of an archived session |
xa archive forensics <id> |
Death reason + last tool use + transcript tail |
Multi-host
| Command | What it does |
|---|---|
xa sync [--host H] [--force] |
Refresh SSH-host caches (rsync pull) |
xa serve [--host H] [--port P] [--username U --password P] [--captcha] |
Run the HTTP service (requires xa[service]) |
Every command supports --help for full flags; every listing supports
--json-out for scripting.
Configuration
xa reads ~/.config/xa/config.toml (respects $XDG_CONFIG_HOME, override
with $XA_CONFIG). Missing file → single LocalHost is used.
[settings]
cache_dir = "~/.cache/xa/remotes"
stale_threshold_sec = 3600
claude_bin = "claude"
tmux_bin = "tmux"
# Local is always there, but you can spell it out:
[hosts.local]
kind = "local"
# Another machine via SSH. Uses your ~/.ssh/config → agent/keys.
[hosts.devbox]
kind = "ssh"
host = "devbox" # SSH alias OR raw hostname
user = "deploy" # optional
remote_claude_home = "~/.claude"
# Another xa server over HTTPS.
[hosts.phone]
kind = "http"
base_url = "https://apps.example.com/api/xa"
auth = "basic" # or "bearer" or omit
username = "me"
password_env = "XA_PHONE_PW" # resolved from env — keeps secrets out of TOML
After editing, xa sync pulls SSH hosts into the local cache.
Python API
Everything the CLI does is also available as a Python library:
import xa
# List all sessions on this machine.
for s in xa.list_sessions(limit=10):
print(s.id[:8], s.state, s.cwd, s.url)
# Multi-host.
hosts = xa.load_hosts() # reads ~/.config/xa/config.toml
for s in xa.list_sessions(hosts=list(hosts.values())):
print(s.host, s.id[:8], s.url)
# Spawn / resume / kill.
local = xa.LocalHost()
result = local.spawn("my-session", cwd="/tmp")
print(result.url) # https://claude.ai/code/session_…
# Read the archive.
events = xa.default_events_store()
panes = xa.default_pane_store()
for rec in xa.records(events, panes):
print(rec.id, rec.gone_reason, rec.url)
Running the HTTP service
Stand up an xa server on a headless box so your phone / laptop / another
script can reach its sessions over HTTPS:
pip install 'xa[service]'
xa serve --host 0.0.0.0 --port 8010 --username me --password "$PW" --captcha
Put it behind a reverse proxy (Caddy, nginx, Coolify/Traefik) with TLS. A
matching HTTPHost entry in another machine's config.toml makes its
sessions show up in your xa list alongside the local ones.
The service mounts:
| Method | Path | Purpose |
|---|---|---|
GET |
/sessions |
List (query: project, state, include_forks, limit) |
POST |
/sessions |
Create a tmux+claude session; wait up to 120s for the bridge URL |
DELETE |
/sessions/{name} |
Kill (optionally captcha-gated) |
GET |
/sessions/{id}/info |
Metadata + forensics + pane tail |
POST |
/sessions/{id}/resume |
claude --resume in a new pane |
GET |
/archive |
Postmortem list |
GET |
/archive/{id}/forensics |
Rich forensics |
GET |
/archive/{id}/log |
Pane log (supports ?tail_kb=) |
GET |
/captcha |
4-letter challenge (only with --captcha) |
GET |
/health |
Liveness |
For embedding in a larger FastAPI app (enlace, tw_platform, etc.), skip
xa serve and use the factory directly:
from xa.service import build_api, make_basic_auth, Captcha
api = build_api(
auth=make_basic_auth("me", os.environ["XA_PW"]),
captcha=Captcha(key=os.environ["XA_CAPTCHA_KEY"]),
)
outer_app.mount("/api/xa", api)
How session data flows
┌──────────── local machine ─────────────┐ ┌────────── remote machine ─────────┐
│ │ │ │
│ ~/.claude/projects/<slug>/*.jsonl │ │ ~/.claude/projects/<slug>/*.jsonl│
│ ~/.claude/sessions/<pid>.json │ │ ~/.claude/sessions/<pid>.json │
│ │ │ │ │ │ │
│ ▼ ▼ │ │ ▼ │
│ xa.claude_fs xa.tmux ─────┐ │ │ rsync (SSHHost) ─┐ │
│ │ │ │ │ │ or HTTP (HTTPHost)│ │
│ └────┬────┘ │ │ │ │ │
│ ▼ │ │ │ ▼ │
│ Session dataclass ◄─────┘ │ │ cached ~/.claude/ │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ xa.list_sessions ◄───────────────┼───┤ remote Sessions │
│ │ │ │ │
│ ▼ │ └───────────────────────────────────┘
│ CLI / TUI / API │
└────────────────────────────────────────┘
How it compares
| Feature | xa |
cc-sessions |
claude --resume |
edualc |
|---|---|---|---|---|
| Cross-project discovery | ✓ | ✓ | per-project | ✓ (single host) |
| Cross-machine discovery | ✓ (SSH + HTTP) | ✓ (SSH) | ✗ | ✗ |
| Live tmux / spawn / kill | ✓ | ✗ | ✗ | ✓ |
| Bridge URL resolution | ✓ | ✗ | N/A | ✓ |
| Postmortem / death reason | ✓ | ✗ | ✗ | ✓ |
| HTTP API surface | ✓ | ✗ | ✗ | ✓ |
| Language | Python | Rust | — | Python |
Short version: cc-sessions pioneered cross-machine listing; edualc pioneered
live CRUD + URL resolution + postmortem on one box; xa unifies them and
exposes both as a library so other apps (including edualc itself) can be
thin clients.
Development
Layered architecture is documented in misc/docs/design.md;
phased plan in misc/docs/roadmap.md; research notes
on Claude Code internals in misc/docs/research.md.
git clone https://github.com/thorwhalen/xa.git
cd xa
pip install -e '.[dev,service]'
pytest -q # ~98 tests, ~8 seconds
XA_RUN_INTEGRATION=1 pytest -q # also runs the real-claude spawn test
Test layout mirrors source layout: tests/test_<module>.py per module.
FastAPI tests stand up a real uvicorn server in-thread; tmux tests use real
tmux (auto-skipped if not installed). Unit tests for cross-machine transport
(SSH) monkeypatch _run so no actual SSH is required.
There's a dev-facing SKILL.md Claude Code will pick up when you're working on this repo (project layout, testing conventions, known pitfalls).
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xa-0.1.1.tar.gz.
File metadata
- Download URL: xa-0.1.1.tar.gz
- Upload date:
- Size: 76.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2652334a65512d478ded0026f72dfef69e2c1543cca01ceada6b49d2f7b2e92
|
|
| MD5 |
4b5158c011b1370dbc35fb3611cf9b17
|
|
| BLAKE2b-256 |
f4809e1aecc31e447649878df0d5a3a003c48b8b21f34c44fda91b254de72fcc
|
File details
Details for the file xa-0.1.1-py3-none-any.whl.
File metadata
- Download URL: xa-0.1.1-py3-none-any.whl
- Upload date:
- Size: 51.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ff9d1e715e632b8f16d47008c9f21dee5519223aa61ab17ba56d2ee4f4a1cee
|
|
| MD5 |
2898503caa5e594c324cf95b08462c66
|
|
| BLAKE2b-256 |
a85f4fd4a6d123ef7e8725fe10522b3bbefd452a563956aaa041f61c668aad6c
|