<!-- generated by epythet -->

# 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](https://code.claude.com/docs/en/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.

```bash
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.

### The recommended recipe

```bash
# 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:

```caddy
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:
   ```caddy
   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

```ini
# /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
```

```bash
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

```console
$ 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.

```console
$ 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)
```

```bash
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:

```console
$ 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:

```bash
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:

```bash
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`.

```toml
[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:

```python
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:

```python
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`](https://github.com/chronologos/cc-sessions)   | `claude --resume`   | [`claude-history-viewer`](https://marketplace.visualstudio.com/items?itemName=agsoft.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

```default
 ┌──────────── 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`]().

```bash
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

<p class="epythet-aggregates">This documentation as a single file: <a href="xa.md">xa.md</a> (Markdown, for agents).</p>
