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 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 via
claude.ai/code/… remote control). Works fine on a laptop too.
Easiest path — let Claude Code set it up
xa ships with an agent skill that teaches any Claude Code instance how to
install, configure, and secure-by-default-set-up the package on your machine.
After pip install xa, a Claude Code session running on that machine can
guide you through the rest with zero docs-reading.
pip install 'xa[service]' # service extra bundles the HTTP server
Then, in a Claude Code session on the target machine:
"Set up xa on this server so I can manage Claude Code sessions from my phone."
The xa-install skill will:
- verify system prerequisites (
tmux,claude,rsync/sshif needed); - generate a strong password + captcha key (via
xa gen-secret); - write a secure
~/.config/xa/config.tomland a systemd unit; - refuse to expose the HTTP service publicly without auth + TLS guidance;
- run a health-check and confirm everything is wired up.
Two companion skills live in the repo:
xa-install— the consumer-facing setup skill, shipped inside the wheel so everypip install xagets it.xa-dev— for collaborators hacking on the codebase (architecture, test conventions, known pitfalls).
Running xa as a phone-accessible server
This is the highest-leverage use case. A headless server runs
xa serve, you open https://your-server/api/xa/sessions/{id} (or a small
web UI) on your phone, and you can list / spawn / resume / kill Claude Code
sessions from anywhere. Each session exposes a claude.ai/code/… URL that
resumes the agent right in your phone's browser.
⚠️ Security first — this is a code-execution endpoint
A live xa session runs claude, which can invoke arbitrary shell
commands via its Bash tool. Anyone who reaches your xa server with
valid credentials can execute code as the server user. Treat the server
like you would an SSH endpoint — only more carefully, because the
attack surface is a web request.
xa is secure by default. When running on a non-loopback interface,
it refuses to start without credentials. You have to pass
--i-know-its-insecure to bypass the check.
The recommended recipe
# 1. Install (on the server).
pip install 'xa[service]'
# 2. Generate secrets. Don't copy passwords from a tutorial — use these.
export XA_PASSWORD=$(xa gen-secret)
export XA_CAPTCHA_KEY=$(xa gen-secret)
# 3. Start xa, bound locally, captcha-gated.
xa serve \
--host 127.0.0.1 --port 8010 \
--username $(whoami) --password-env XA_PASSWORD \
--captcha
Then put a TLS-terminating reverse proxy in front of port 8010. Caddy, nginx, Traefik, Coolify — any of them. Without TLS, HTTP Basic sends your password in plaintext on every request.
A minimal Caddy block:
xa.example.com {
reverse_proxy 127.0.0.1:8010
}
Defense in depth — things to add
-
mTLS client certs. Issue one per device (phone, laptop). Caddy's
client_auth { mode require_and_verify }rejects any connection without a valid cert. Removes the "shared password" problem entirely. Sketch:xa.example.com { tls { client_auth { mode require_and_verify trusted_ca_cert_file /etc/caddy/xa_ca.pem } } reverse_proxy 127.0.0.1:8010 }Generate a CA with
step-caor plainopenssl; export per-device.p12bundles to install on each device. Once you have this, you can runxa serve --i-know-its-insecure(Basic auth becomes redundant — the proxy already authenticated the client) or keep Basic as a second factor. -
Obscure the hostname. Don't call it
xa.example.com. Random subdomain + private DNS is cheap friction for bots. -
Systemd hardening. Run as a non-privileged user with
ProtectSystem=strict,NoNewPrivileges=true, and a tightReadWritePaths=list. -
Firewall. Bind only to loopback; let the reverse proxy be the only thing on a public interface.
-
Rotate credentials.
xa gen-secretis a one-liner; rotate monthly and restart the service.
Systemd unit
# /etc/systemd/system/xa.service
[Unit]
Description=xa Claude Code session server
After=network.target
[Service]
Type=simple
User=xa
WorkingDirectory=/home/xa
Environment="XA_PASSWORD=<paste the output of `xa gen-secret`>"
Environment="XA_CAPTCHA_KEY=<paste a second `xa gen-secret`>"
ExecStart=/path/to/venv/bin/xa serve \
--host 127.0.0.1 --port 8010 \
--username xa --password-env XA_PASSWORD \
--captcha
Restart=on-failure
# Hardening — uncomment and tune if your setup allows:
# ProtectSystem=strict
# ProtectHome=true
# NoNewPrivileges=true
# PrivateTmp=true
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now xa
curl -u xa:$XA_PASSWORD http://127.0.0.1:8010/health
What happens if you try to skip the guardrails
$ xa serve --host 0.0.0.0 --port 8010
error: refusing to bind to a non-loopback interface ('0.0.0.0') without
--username/--password.
Anyone who can reach this port could spawn arbitrary shell
commands via Claude Code sessions on this host.
Fix (recommended): add credentials + captcha.
xa gen-secret # use for password + XA_CAPTCHA_KEY
…
Bypass (only when an outer layer already gates access,
e.g. mTLS in the reverse proxy): --i-know-its-insecure
The flag exists, but it's named so the error is honest.
The rest of the CLI
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:
xa info 7b875f44 # full record + forensics
xa archive forensics 7b875f44 # what was it doing when it last ran a tool?
xa history --search "bridge URL" # cross-project prompt grep
Or walk through them interactively:
xa pick
Commands
| Command | Purpose |
|---|---|
xa list |
Table of sessions across configured hosts |
xa info <id> |
One session — metadata + forensics |
xa history [--search X] |
Grep ~/.claude/history.jsonl |
xa pick |
Interactive list → number → action |
xa spawn <cwd> |
Start claude in tmux, print phone URL |
xa resume <id> |
claude --resume in a new tmux pane |
xa kill <id> |
End a live session |
xa archive list |
Postmortem of every session xa spawned |
xa archive log <id> |
Raw pane log |
xa archive forensics <id> |
Death reason + last tool use |
xa sync [--host H] |
Refresh SSH-host caches |
xa serve … |
Run the HTTP service (secure-by-default) |
xa gen-secret |
Cryptographically strong random hex string |
Every command supports --help; every listing supports --json-out.
Configuration
Config lives at ~/.config/xa/config.toml (respects $XDG_CONFIG_HOME;
override with $XA_CONFIG). Missing file → implicit LocalHost.
[settings]
cache_dir = "~/.cache/xa/remotes"
stale_threshold_sec = 3600
claude_bin = "claude"
tmux_bin = "tmux"
[hosts.local]
kind = "local"
# Another machine via SSH. Uses your ~/.ssh/config.
[hosts.devbox]
kind = "ssh"
host = "devbox"
user = "deploy"
remote_claude_home = "~/.claude"
# Another xa server over HTTPS.
[hosts.phone_server]
kind = "http"
base_url = "https://xa.example.com"
auth = "basic"
username = "me"
password_env = "XA_PHONE_PW" # secret stays in env, not TOML
After editing, xa sync pulls SSH hosts into the local cache.
Python API
Everything the CLI does is available as a 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()
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)
# 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)
For embedding the HTTP service in a larger app:
from xa.service import build_api, make_basic_auth, Captcha
import os
api = build_api(
auth=make_basic_auth("me", os.environ["XA_PASSWORD"]),
captcha=Captcha(key=os.environ["XA_CAPTCHA_KEY"]),
)
outer_app.mount("/api/xa", api)
HTTP API surface
| Method | Path | Purpose |
|---|---|---|
GET |
/sessions |
List (query: project, state, include_forks, limit) |
POST |
/sessions |
Create a tmux+claude session; up to 120 s for the bridge URL |
DELETE |
/sessions/{name} |
Kill (captcha-gated when --captcha) |
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 (?tail_kb=) |
GET |
/captcha |
4-letter challenge (only with --captcha) |
GET |
/health |
Liveness |
How it compares
| Feature | xa |
cc-sessions |
claude --resume |
claude-history-viewer |
|---|---|---|---|---|
| Cross-project discovery | ✓ | ✓ | per-project | ✓ |
| Cross-machine discovery | ✓ (SSH + HTTP) | ✓ (SSH) | ✗ | ✗ |
| Live tmux / spawn / kill | ✓ | ✗ | ✗ | ✗ |
| Bridge URL resolution (phone) | ✓ | ✗ | N/A | ✗ |
| Postmortem / death reason | ✓ | ✗ | ✗ | ✗ |
| HTTP API surface | ✓ | ✗ | ✗ | ✗ |
| Python library | ✓ | ✗ | — | ✗ |
| Language | Python | Rust | — | TypeScript (VS Code) |
Short version: cc-sessions pioneered cross-machine listing; the VS Code
extension pioneered transcript search with a UI; xa unifies those with
live CRUD + bridge URL resolution + postmortem, and exposes all of it as
both a CLI and a Python library you can embed.
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 / API / phone URL │
└────────────────────────────────────────┘
Development
Layered architecture in misc/docs/design.md;
phased plan in misc/docs/roadmap.md; research
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 # ~100 tests, ~8 seconds
XA_RUN_INTEGRATION=1 pytest -q # also runs the real-claude spawn test
The xa-dev skill is picked up
automatically when you're working on this repo (architecture overview,
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.4.tar.gz.
File metadata
- Download URL: xa-0.1.4.tar.gz
- Upload date:
- Size: 104.3 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 |
e7dc90df336882e77e08c0af1d0fdd5a0a2d756eb0428f087701679bb2aaeacd
|
|
| MD5 |
4945fed98a30d45a57977977a1a92346
|
|
| BLAKE2b-256 |
2641b5ed29d13c0fb5f31563553adfa1cf0f6ac3e604c8227758880b7e39cf80
|
File details
Details for the file xa-0.1.4-py3-none-any.whl.
File metadata
- Download URL: xa-0.1.4-py3-none-any.whl
- Upload date:
- Size: 74.6 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 |
e8405585313bfc1728fd086a5a67ec7ad0eca65276602c112b25651527fbf349
|
|
| MD5 |
8e7ad364810a2b70afd8720a97815f2d
|
|
| BLAKE2b-256 |
37bc01c4a73b66f55e21094092a2bb96a0519e5bff2edad1508ad7a84bed998d
|