Skip to main content

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.

What xa is for, now that Remote Control exists

Claude Code's own Remote Control does the "start a session and pick it up on my phone" job natively — server mode serves up to 32 sessions from one process, the claude.ai session list shows them across machines, and mobile push tells you when one needs you. Use it. Set "remoteControlAtStartup": true and xa gets out of the way.

xa is for the four things it doesn't do:

  1. Launch in an arbitrary directory, from your phone. A claude remote-control server is rooted at one directory (--spawn worktree means worktrees of that same repo). xa spawn <any-cwd> — and the web UI's folder picker — is how you start a session in any one of a few hundred projects without being at the machine.
  2. See sessions that were never remote-controlled. claude.ai lists only live Remote Control sessions. xa list reads the whole transcript tree, and xa history --search greps every prompt you have ever typed, across projects and machines.
  3. Find out what happened to a session that died. xa archive forensics gives a death reason and the last tool call. Nothing upstream keeps one.
  4. Reconnect a session whose Remote Control dropped. After a network change Claude Code retries, gives up, and leaves the session running but unreachable. xa revive finds those and reconnects them — carefully; see xa revive --help.

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/ssh if needed);
  • generate a strong password + captcha key (via xa gen-secret);
  • write a secure ~/.config/xa/config.toml and 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 every pip install xa gets 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.

# 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

  1. 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-ca or plain openssl; export per-device .p12 bundles to install on each device. Once you have this, you can run xa serve --i-know-its-insecure (Basic auth becomes redundant — the proxy already authenticated the client) or keep Basic as a second factor.

  2. Obscure the hostname. Don't call it xa.example.com. Random subdomain + private DNS is cheap friction for bots.

  3. Systemd hardening. Run as a non-privileged user with ProtectSystem=strict, NoNewPrivileges=true, and a tight ReadWritePaths= list.

  4. Firewall. Bind only to loopback; let the reverse proxy be the only thing on a public interface.

  5. Rotate credentials. xa gen-secret is 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.


Reviving dropped Remote Control sessions

Remote Control gives up on its own. A network change — moving between networks, toggling a VPN — produces a transient 403, Claude Code retries for a few minutes and then stops, and the session keeps running locally while becoming unreachable from claude.ai. xa revive finds those and reconnects them.

$ xa revive
reconnectable  work:@1.%1     would send /remote-control  [remote control disconnected]
held_elsewhere phone:@1.%1    not actionable              [ended or archived from another device]
connected      build:@2.%1    [/rc]

(dry run — re-run with --apply to send)
xa revive --apply                      # send /remote-control to the dropped ones
xa revive --include-held-elsewhere     # …also take back ones held on another device
xa revive --server-mode                # …also restart died `claude remote-control` servers
xa revive --json-out                   # machine-readable, one record per pane

It only reaches tmux-hosted sessions. /remote-control is a built-in slash command with no CLI equivalent, so reconnecting means typing into the pane — a session running in a plain terminal tab has to be reconnected by hand.

Four refusals are deliberate, and each cost something to learn:

It will not Because
reconnect a session held on another device it would take the session back from your phone mid-sentence
type into a pane with unsent text in its prompt send-keys appends, so it would submit your half-typed instruction with /remote-control glued to the end
touch a busy or API-stalled session the fix there is resending a prompt, which costs money and can duplicate work
touch the same pane twice in ten minutes so a timer, a hook and an impatient human cannot add up to a keystroke storm

Detection reads the footer's /rc pill, which is present while Remote Control is connected and gone when it is not, and cross-checks it against bridgeSessionId in the session's own state file. Both were verified against a live session rather than inferred — getting the pill backwards would send /remote-control at healthy sessions, which opens a modal menu inside them.

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

Metadata

Release files for xa 0.1.12

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xa 0.1.12
File Size Uploaded
xa-0.1.12.tar.gz 146.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for xa 0.1.12
File Interpreter ABI Platform
xa-0.1.12-py3-none-any.whl Python 3 none any Details

Total release size: 249.6 kB

Release files / xa-0.1.12.tar.gz

Download URL xa-0.1.12.tar.gz
Size 146.4 kB
Tags Source
SHA-256 checksum
How to use checksums
b86db559c2924464e379943689504b06e73ecd352c8e533d9d093f0a610102b7
BLAKE2b-256 checksum
How to use checksums
f7b02ce2b6ec1cc24cd9358aeb0b95af47da9652d519faf0e16c44f6c08858c3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}

Release files / xa-0.1.12-py3-none-any.whl

Download URL xa-0.1.12-py3-none-any.whl
Size 103.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fe6d8efb31d7d7e048337616451853be4d96d4bd07c4666549fe02ac82a603f0
BLAKE2b-256 checksum
How to use checksums
f91430ad63495ef64b8f0ffdac73ed7e586f35ca0d0c606b37ac7468e4ca3a49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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}

Release history Release notifications | RSS feed

This release

0.1.12 This release

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page