> built 2026-09-22 13:02 UTC from 70fba97 (main) · xa 0.1.12. Details: build_info.json

# index.html.md

<!-- 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>


# _autosummary/xa.archive.html.md

# xa.archive

Postmortem archive of Claude Code sessions spawned by `xa`.

Every session that `xa` spawns emits `created` + `url_acquired` events
into an append-only JSONL log and its tmux pane is tee’d to a per-session
file via `tmux pipe-pane`. When a session disappears from the live list,
`reconcile()` appends a `gone` event with an inferred death time and a
classified reason.

Death-reason taxonomy (most specific wins):

- `replaced`    — same tmux name, different `tmux_created_ts` (the
  original died and got taken over by a new session of the same name).
- `missing`     — no pane log exists (session predates our logging or
  the log was cleaned up).
- `interrupted` — transcript shows the user-interrupt marker AND the
  pane tail shows a clean exit. **Ambiguous**: the same marker fires on
  bridge-WebSocket resets (phone standby, reconnect), not only on human
  ESC. Don’t derive user intent from this alone.
- `oom_killed`  — last tool exited with 137 (SIGKILL) AND the pane log
  tail contains a kernel/shell OOM marker (`Killed`, `Out of memory`,
  `MemoryError`). Strongest single signal we can derive without root —
  the kernel’s “Killed” message rides through the same tty bash uses to
  print SIGKILL notices, so it ends up in the tee’d pane log.
- `tool_crash`  — pane tail shows clean exit AND the last tool use
  exited with a non-zero code (and we couldn’t promote to `oom_killed`).
- `clean_exit`  — pane tail contains `"Resume this session with:"`.
- `abrupt`      — killed / crashed / bridge-dropped with no clean-exit
  marker.

### Functions

| `append_created`(events, \*, id, name, cwd, ...)                                                    |                                                                        |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| `append_gone`(events, \*, id, name, reason[, ...])                                                  |                                                                        |
| [`append_hidden`](_autosummary/xa.archive.html.md#xa.archive.append_hidden)(events, \*, id, hidden)              | Mark an archived session as hidden (or un-hide it).                    |
| [`append_label`](_autosummary/xa.archive.html.md#xa.archive.append_label)(events, \*, id, label)                | Set or clear a user-supplied display label for a session.              |
| `append_url_acquired`(events, \*, id, name[, ...])                                                  |                                                                        |
| [`classify_death`](_autosummary/xa.archive.html.md#xa.archive.classify_death)(pane_kind, \*[, replaced, ...])     | Pick the most specific death reason from available signals.            |
| [`overlays`](_autosummary/xa.archive.html.md#xa.archive.overlays)(events)                                   | Fold `labeled` / `hidden` events into `{id: {label, hidden}}`.         |
| [`reconcile`](_autosummary/xa.archive.html.md#xa.archive.reconcile)(events, panes, live_sessions, \*[, ...]) | Emit `gone` events for archived sessions missing from `live_sessions`. |
| [`records`](_autosummary/xa.archive.html.md#xa.archive.records)(events, panes)                             | Return per-session summaries, newest-first (by creation time).         |
| [`synthesize_diagnosis`](_autosummary/xa.archive.html.md#xa.archive.synthesize_diagnosis)(\*, state[, reason, ...])     | One-paragraph plain-English hint about what likely happened.           |

### Classes

| [`ArchiveRecord`](_autosummary/xa.archive.html.md#xa.archive.ArchiveRecord)(id, name, cwd, created, url, ...)   |                                                                 |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`PaneInspection`](_autosummary/xa.archive.html.md#xa.archive.PaneInspection)(death_ts, kind, oom_markers, tail) | Read-once view of a pane log tail used by death classification. |

### *class* xa.archive.ArchiveRecord(id, name, cwd, created, url, gone, gone_detected, gone_reason, pane_log_bytes, claude_session_id, forensics, label=None, hidden=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

### *class* xa.archive.PaneInspection(death_ts, kind, oom_markers, tail)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Read-once view of a pane log tail used by death classification.

### xa.archive.append_hidden(events, , id, hidden)

Mark an archived session as hidden (or un-hide it).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.archive.append_label(events, , id, label)

Set or clear a user-supplied display label for a session.

`id` can be an archive id, a tmux session name, or a
`claude_session_id` — whatever key the caller will use to look it
up later. Empty-string or `None` label clears any prior label.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.archive.classify_death(pane_kind, , replaced=False, forensics=None, oom_markers=())

Pick the most specific death reason from available signals.

`oom_markers` is the tuple of OOM-shaped strings observed in the
pane tail (see `_OOM_PANE_MARKERS`). When the last tool exited
with 137 and at least one marker is present, we promote the verdict
to `oom_killed` — that pair is the strongest single signal we can
get without reading kernel logs (which would need root and is not
portable).

* **Return type:**
  [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'clean_exit'`, `'abrupt'`, `'interrupted'`, `'tool_crash'`, `'oom_killed'`, `'replaced'`, `'missing'`]

### xa.archive.overlays(events)

Fold `labeled` / `hidden` events into `{id: {label, hidden}}`.

Later events win. Useful when rendering live sessions too — callers
look up by whatever id they know (archive id, claude_session_id,
tmux name) and apply the overlay if present.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.archive.reconcile(events, panes, live_sessions, , claude_home=PosixPath('/home/runner/.claude'))

Emit `gone` events for archived sessions missing from `live_sessions`.

Returns the list of freshly-emitted events (handy for tests). Idempotent:
calling twice with the same live list produces no new events the
second time.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.archive.records(events, panes)

Return per-session summaries, newest-first (by creation time).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`ArchiveRecord`](_autosummary/xa.archive.html.md#xa.archive.ArchiveRecord)]

### xa.archive.synthesize_diagnosis(, state, reason=None, forensics=None, oom_markers=())

One-paragraph plain-English hint about what likely happened.

Designed to be useful to both a human reading the UI and an LLM agent
deciding whether to retry or escalate. The hint surfaces the actionable
bit (e.g., “add swap”, “re-run with –resume”) rather than just naming
the verdict — the structured fields next to it already do that.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/xa.claude_cli.html.md

# xa.claude_cli

Spawn, resume, and URL-resolve Claude Code sessions.

Ties `xa.tmux` (pane / process control), `xa.claude_fs` (read the
ephemeral `~/.claude/sessions/<pid>.json` file) and the `claude` CLI
binary together. Everything here targets the *local* machine; remote-host
dispatch lives in `xa.hosts` (Phase 6+).

Bridge URL format: `https://claude.ai/code/<bridgeSessionId>`. The
`bridgeSessionId` already starts with `session_` — do not prepend
anything.

### Functions

| [`complete_spawn`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.complete_spawn)(pending)                           | Blocking half of the spawn: wait for the URL, then report.              |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`diagnose_bridgeless`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.diagnose_bridgeless)(session_name, \*[, ...])      | Why has this session no bridge URL? Returns `(verdict, hint)`.          |
| [`find_claude_pid`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.find_claude_pid)(session_name, \*[, tmux_bin])     | Return the PID of the claude process living in the tmux session.        |
| [`new_archive_id`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.new_archive_id)()                                  | Fresh 12-char hex id used as the key for pane logs + archive events.    |
| [`pid_is_claude`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.pid_is_claude)(pid)                                | Portable positive identity check — used before ever signaling a pid.    |
| [`prepare_spawn`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.prepare_spawn)(name, \*, cwd[, resume_id, ...])    | Launch the tmux session and return without waiting for the URL.         |
| [`resolve_bridge_url`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.resolve_bridge_url)(session_name, \*[, ...])       | Return `(url, source)` for a live tmux-hosted claude session.           |
| [`resume_session`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.resume_session)(claude_session_id, \*, cwd[, ...]) | Launch `claude --resume <id>` in a new detached tmux session.           |
| [`spawn_session`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.spawn_session)(name, \*, cwd[, claude_bin, ...])   | Create a detached tmux session running `claude` in `cwd`.               |
| [`supported_cli_flags`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.supported_cli_flags)([claude_bin, ttl_sec])        | Long-form flags advertised by `<claude_bin> --help`, cached per binary. |
| [`tmux_session_dedicated_to`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.tmux_session_dedicated_to)(session_name, ...)      | Is killing this whole tmux session equivalent to killing this claude?   |
| [`wait_for_bridge_url`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.wait_for_bridge_url)(session_name, \*, ...[, ...]) | Poll the ephemeral session file until a bridge URL appears.             |

### Classes

| [`PendingSpawn`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.PendingSpawn)(name, cwd, claude_home, ...)      | A launched tmux session whose bridge-URL handshake hasn't finished.   |
|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)(name, cwd, claude_pid, ...[, ...]) | What `spawn_session` / `resume_session` return to the caller.         |

### *class* xa.claude_cli.PendingSpawn(name, cwd, claude_home, tmux_bin, deadline, warning, archive_ctx)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A launched tmux session whose bridge-URL handshake hasn’t finished.

Returned by [`prepare_spawn()`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.prepare_spawn) — the fast, synchronous part (tmux
session exists, pane log piping, `created` archive event emitted).
Pass it to [`complete_spawn()`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.complete_spawn) to run the slow part (URL wait,
prompt dismissal); services may do that from a background thread so
the caller gets an immediate acknowledgment.

### *class* xa.claude_cli.SpawnResult(name, cwd, claude_pid, claude_session_id, bridge_session_id, url, url_source, warning, attention=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What `spawn_session` / `resume_session` return to the caller.

### xa.claude_cli.complete_spawn(pending)

Blocking half of the spawn: wait for the URL, then report.

Safe to call from a background thread — all state it touches (tmux,
`~/.claude/sessions/`, the append-only events log) is shared
machine-wide, not per-process.

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)

### xa.claude_cli.diagnose_bridgeless(session_name, , claude_pid=None, tmux_bin='tmux')

Why has this session no bridge URL? Returns `(verdict, hint)`.

One pane capture, classified by [`xa.revive.classify()`](_autosummary/xa.revive.html.md#xa.revive.classify) — the single
rule engine for reading a claude pane. Only meaningful for a session
with no bridge id: a connected session’s pane renders conversation text,
which may legitimately quote `/login`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### xa.claude_cli.find_claude_pid(session_name, , tmux_bin='tmux')

Return the PID of the claude process living in the tmux session.

Walks **every** pane’s process tree (a claude can live in any window
of a multi-window session, not just the first pane). Recognizes both
the native binary (`comm == "claude"`) and npm/bun installs.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

### xa.claude_cli.new_archive_id()

Fresh 12-char hex id used as the key for pane logs + archive events.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### xa.claude_cli.pid_is_claude(pid)

Portable positive identity check — used before ever signaling a pid.

Liveness ([`xa.claude_fs.ephemeral_session_alive()`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.ephemeral_session_alive)) proves a
process exists; this proves it is actually a claude, so a stale
ephemeral file whose pid got recycled can never direct a kill at an
unrelated process. Uses `/proc` where available, `ps` otherwise.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### xa.claude_cli.prepare_spawn(name, , cwd, resume_id=None, claude_bin='claude', claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', url_timeout_sec=120.0, auto_remote_control=True, claude_name=None, model=None, effort=None, pane_log_path=None, archive_store=None, pane_store=None, archive_id=None)

Launch the tmux session and return without waiting for the URL.

Composes per-session claude flags (`--name` / `--model` /
`--effort` / `--remote-control`) when the installed binary
supports them; requested-but-unsupported flags are reported in
`PendingSpawn.warning` rather than failing the spawn.

`auto_remote_control` passes `--remote-control`, which is how xa
asks for Remote Control now — a documented CLI flag, not a keystroke
sent at a TUI. On a claude too old to advertise it, the host’s
`remoteControlAtStartup` setting is the remaining path, so the
warning says so rather than falling back to typing.

* **Return type:**
  [`PendingSpawn`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.PendingSpawn)

### xa.claude_cli.resolve_bridge_url(session_name, , claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', scrape_lines=400)

Return `(url, source)` for a live tmux-hosted claude session.

Primary path: find the claude descendant pid → read
`~/.claude/sessions/<pid>.json` → take `bridgeSessionId`.
Fallback: regex-scrape `capture-pane` output for the full URL.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'session_file'`, `'pane_capture'`]]]

### xa.claude_cli.resume_session(claude_session_id, , cwd, name=None, claude_bin='claude', claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', url_timeout_sec=120.0, auto_remote_control=True, model=None, effort=None, pane_log_path=None, archive_store=None, pane_store=None, archive_id=None)

Launch `claude --resume <id>` in a new detached tmux session.

A user-supplied `name` is also passed to `claude --name` (renaming
the resumed session’s display name); the auto-generated fallback name
is tmux-only so a resume never clobbers the session’s existing name.

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)

### xa.claude_cli.spawn_session(name, , cwd, claude_bin='claude', claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', url_timeout_sec=120.0, auto_remote_control=True, model=None, effort=None, pane_log_path=None, archive_store=None, pane_store=None, archive_id=None)

Create a detached tmux session running `claude` in `cwd`.

Waits up to `url_timeout_sec` for the bridge URL to appear, dismissing
the “trust this folder” prompt and enabling remote control (via the
`--remote-control` flag on modern claude binaries, or the TUI
handshake on older ones). The session name is also passed to
`claude --name` so the Claude-side display name matches; `model`
and `effort` become per-session `--model` / `--effort` flags.

If `archive_store` (and optionally `pane_store`) are given, emits
`created` + `url_acquired` events to the archive and pipes the pane
output to a file under `pane_store`.

Services that want an immediate return should call
[`prepare_spawn()`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.prepare_spawn) and run [`complete_spawn()`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.complete_spawn) in the background
instead — this function is simply the composition of the two.

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)

### xa.claude_cli.supported_cli_flags(claude_bin='claude', , ttl_sec=900.0)

Long-form flags advertised by `<claude_bin> --help`, cached per binary.

Lets spawn compose modern per-session flags (`--remote-control`,
`--name`, `--model`, `--effort`) while degrading gracefully on
older installs. Empty set when the binary can’t be executed — callers
then omit every optional flag, matching pre-flag behavior. Only
successful probes are cached (for `ttl_sec`); failures are retried.

* **Return type:**
  [`frozenset`](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### xa.claude_cli.tmux_session_dedicated_to(session_name, claude_pid, , tmux_bin='tmux')

Is killing this whole tmux session equivalent to killing this claude?

True iff the session consists of a single pane whose claude is
`claude_pid`. xa-spawned sessions always qualify; a claude living in
one window of a user’s multi-window workspace never does — killing
the session there would destroy unrelated panes, so callers must fall
back to signaling the pid instead.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### xa.claude_cli.wait_for_bridge_url(session_name, , claude_home, tmux_bin, deadline, poll_sec=0.5)

Poll the ephemeral session file until a bridge URL appears.

Nothing is typed into the pane. Claude Code connects Remote Control by
itself — from the `--remote-control` flag `_claude_argv()` passes,
or from the host’s `remoteControlAtStartup` setting — and records the
`bridgeSessionId` when it does. This function’s whole job is to wait
for that write.

Until 0.1.10 this drove the TUI instead: dismissing the workspace-trust
prompt with a blind Enter and sending `/remote-control` once a prompt
glyph appeared. That emulated, by scraping and typing, what the flag and
the setting do properly — and every upstream redraw was a chance for it
to type into the wrong thing. See [`xa.revive`](_autosummary/xa.revive.html.md#module-xa.revive) for the pane reading
that *is* still warranted: reconnecting a session whose Remote Control
dropped is a gap Claude Code genuinely leaves open.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'session_file'`, `'pane_capture'`]]]


# _autosummary/xa.claude_fs.html.md

# xa.claude_fs

Pure filesystem view of `~/.claude/`.

This layer reads Claude Code’s on-disk state and nothing else: no tmux, no
subprocess, no network. It is the substrate every higher layer of `xa`
reads from.

What lives under `~/.claude/` (as of Claude Code 2.x):

- `projects/<slug>/<uuid>.jsonl` — one file per session, full transcript.
  `<slug>` is the cwd with `/` replaced by `-` (leading `-` kept).
  `<uuid>` is the Claude `sessionId`.
- `sessions/<pid>.json` — per-process ephemeral metadata while a
  `claude` process is alive. Holds `pid`, `sessionId`,
  `bridgeSessionId`, `cwd`, `startedAt`, `version`. Deleted when
  the process exits.
- `history.jsonl` — global append-only log of every prompt sent. Each
  line carries `sessionId` + `cwd`.
- `sessions-index.json` — summary index (not yet consumed here).

All functions are read-only. All paths default to `~/.claude/` but accept
a `claude_home` override (set `XA_CLAUDE_HOME` in the env to point at a
fixture tree during testing).

### Functions

| [`encode_project_slug`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.encode_project_slug)(cwd)                       | Encode a cwd into the `~/.claude/projects/` slug form.                     |
|-------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`ephemeral_session_alive`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.ephemeral_session_alive)(eph, \*[, proc_root])  | Is the process behind an ephemeral session dict actually alive?            |
| [`history_iter`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.history_iter)(\*[, claude_home])                | Yield entries from `~/.claude/history.jsonl` in file order (oldest first). |
| [`iter_ephemeral_sessions`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.iter_ephemeral_sessions)(\*[, claude_home])     | Yield all live ephemeral session dicts.                                    |
| [`iter_project_slugs`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.iter_project_slugs)(\*[, claude_home])          | Yield slugs (directory names) under `~/.claude/projects/`.                 |
| [`iter_transcript_events`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.iter_transcript_events)(path)                   | Yield every event from a transcript JSONL.                                 |
| [`iter_transcript_files`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.iter_transcript_files)(\*[, claude_home, ...])  | Yield transcript JSONL paths, optionally restricted to one project.        |
| [`parse_project_slug`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.parse_project_slug)(slug)                       | Decode a `~/.claude/projects/` slug back to a cwd.                         |
| [`read_ephemeral_session`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.read_ephemeral_session)(pid, \*[, claude_home]) | Read the per-process session file for `pid` if present.                    |
| [`remote_control_at_startup`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.remote_control_at_startup)(\*[, claude_home])   | Is this host set to auto-connect Remote Control for every session?         |
| [`transcript_forensics`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.transcript_forensics)(path)                     | Extract postmortem-relevant facts from the tail of a transcript.           |
| [`transcript_metadata`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.transcript_metadata)(path)                      | Summarise a transcript without holding it all in memory.                   |
| [`transcript_path`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.transcript_path)(cwd, session_id, \*[, ...])    | Return the transcript path for `(cwd, session_id)` if it exists.           |

### Classes

| [`HistoryEntry`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.HistoryEntry)(cwd, project, display, ...)   | One line from `~/.claude/history.jsonl`.                             |
|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`TranscriptForensics`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.TranscriptForensics)(transcript_path, ...)  | Postmortem facts extracted by walking a transcript from the end.     |
| [`TranscriptMeta`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.TranscriptMeta)(path, session_id, cwd, ...) | Summary of a transcript JSONL, cheap enough to compute for listings. |

### *class* xa.claude_fs.HistoryEntry(cwd, project, display, pasted_contents)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One line from `~/.claude/history.jsonl`.

### *class* xa.claude_fs.TranscriptForensics(transcript_path, line_count, last_tool_name, last_tool_command, last_tool_exit_code, last_tool_result_tail, final_assistant_text, user_interrupted)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Postmortem facts extracted by walking a transcript from the end.

`user_interrupted` is the raw marker presence — it is **ambiguous**
(also fires on phone-standby bridge drops), so do not derive user intent
from it alone. Callers should corroborate against pane-log tails.

### *class* xa.claude_fs.TranscriptMeta(path, session_id, cwd, project_slug, summary, custom_title, first_user_message, turn_count, forked_from, created, modified, size_bytes)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Summary of a transcript JSONL, cheap enough to compute for listings.

### xa.claude_fs.encode_project_slug(cwd)

Encode a cwd into the `~/.claude/projects/` slug form.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> encode_project_slug('/root/py/proj/tt/glossa')
'-root-py-proj-tt-glossa'
>>> encode_project_slug('/')
'-'
```

### xa.claude_fs.ephemeral_session_alive(eph, , proc_root=PosixPath('/proc'))

Is the process behind an ephemeral session dict actually alive?

The mere presence of `~/.claude/sessions/<pid>.json` proves nothing:
claude removes it on clean exit, but crashes, SIGKILL, and host
reboots leave stale files behind — and a stale file must not surface
as a “live” session (with a dead URL, an unkillable card, and no
postmortem).

Checks, strongest first:

- newer claude versions write `procStart` (the kernel start-time
  ticks) into the file — when present, it must match
  `/proc/<pid>/stat`, which also defeats PID reuse;
- otherwise the pid must exist and look like a claude process
  (`comm == "claude"` for the native binary, or `claude` in the
  cmdline for npm installs running under `node`);
- on platforms without `/proc` (macOS), fall back to signal-0
  existence (no PID-reuse protection, best available).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### xa.claude_fs.history_iter(, claude_home=PosixPath('/home/runner/.claude'))

Yield entries from `~/.claude/history.jsonl` in file order (oldest first).

Useful for cross-project full-text prompt search without loading
every transcript.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`HistoryEntry`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.HistoryEntry)]

### xa.claude_fs.iter_ephemeral_sessions(, claude_home=PosixPath('/home/runner/.claude'))

Yield all live ephemeral session dicts.

A session file disappears when claude exits, so this reflects
momentary state only. NB: claude deletes the file on *clean* exit
only — a crash / SIGKILL / reboot leaves a stale file behind, so
callers who care about actual liveness must validate each dict with
[`ephemeral_session_alive()`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.ephemeral_session_alive) (or an equivalent predicate).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.claude_fs.iter_project_slugs(, claude_home=PosixPath('/home/runner/.claude'))

Yield slugs (directory names) under `~/.claude/projects/`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### xa.claude_fs.iter_transcript_events(path)

Yield every event from a transcript JSONL.

Lines that fail to parse are silently skipped — transcripts are
append-only on a live process, so the tail can be partial.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.claude_fs.iter_transcript_files(, claude_home=PosixPath('/home/runner/.claude'), project_slug=None)

Yield transcript JSONL paths, optionally restricted to one project.

Only files whose stem is a valid UUID are yielded — this filters out
the `memory/` subfolder and other non-session artefacts Claude Code
stores alongside transcripts.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### xa.claude_fs.parse_project_slug(slug)

Decode a `~/.claude/projects/` slug back to a cwd.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> parse_project_slug('-root-py-proj-tt-glossa')
'/root/py/proj/tt/glossa'
>>> parse_project_slug('-Users-thorwhalen-tw-server')
'/Users/thorwhalen/tw/server'
```

NB: This transform is lossy — a cwd with literal `-` in segment names
round-trips to a different slug. Claude Code itself has the same
limitation, so we accept it.

### xa.claude_fs.read_ephemeral_session(pid, , claude_home=PosixPath('/home/runner/.claude'))

Read the per-process session file for `pid` if present.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.claude_fs.remote_control_at_startup(, claude_home=PosixPath('/home/runner/.claude'))

Is this host set to auto-connect Remote Control for every session?

Reads `remoteControlAtStartup` from the user `settings.json`.
`None` means “can’t tell” — no file, unreadable, or the key absent
(in which case Claude Code follows its own default, which xa does not
get to observe).

Deliberately only the *user* settings file. Claude Code also honors
the key in project, local and managed settings, and a project-level
`false` outranks a user-level `true` — so a `True` here is
“configured on for this user”, not a guarantee about a given session.
xa uses it to explain a missing URL, never to decide behavior.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

### xa.claude_fs.transcript_forensics(path)

Extract postmortem-relevant facts from the tail of a transcript.

Walks from the *end* of the file so we find the most recent
`tool_use` / `tool_result` / assistant `text` without reparsing
the whole transcript.

* **Return type:**
  [`TranscriptForensics`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.TranscriptForensics)

### xa.claude_fs.transcript_metadata(path)

Summarise a transcript without holding it all in memory.

Walks forward, counting turns and capturing the first user message /
summary / custom title / fork pointer / cwd / sessionId. Stops reading
content fields once it has what it needs, but still counts turns to
the end. For very large transcripts, callers who only need counts
should use `iter_transcript_events` directly.

* **Return type:**
  [`TranscriptMeta`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.TranscriptMeta)

### xa.claude_fs.transcript_path(cwd, session_id, , claude_home=PosixPath('/home/runner/.claude'))

Return the transcript path for `(cwd, session_id)` if it exists.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

```pycon
>>> from pathlib import Path
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as td:
...     home = Path(td)
...     (home / 'projects' / '-foo-bar').mkdir(parents=True)
...     f = home / 'projects' / '-foo-bar' / 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jsonl'
...     _ = f.write_text('')
...     p = transcript_path('/foo/bar', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', claude_home=home)
...     p == f
True
```


# _autosummary/xa.cli.html.md

# xa.cli

Command-line interface for `xa`.

Exposes subcommands via `cw`: `list`, `info`, `history`, `spawn`,
`resume`, `kill`, `serve`, `sync`, `pick`, `gen-secret`, `revive`,
and the `archive` group (`list`, `log`, `forensics`).

Entry point: `xa` (see `[project.scripts]` in `pyproject.toml`).

### Functions

| [`archive_forensics_cmd`](_autosummary/xa.cli.html.md#xa.cli.archive_forensics_cmd)(archive_id)                 | Print rich forensics for an archived session as JSON.                                                                                            |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| [`archive_list_cmd`](_autosummary/xa.cli.html.md#xa.cli.archive_list_cmd)([limit, json_out])               | List archived sessions (`created` events, newest first).                                                                                         |
| [`archive_log_cmd`](_autosummary/xa.cli.html.md#xa.cli.archive_log_cmd)(archive_id[, tail_kb])            | Print the pane log for an archived session.                                                                                                      |
| [`gen_secret_cmd`](_autosummary/xa.cli.html.md#xa.cli.gen_secret_cmd)([length])                          | Print a cryptographically strong random hex secret.                                                                                              |
| [`history_cmd`](_autosummary/xa.cli.html.md#xa.cli.history_cmd)([search, limit, json_out])            | Grep over `~/.claude/history.jsonl`.                                                                                                             |
| [`info_cmd`](_autosummary/xa.cli.html.md#xa.cli.info_cmd)(session_id[, json_out])                  | Show full metadata + forensics for one session.                                                                                                  |
| [`kill_cmd`](_autosummary/xa.cli.html.md#xa.cli.kill_cmd)(session_id)                              | Kill the tmux session backing a live Claude Code session.                                                                                        |
| [`list_cmd`](_autosummary/xa.cli.html.md#xa.cli.list_cmd)([project, limit, include_forks, ...])    | List Claude Code sessions across configured hosts, newest first.                                                                                 |
| [`main`](_autosummary/xa.cli.html.md#xa.cli.main)()                                            | Entry point referenced by `pyproject.toml`'s `[project.scripts]`.                                                                                |
| [`mk_parser`](_autosummary/xa.cli.html.md#xa.cli.mk_parser)()                                       | The `xa` parser -- a plain [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser), built, not run. |
| [`pick_cmd`](_autosummary/xa.cli.html.md#xa.cli.pick_cmd)([project, limit, host])                  | Interactive session picker — pick a row by number, then choose an action.                                                                        |
| [`resume_cmd`](_autosummary/xa.cli.html.md#xa.cli.resume_cmd)(session_id[, name, cwd, timeout, ...]) | Resume a past session (`claude --resume`) in a new tmux pane.                                                                                    |
| [`revive_cmd`](_autosummary/xa.cli.html.md#xa.cli.revive_cmd)([apply, include_held_elsewhere, ...])  | Report — and optionally reconnect — panes whose Remote Control dropped.                                                                          |
| [`serve_cmd`](_autosummary/xa.cli.html.md#xa.cli.serve_cmd)([host, port, mount, username, ...])     | Run the xa HTTP service (requires the `xa[service]` extra).                                                                                      |
| [`spawn_cmd`](_autosummary/xa.cli.html.md#xa.cli.spawn_cmd)(cwd[, name, timeout, ...])              | Spawn a detached claude-in-tmux session and print its remote URL.                                                                                |
| [`sync_cmd`](_autosummary/xa.cli.html.md#xa.cli.sync_cmd)([host, force])                           | Refresh remote-host caches (SSH hosts; HTTP is always fresh).                                                                                    |

### xa.cli.archive_forensics_cmd(archive_id)

Print rich forensics for an archived session as JSON.

* **Parameters:**
  **archive_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Archive id from `xa archive list`.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.archive_list_cmd(limit=30, json_out=False)

List archived sessions (`created` events, newest first).

* **Parameters:**
  * **limit** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Maximum rows (0 = unlimited).
  * **json_out** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – One JSON object per line.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.archive_log_cmd(archive_id, tail_kb=64)

Print the pane log for an archived session.

* **Parameters:**
  * **archive_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The archive id (first column of `xa archive list`).
  * **tail_kb** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Only print the last N KB (0 = full log).
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.gen_secret_cmd(length=32)

Print a cryptographically strong random hex secret.

Useful for `XA_PASSWORD`, `XA_CAPTCHA_KEY`, bearer tokens. Output
is to stdout with a trailing newline; pipe it into anything.

* **Parameters:**
  **length** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Byte length of the secret (hex output is 2x this).
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.history_cmd(search=None, limit=20, json_out=False)

Grep over `~/.claude/history.jsonl`.

* **Parameters:**
  * **search** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Case-insensitive substring to filter on (display field).
  * **limit** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Maximum rows to emit (0 = unlimited).
  * **json_out** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Emit one JSON object per line.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.info_cmd(session_id, json_out=False)

Show full metadata + forensics for one session.

* **Parameters:**
  * **session_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Full UUID or unique prefix (e.g. first 8 chars).
  * **json_out** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Emit a single JSON object.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.kill_cmd(session_id)

Kill the tmux session backing a live Claude Code session.

* **Parameters:**
  **session_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Full UUID or unique prefix, or a tmux session name.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.list_cmd(project=None, limit=30, include_forks=True, no_live=False, state=None, host=None, json_out=False)

List Claude Code sessions across configured hosts, newest first.

* **Parameters:**
  * **project** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Substring match against session cwd (case-insensitive).
  * **limit** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Maximum rows to show (0 = unlimited).
  * **include_forks** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Include sessions spawned with `claude --resume`.
  * **no_live** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Skip the tmux/ephemeral scan (archive-only view).
  * **state** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Restrict to ‘live’, ‘archived’, or ‘transcript_only’.
  * **host** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Restrict to a single configured host by name.
  * **json_out** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Emit one JSON object per line instead of a table.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.main()

Entry point referenced by `pyproject.toml`’s `[project.scripts]`.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.mk_parser()

The `xa` parser – a plain [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser), built, not run.

Two calls rather than one `cw.dispatch(...)` because the `archive` group
carries `group_kwargs` (its one-line help in the top-level listing), and a
single mapping has nowhere to put those.

* **Return type:**
  [`ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)

### xa.cli.pick_cmd(project=None, limit=30, host=None)

Interactive session picker — pick a row by number, then choose an action.

A pragmatic v1 picker without a TUI framework. Use **xa list**
for scripting; use **xa pick** when a human is at the keyboard.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.resume_cmd(session_id, name=None, cwd=None, timeout=120.0, model=None, effort=None)

Resume a past session (`claude --resume`) in a new tmux pane.

* **Parameters:**
  * **session_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Full UUID or unique prefix.
  * **name** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Session name — tmux session and claude display name (auto if omitted).
  * **cwd** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Override cwd (defaults to the original session’s cwd).
  * **timeout** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Seconds to wait for the bridge URL.
  * **model** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Per-session `claude --model` (omitted → claude’s default).
  * **effort** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Per-session `claude --effort` (omitted → claude’s default).
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.revive_cmd(apply=False, include_held_elsewhere=False, server_mode=False, min_interval=0.0, json_out=False)

Report — and optionally reconnect — panes whose Remote Control dropped.

Prints one line per live claude pane with its verdict, so “nothing to do”
is legible next to how much was looked at. Sends nothing without
`--apply`.

* **Parameters:**
  * **apply** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Actually send `/remote-control`. Off by default.
  * **include_held_elsewhere** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also reconnect sessions taken over from
    another device. This steals them back — deliberate keystroke only.
  * **server_mode** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also restart died `claude remote-control` servers.
  * **min_interval** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Seconds before the same pane may be touched again
    (0 uses the default guard interval).
  * **json_out** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Emit JSON instead of the table.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.serve_cmd(host='127.0.0.1', port=8010, mount='', username=None, password=None, password_env=None, captcha=False, webui=True, default_folder=None, i_know_its_insecure=False)

Run the xa HTTP service (requires the `xa[service]` extra).

**Secure by default.** Refuses to bind to a non-loopback interface
without HTTP Basic credentials. Override this guardrail with
`--i-know-its-insecure` only in locked-down deployments (e.g.
behind an mTLS-terminating proxy).

* **Parameters:**
  * **host** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Bind host. `127.0.0.1` by default — only this machine.
  * **port** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Bind port.
  * **mount** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Optional mount prefix (e.g. `/api/xa`).
  * **username** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – HTTP Basic username.
  * **password** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – HTTP Basic password. Prefer `--password-env`.
  * **password_env** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Name of env var to read the password from (recommended).
  * **captcha** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Enable captcha-gated deletes. Strongly recommended for
    any non-loopback deployment.
  * **webui** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Serve the bundled web UI at the mount root (default on).
    Pass `--no-webui` to run API-only.
  * **default_folder** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Default working directory the webui prefills into
    the “new session” dialog and the folder chooser opens at. Falls back
    to `$XA_DEFAULT_FOLDER` or `$HOME` if unset.
  * **i_know_its_insecure** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Skip the public-bind-without-auth safety check.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.spawn_cmd(cwd, name=None, timeout=120.0, no_remote_control=False, model=None, effort=None)

Spawn a detached claude-in-tmux session and print its remote URL.

* **Parameters:**
  * **cwd** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Working directory to start claude in.
  * **name** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Session name — tmux session and claude display name (auto if omitted).
  * **timeout** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Seconds to wait for the bridge URL.
  * **no_remote_control** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Spawn without passing `--remote-control`.
  * **model** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Per-session `claude --model` (omitted → claude’s default).
  * **effort** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Per-session `claude --effort` (omitted → claude’s default).
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.cli.sync_cmd(host=None, force=False)

Refresh remote-host caches (SSH hosts; HTTP is always fresh).

* **Parameters:**
  * **host** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Only sync this one host (default: all configured).
  * **force** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Sync even if the cache isn’t stale yet.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/xa.config.html.md

# xa.config

TOML config loader for `xa`.

Resolves a host registry and settings from `~/.config/xa/config.toml`
(respecting `$XDG_CONFIG_HOME`). Override the path via `$XA_CONFIG`.

### Example

```toml
[settings]
cache_dir = "~/.cache/xa/remotes"
stale_threshold_sec = 3600

[hosts.local]
kind = "local"

[hosts.devbox]
kind = "ssh"
host = "devbox"

[hosts.phone_mirror]
kind = "http"
base_url = "https://apps.example.com/api/xa"
auth = "basic"
username = "me"
password_env = "XA_PHONE_PASSWORD"   # value read from env, not stored in TOML
```

The registry exposed by [`load_hosts()`](_autosummary/xa.config.html.md#xa.config.load_hosts) is `{name: Host}` and can be
passed directly to [`xa.sessions.list_sessions()`](_autosummary/xa.sessions.html.md#xa.sessions.list_sessions).

### Functions

| `default_config_path`()                                             |                                                         |
|---------------------------------------------------------------------|---------------------------------------------------------|
| [`load`](_autosummary/xa.config.html.md#xa.config.load)([path])       | Return `(settings, hosts_registry)` from a config file. |
| [`load_hosts`](_autosummary/xa.config.html.md#xa.config.load_hosts)([path]) | Shortcut: just the host registry.                       |

### Classes

| [`Settings`](_autosummary/xa.config.html.md#xa.config.Settings)(cache_dir[, stale_threshold_sec, ...])   |    |
|----------------------------------------------------------------------------------------------------|----|

### *class* xa.config.Settings(cache_dir, stale_threshold_sec=3600, claude_bin='claude', tmux_bin='tmux')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

### xa.config.load(path=None)

Return `(settings, hosts_registry)` from a config file.

Missing file → `(defaults, {"local": LocalHost()})`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Settings`](_autosummary/xa.config.html.md#xa.config.Settings), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.config.load_hosts(path=None)

Shortcut: just the host registry.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/xa.hosts.html.md

# xa.hosts

Host abstraction — local, SSH, HTTP backends.

A `Host` is anything that can enumerate Claude Code sessions and
perform lifecycle actions on them (spawn / resume / kill). All transports
return the same [`xa.sessions.Session`](_autosummary/xa.sessions.html.md#xa.sessions.Session) shape so higher layers
don’t care whether sessions live on this machine, on another machine
over SSH, or behind another `xa` server over HTTPS.

The protocol is duck-typed (`runtime_checkable`), so user extensions
only need to implement the methods they actually use — unused methods
can raise `NotImplementedError`.

### Functions

| [`default_hosts`](_autosummary/xa.hosts.html.md#xa.hosts.default_hosts)()   | The out-of-the-box registry: one local host, nothing else.   |
|--------------------------------------------------------------------|--------------------------------------------------------------|

### Classes

| [`Host`](_autosummary/xa.hosts.html.md#xa.hosts.Host)(\*args, \*\*kwargs)                        | Duck-typed interface every transport satisfies.   |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------|
| [`LocalHost`](_autosummary/xa.hosts.html.md#xa.hosts.LocalHost)([name, claude_home, claude_bin, ...]) | The machine `xa` is running on.                   |
| [`SSHHost`](_autosummary/xa.hosts.html.md#xa.hosts.SSHHost)(name, \*, host[, user, ...])            | Transcripts via rsync; actions via ssh exec.      |
| [`HTTPHost`](_autosummary/xa.hosts.html.md#xa.hosts.HTTPHost)(name, \*, base_url[, auth, ...])       | Client for a remote `xa serve` instance.          |

### *class* xa.hosts.HTTPHost(name, , base_url, auth=None, username=None, password=None, token=None, timeout=30.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Client for a remote `xa serve` instance.

#### sync(, force=False)

HTTP sessions are fetched fresh on each listing — no separate sync.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* xa.hosts.Host(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Duck-typed interface every transport satisfies.

### *class* xa.hosts.LocalHost(name='local', , claude_home=PosixPath('/home/runner/.claude'), claude_bin='claude', tmux_bin='tmux', alive_predicate=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The machine `xa` is running on.

#### sync(, force=False)

No-op — local has nothing to sync.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* xa.hosts.SSHHost(name, , host, user=None, remote_claude_home='~/.claude', cache_dir=PosixPath('/home/runner/.cache/xa/remotes'), stale_threshold_sec=3600, claude_bin='claude', tmux_bin='tmux', ssh_bin='ssh', rsync_bin='rsync')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Transcripts via rsync; actions via ssh exec.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Logical name used in `Session.host` and elsewhere.
  * **host** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – SSH hostname / alias. Matches cc-sessions: either an
    `~/.ssh/config` alias (`devbox`) or a raw host (`1.2.3.4`).
  * **user** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional SSH user for raw hosts.
  * **remote_claude_home** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Remote path to `~/.claude/`. Defaults to `~/.claude` which
    resolves relative to the SSH user’s home on the remote side.
  * **cache_dir** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Where to stage the rsync’d tree locally.
  * **stale_threshold_sec** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Re-sync when the cached copy is older than this. `0` means
    always sync.

#### spawn(name, , cwd, \*\*opts)

Spawn `tmux new-session -d 's <name>' 'cd <cwd> && exec claude'` over SSH.

URL detection is left to the caller’s next [`sync()`](_autosummary/xa.hosts.html.md#xa.hosts.SSHHost.sync) + listing —
we don’t round-trip waiting for the bridge URL here (that would
hold an SSH connection open for up to 2 minutes).

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)

#### sync(, force=False)

Pull `<remote_claude_home>/projects/` and `/sessions/` into the cache.

Non-existent remote directories are tolerated (rsync returns 23).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.hosts.default_hosts()

The out-of-the-box registry: one local host, nothing else.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Host`](_autosummary/xa.hosts.html.md#xa.hosts.Host)]

### Modules

| [`http`](_autosummary/xa.hosts.http.html.md#module-xa.hosts.http)   | HTTP host — a remote `xa serve` instance.   |
|------------------------------------------------------------------------------|---------------------------------------------|
| [`local`](_autosummary/xa.hosts.local.html.md#module-xa.hosts.local) | Local in-process host.                      |
| [`ssh`](_autosummary/xa.hosts.ssh.html.md#module-xa.hosts.ssh)     | SSH-backed host.                            |


# _autosummary/xa.hosts.http.html.md

# xa.hosts.http

HTTP host — a remote `xa serve` instance.

The remote must be running `xa serve` (or an API with the same
`/sessions` + `/archive` surface) and be reachable over HTTPS.
All discovery and actions are performed through the API; no SSH keys,
no rsync. Auth is pluggable:

- `auth="basic"` → HTTP Basic with `username` / `password`
- `auth="bearer"` → `Authorization: Bearer <token>`
- `auth=None`   → no auth (host is already gated upstream)

`urllib` is used for transport so there’s no hard dep on `httpx`
or `requests`; `xa.hosts.http` works in any Python 3.10+ install.

### Classes

| [`HTTPHost`](_autosummary/xa.hosts.http.html.md#xa.hosts.http.HTTPHost)(name, \*, base_url[, auth, ...])   | Client for a remote `xa serve` instance.   |
|----------------------------------------------------------------------------------------------|--------------------------------------------|

### *class* xa.hosts.http.HTTPHost(name, , base_url, auth=None, username=None, password=None, token=None, timeout=30.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Client for a remote `xa serve` instance.

#### sync(, force=False)

HTTP sessions are fetched fresh on each listing — no separate sync.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/xa.hosts.local.html.md

# xa.hosts.local

Local in-process host.

All discovery and actions hit the local filesystem / tmux / `claude`
binary directly. Most `xa` users never instantiate any other host.

### Classes

| [`LocalHost`](_autosummary/xa.hosts.local.html.md#xa.hosts.local.LocalHost)([name, claude_home, claude_bin, ...])   | The machine `xa` is running on.   |
|----------------------------------------------------------------------------------------------------|-----------------------------------|

### *class* xa.hosts.local.LocalHost(name='local', , claude_home=PosixPath('/home/runner/.claude'), claude_bin='claude', tmux_bin='tmux', alive_predicate=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The machine `xa` is running on.

#### sync(, force=False)

No-op — local has nothing to sync.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/xa.hosts.ssh.html.md

# xa.hosts.ssh

SSH-backed host.

Mirrors the cc-sessions transport pattern: pull the remote’s
`~/.claude/` tree into a local cache via `rsync -az --delete -e ssh`,
then read the cache with the same `claude_fs` code paths we use for
local discovery. Actions (`spawn` / `resume` / `kill` / tmux queries)
dispatch to the remote via `ssh <host> <cmd>`.

No Python SSH library is pulled in: we shell out to the system `ssh` /
`rsync` binaries, which means the user’s `~/.ssh/config`, agent, and
host keys Just Work. Install overhead: zero.

### Classes

| [`SSHHost`](_autosummary/xa.hosts.ssh.html.md#xa.hosts.ssh.SSHHost)(name, \*, host[, user, ...])   | Transcripts via rsync; actions via ssh exec.   |
|-----------------------------------------------------------------------------------------|------------------------------------------------|

### *class* xa.hosts.ssh.SSHHost(name, , host, user=None, remote_claude_home='~/.claude', cache_dir=PosixPath('/home/runner/.cache/xa/remotes'), stale_threshold_sec=3600, claude_bin='claude', tmux_bin='tmux', ssh_bin='ssh', rsync_bin='rsync')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Transcripts via rsync; actions via ssh exec.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Logical name used in `Session.host` and elsewhere.
  * **host** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – SSH hostname / alias. Matches cc-sessions: either an
    `~/.ssh/config` alias (`devbox`) or a raw host (`1.2.3.4`).
  * **user** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional SSH user for raw hosts.
  * **remote_claude_home** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Remote path to `~/.claude/`. Defaults to `~/.claude` which
    resolves relative to the SSH user’s home on the remote side.
  * **cache_dir** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Where to stage the rsync’d tree locally.
  * **stale_threshold_sec** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Re-sync when the cached copy is older than this. `0` means
    always sync.

#### spawn(name, , cwd, \*\*opts)

Spawn `tmux new-session -d 's <name>' 'cd <cwd> && exec claude'` over SSH.

URL detection is left to the caller’s next [`sync()`](_autosummary/xa.hosts.ssh.html.md#xa.hosts.ssh.SSHHost.sync) + listing —
we don’t round-trip waiting for the bridge URL here (that would
hold an SSH connection open for up to 2 minutes).

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)

#### sync(, force=False)

Pull `<remote_claude_home>/projects/` and `/sessions/` into the cache.

Non-existent remote directories are tolerated (rsync returns 23).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/xa.html.md

# xa

xa — tools for managing remote Claude Code sessions.

### Functions

| [`encode_project_slug`](_autosummary/xa.html.md#xa.encode_project_slug)(cwd)                           | Encode a cwd into the `~/.claude/projects/` slug form.                     |
|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`history_iter`](_autosummary/xa.html.md#xa.history_iter)(\*[, claude_home])                    | Yield entries from `~/.claude/history.jsonl` in file order (oldest first). |
| [`iter_ephemeral_sessions`](_autosummary/xa.html.md#xa.iter_ephemeral_sessions)(\*[, claude_home])         | Yield all live ephemeral session dicts.                                    |
| [`iter_project_slugs`](_autosummary/xa.html.md#xa.iter_project_slugs)(\*[, claude_home])              | Yield slugs (directory names) under `~/.claude/projects/`.                 |
| [`iter_transcript_events`](_autosummary/xa.html.md#xa.iter_transcript_events)(path)                       | Yield every event from a transcript JSONL.                                 |
| [`iter_transcript_files`](_autosummary/xa.html.md#xa.iter_transcript_files)(\*[, claude_home, ...])      | Yield transcript JSONL paths, optionally restricted to one project.        |
| [`parse_project_slug`](_autosummary/xa.html.md#xa.parse_project_slug)(slug)                           | Decode a `~/.claude/projects/` slug back to a cwd.                         |
| [`read_ephemeral_session`](_autosummary/xa.html.md#xa.read_ephemeral_session)(pid, \*[, claude_home])     | Read the per-process session file for `pid` if present.                    |
| [`transcript_forensics`](_autosummary/xa.html.md#xa.transcript_forensics)(path)                         | Extract postmortem-relevant facts from the tail of a transcript.           |
| [`transcript_metadata`](_autosummary/xa.html.md#xa.transcript_metadata)(path)                          | Summarise a transcript without holding it all in memory.                   |
| [`transcript_path`](_autosummary/xa.html.md#xa.transcript_path)(cwd, session_id, \*[, ...])        | Return the transcript path for `(cwd, session_id)` if it exists.           |
| [`resolve_bridge_url`](_autosummary/xa.html.md#xa.resolve_bridge_url)(session_name, \*[, ...])        | Return `(url, source)` for a live tmux-hosted claude session.              |
| [`resume_session`](_autosummary/xa.html.md#xa.resume_session)(claude_session_id, \*, cwd[, ...])  | Launch `claude --resume <id>` in a new detached tmux session.              |
| [`spawn_session`](_autosummary/xa.html.md#xa.spawn_session)(name, \*, cwd[, claude_bin, ...])    | Create a detached tmux session running `claude` in `cwd`.                  |
| [`get_session`](_autosummary/xa.html.md#xa.get_session)(session_id, \*[, hosts, claude_home])  | Find one session by full ID or unique prefix.                              |
| [`iter_local_sessions`](_autosummary/xa.html.md#xa.iter_local_sessions)(\*[, claude_home, ...])        | Legacy local-only iterator.                                                |
| [`kill_session`](_autosummary/xa.html.md#xa.kill_session)(session, \*[, hosts])                 | Kill a live `Session`, scoped to what actually belongs to it.              |
| [`list_sessions`](_autosummary/xa.html.md#xa.list_sessions)(\*[, hosts, project, ...])           | Return sessions sorted by recency (live first, then newest-modified).      |
| [`resume`](_autosummary/xa.html.md#xa.resume)(session, \*[, cwd, name, hosts])            | Resume a transcript / archived session on its originating host.            |
| [`default_events_store`](_autosummary/xa.html.md#xa.default_events_store)([state_dir])                  | The event log at `<state_dir>/events.jsonl`.                               |
| [`default_pane_store`](_autosummary/xa.html.md#xa.default_pane_store)([state_dir])                    | Per-session pane logs at `<state_dir>/panes/<id>.log`.                     |
| `append_created`(events, \*, id, name, cwd, ...)                                                    |                                                                            |
| `append_gone`(events, \*, id, name, reason[, ...])                                                  |                                                                            |
| [`append_hidden`](_autosummary/xa.html.md#xa.append_hidden)(events, \*, id, hidden)              | Mark an archived session as hidden (or un-hide it).                        |
| [`append_label`](_autosummary/xa.html.md#xa.append_label)(events, \*, id, label)                | Set or clear a user-supplied display label for a session.                  |
| `append_url_acquired`(events, \*, id, name[, ...])                                                  |                                                                            |
| [`classify_death`](_autosummary/xa.html.md#xa.classify_death)(pane_kind, \*[, replaced, ...])     | Pick the most specific death reason from available signals.                |
| [`overlays`](_autosummary/xa.html.md#xa.overlays)(events)                                   | Fold `labeled` / `hidden` events into `{id: {label, hidden}}`.             |
| [`reconcile`](_autosummary/xa.html.md#xa.reconcile)(events, panes, live_sessions, \*[, ...]) | Emit `gone` events for archived sessions missing from `live_sessions`.     |
| [`records`](_autosummary/xa.html.md#xa.records)(events, panes)                             | Return per-session summaries, newest-first (by creation time).             |
| [`default_hosts`](_autosummary/xa.html.md#xa.default_hosts)()                                    | The out-of-the-box registry: one local host, nothing else.                 |
| `default_config_path`()                                                                             |                                                                            |
| [`load`](_autosummary/xa.html.md#xa.load)([path])                                       | Return `(settings, hosts_registry)` from a config file.                    |
| [`load_hosts`](_autosummary/xa.html.md#xa.load_hosts)([path])                                 | Shortcut: just the host registry.                                          |

### Classes

| [`HistoryEntry`](_autosummary/xa.html.md#xa.HistoryEntry)(cwd, project, display, ...)        | One line from `~/.claude/history.jsonl`.                             |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`TranscriptForensics`](_autosummary/xa.html.md#xa.TranscriptForensics)(transcript_path, ...)       | Postmortem facts extracted by walking a transcript from the end.     |
| [`TranscriptMeta`](_autosummary/xa.html.md#xa.TranscriptMeta)(path, session_id, cwd, ...)      | Summary of a transcript JSONL, cheap enough to compute for listings. |
| [`SpawnResult`](_autosummary/xa.html.md#xa.SpawnResult)(name, cwd, claude_pid, ...[, ...])  | What `spawn_session` / `resume_session` return to the caller.        |
| [`Session`](_autosummary/xa.html.md#xa.Session)(id, claude_session_id, ...[, ...])      | Canonical session record — valid across hosts and states.            |
| [`FileStore`](_autosummary/xa.html.md#xa.FileStore)(root, \*[, suffix])                   | Directory of files, accessed by key.                                 |
| [`JsonLinesStore`](_autosummary/xa.html.md#xa.JsonLinesStore)(path)                            | Append-only JSONL file with an `Iterable` reader.                    |
| [`ArchiveRecord`](_autosummary/xa.html.md#xa.ArchiveRecord)(id, name, cwd, created, url, ...) |                                                                      |
| [`HTTPHost`](_autosummary/xa.html.md#xa.HTTPHost)(name, \*, base_url[, auth, ...])       | Client for a remote `xa serve` instance.                             |
| [`Host`](_autosummary/xa.html.md#xa.Host)(\*args, \*\*kwargs)                        | Duck-typed interface every transport satisfies.                      |
| [`LocalHost`](_autosummary/xa.html.md#xa.LocalHost)([name, claude_home, claude_bin, ...]) | The machine `xa` is running on.                                      |
| [`SSHHost`](_autosummary/xa.html.md#xa.SSHHost)(name, \*, host[, user, ...])            | Transcripts via rsync; actions via ssh exec.                         |
| [`Settings`](_autosummary/xa.html.md#xa.Settings)(cache_dir[, stale_threshold_sec, ...]) |                                                                      |

### *class* xa.ArchiveRecord(id, name, cwd, created, url, gone, gone_detected, gone_reason, pane_log_bytes, claude_session_id, forensics, label=None, hidden=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

### *class* xa.FileStore(root, , suffix='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Directory of files, accessed by key.

Values are `bytes`. Keys must match `[A-Za-z0-9_.-]+` — this
rejects path traversal attempts like `../etc/passwd`.

#### path_for(key)

Public path accessor (for callers that need to hand the path to tmux).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### *class* xa.HTTPHost(name, , base_url, auth=None, username=None, password=None, token=None, timeout=30.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Client for a remote `xa serve` instance.

#### sync(, force=False)

HTTP sessions are fetched fresh on each listing — no separate sync.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* xa.HistoryEntry(cwd, project, display, pasted_contents)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One line from `~/.claude/history.jsonl`.

### *class* xa.Host(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Duck-typed interface every transport satisfies.

### *class* xa.JsonLinesStore(path)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Append-only JSONL file with an `Iterable` reader.

```pycon
>>> import tempfile, pathlib
>>> with tempfile.TemporaryDirectory() as td:
...     s = JsonLinesStore(pathlib.Path(td) / 'log.jsonl')
...     s.append({'a': 1})
...     s.append({'b': 2})
...     list(s) == [{'a': 1}, {'b': 2}]
True
```

### *class* xa.LocalHost(name='local', , claude_home=PosixPath('/home/runner/.claude'), claude_bin='claude', tmux_bin='tmux', alive_predicate=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The machine `xa` is running on.

#### sync(, force=False)

No-op — local has nothing to sync.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* xa.SSHHost(name, , host, user=None, remote_claude_home='~/.claude', cache_dir=PosixPath('/home/runner/.cache/xa/remotes'), stale_threshold_sec=3600, claude_bin='claude', tmux_bin='tmux', ssh_bin='ssh', rsync_bin='rsync')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Transcripts via rsync; actions via ssh exec.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Logical name used in `Session.host` and elsewhere.
  * **host** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – SSH hostname / alias. Matches cc-sessions: either an
    `~/.ssh/config` alias (`devbox`) or a raw host (`1.2.3.4`).
  * **user** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional SSH user for raw hosts.
  * **remote_claude_home** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Remote path to `~/.claude/`. Defaults to `~/.claude` which
    resolves relative to the SSH user’s home on the remote side.
  * **cache_dir** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Where to stage the rsync’d tree locally.
  * **stale_threshold_sec** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Re-sync when the cached copy is older than this. `0` means
    always sync.

#### spawn(name, , cwd, \*\*opts)

Spawn `tmux new-session -d 's <name>' 'cd <cwd> && exec claude'` over SSH.

URL detection is left to the caller’s next [`sync()`](_autosummary/xa.html.md#xa.SSHHost.sync) + listing —
we don’t round-trip waiting for the bridge URL here (that would
hold an SSH connection open for up to 2 minutes).

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)

#### sync(, force=False)

Pull `<remote_claude_home>/projects/` and `/sessions/` into the cache.

Non-existent remote directories are tolerated (rsync returns 23).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* xa.Session(id, claude_session_id, bridge_session_id, host, cwd, project_slug, state, live_pid, tmux_name, name, summary, first_user_message, turn_count, forked_from, created, modified, url, url_source, transcript_path, pre_first_turn=False, attention=None, attention_hint=None, tmux_pane=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Canonical session record — valid across hosts and states.

Fields that don’t apply to a given state are `None`. Discovery
functions return fresh instances; callers should treat it as
immutable.

### *class* xa.Settings(cache_dir, stale_threshold_sec=3600, claude_bin='claude', tmux_bin='tmux')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

### *class* xa.SpawnResult(name, cwd, claude_pid, claude_session_id, bridge_session_id, url, url_source, warning, attention=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What `spawn_session` / `resume_session` return to the caller.

### *class* xa.TranscriptForensics(transcript_path, line_count, last_tool_name, last_tool_command, last_tool_exit_code, last_tool_result_tail, final_assistant_text, user_interrupted)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Postmortem facts extracted by walking a transcript from the end.

`user_interrupted` is the raw marker presence — it is **ambiguous**
(also fires on phone-standby bridge drops), so do not derive user intent
from it alone. Callers should corroborate against pane-log tails.

### *class* xa.TranscriptMeta(path, session_id, cwd, project_slug, summary, custom_title, first_user_message, turn_count, forked_from, created, modified, size_bytes)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Summary of a transcript JSONL, cheap enough to compute for listings.

### xa.append_hidden(events, , id, hidden)

Mark an archived session as hidden (or un-hide it).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.append_label(events, , id, label)

Set or clear a user-supplied display label for a session.

`id` can be an archive id, a tmux session name, or a
`claude_session_id` — whatever key the caller will use to look it
up later. Empty-string or `None` label clears any prior label.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.classify_death(pane_kind, , replaced=False, forensics=None, oom_markers=())

Pick the most specific death reason from available signals.

`oom_markers` is the tuple of OOM-shaped strings observed in the
pane tail (see `_OOM_PANE_MARKERS`). When the last tool exited
with 137 and at least one marker is present, we promote the verdict
to `oom_killed` — that pair is the strongest single signal we can
get without reading kernel logs (which would need root and is not
portable).

* **Return type:**
  [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'clean_exit'`, `'abrupt'`, `'interrupted'`, `'tool_crash'`, `'oom_killed'`, `'replaced'`, `'missing'`]

### xa.default_events_store(state_dir=PosixPath('/home/runner/.xa'))

The event log at `<state_dir>/events.jsonl`.

* **Return type:**
  [`JsonLinesStore`](_autosummary/xa.store.html.md#xa.store.JsonLinesStore)

### xa.default_hosts()

The out-of-the-box registry: one local host, nothing else.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Host`](_autosummary/xa.hosts.html.md#xa.hosts.Host)]

### xa.default_pane_store(state_dir=PosixPath('/home/runner/.xa'))

Per-session pane logs at `<state_dir>/panes/<id>.log`.

* **Return type:**
  [`FileStore`](_autosummary/xa.store.html.md#xa.store.FileStore)

### xa.encode_project_slug(cwd)

Encode a cwd into the `~/.claude/projects/` slug form.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> encode_project_slug('/root/py/proj/tt/glossa')
'-root-py-proj-tt-glossa'
>>> encode_project_slug('/')
'-'
```

### xa.get_session(session_id, , hosts=None, claude_home=None)

Find one session by full ID or unique prefix.

Raises `LookupError` if the prefix is ambiguous.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Session`](_autosummary/xa.sessions.html.md#xa.sessions.Session)]

### xa.history_iter(, claude_home=PosixPath('/home/runner/.claude'))

Yield entries from `~/.claude/history.jsonl` in file order (oldest first).

Useful for cross-project full-text prompt search without loading
every transcript.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`HistoryEntry`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.HistoryEntry)]

### xa.iter_ephemeral_sessions(, claude_home=PosixPath('/home/runner/.claude'))

Yield all live ephemeral session dicts.

A session file disappears when claude exits, so this reflects
momentary state only. NB: claude deletes the file on *clean* exit
only — a crash / SIGKILL / reboot leaves a stale file behind, so
callers who care about actual liveness must validate each dict with
`ephemeral_session_alive()` (or an equivalent predicate).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.iter_local_sessions(, claude_home=PosixPath('/home/runner/.claude'), project_slug=None, include_live=True, tmux_bin=None)

Legacy local-only iterator.

Retained for backward compatibility and because some tests construct
sessions directly from a fake `~/.claude/`. Wraps a fresh
[`LocalHost`](_autosummary/xa.html.md#xa.LocalHost).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Session`](_autosummary/xa.sessions.html.md#xa.sessions.Session)]

### xa.iter_project_slugs(, claude_home=PosixPath('/home/runner/.claude'))

Yield slugs (directory names) under `~/.claude/projects/`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### xa.iter_transcript_events(path)

Yield every event from a transcript JSONL.

Lines that fail to parse are silently skipped — transcripts are
append-only on a live process, so the tail can be partial.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.iter_transcript_files(, claude_home=PosixPath('/home/runner/.claude'), project_slug=None)

Yield transcript JSONL paths, optionally restricted to one project.

Only files whose stem is a valid UUID are yielded — this filters out
the `memory/` subfolder and other non-session artefacts Claude Code
stores alongside transcripts.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### xa.kill_session(session, , hosts=None)

Kill a live `Session`, scoped to what actually belongs to it.

Kills the backing tmux session only when it is *dedicated* to this
claude (single pane, claude verified in it) — a claude living in one
window of a shared multi-window workspace must not take the whole
workspace down, so there we SIGTERM the identity-verified pid
instead. Remote hosts keep the name-kill contract (their server
applies its own scoping). Returns a short description of what was
killed (e.g. `"tmux:mysess"` / `"pid:1234"`).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### xa.list_sessions(, hosts=None, project=None, include_forks=True, include_live=True, state=None, limit=None, claude_home=None, tmux_bin=None)

Return sessions sorted by recency (live first, then newest-modified).

When `hosts` is `None` we default to a single [`LocalHost`](_autosummary/xa.html.md#xa.LocalHost).
If `claude_home` or `tmux_bin` are given, they’re forwarded into
that default [`LocalHost`](_autosummary/xa.html.md#xa.LocalHost) — handy for fixture-based tests.

Filters:

- `project` — substring match against `cwd` (case-insensitive)
- `include_forks=False` — drop sessions with a `forked_from`
- `include_live=False` — skip live discovery (per host)
- `state` — one of `"live"` / `"archived"` / `"transcript_only"`

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Session`](_autosummary/xa.sessions.html.md#xa.sessions.Session)]

### xa.load(path=None)

Return `(settings, hosts_registry)` from a config file.

Missing file → `(defaults, {"local": LocalHost()})`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Settings`](_autosummary/xa.config.html.md#xa.config.Settings), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.load_hosts(path=None)

Shortcut: just the host registry.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### xa.overlays(events)

Fold `labeled` / `hidden` events into `{id: {label, hidden}}`.

Later events win. Useful when rendering live sessions too — callers
look up by whatever id they know (archive id, claude_session_id,
tmux name) and apply the overlay if present.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.parse_project_slug(slug)

Decode a `~/.claude/projects/` slug back to a cwd.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> parse_project_slug('-root-py-proj-tt-glossa')
'/root/py/proj/tt/glossa'
>>> parse_project_slug('-Users-thorwhalen-tw-server')
'/Users/thorwhalen/tw/server'
```

NB: This transform is lossy — a cwd with literal `-` in segment names
round-trips to a different slug. Claude Code itself has the same
limitation, so we accept it.

### xa.read_ephemeral_session(pid, , claude_home=PosixPath('/home/runner/.claude'))

Read the per-process session file for `pid` if present.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.reconcile(events, panes, live_sessions, , claude_home=PosixPath('/home/runner/.claude'))

Emit `gone` events for archived sessions missing from `live_sessions`.

Returns the list of freshly-emitted events (handy for tests). Idempotent:
calling twice with the same live list produces no new events the
second time.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### xa.records(events, panes)

Return per-session summaries, newest-first (by creation time).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`ArchiveRecord`](_autosummary/xa.archive.html.md#xa.archive.ArchiveRecord)]

### xa.resolve_bridge_url(session_name, , claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', scrape_lines=400)

Return `(url, source)` for a live tmux-hosted claude session.

Primary path: find the claude descendant pid → read
`~/.claude/sessions/<pid>.json` → take `bridgeSessionId`.
Fallback: regex-scrape `capture-pane` output for the full URL.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'session_file'`, `'pane_capture'`]]]

### xa.resume(session, , cwd=None, name=None, hosts=None, \*\*opts)

Resume a transcript / archived session on its originating host.

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)

### xa.resume_session(claude_session_id, , cwd, name=None, claude_bin='claude', claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', url_timeout_sec=120.0, auto_remote_control=True, model=None, effort=None, pane_log_path=None, archive_store=None, pane_store=None, archive_id=None)

Launch `claude --resume <id>` in a new detached tmux session.

A user-supplied `name` is also passed to `claude --name` (renaming
the resumed session’s display name); the auto-generated fallback name
is tmux-only so a resume never clobbers the session’s existing name.

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)

### xa.spawn_session(name, , cwd, claude_bin='claude', claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', url_timeout_sec=120.0, auto_remote_control=True, model=None, effort=None, pane_log_path=None, archive_store=None, pane_store=None, archive_id=None)

Create a detached tmux session running `claude` in `cwd`.

Waits up to `url_timeout_sec` for the bridge URL to appear, dismissing
the “trust this folder” prompt and enabling remote control (via the
`--remote-control` flag on modern claude binaries, or the TUI
handshake on older ones). The session name is also passed to
`claude --name` so the Claude-side display name matches; `model`
and `effort` become per-session `--model` / `--effort` flags.

If `archive_store` (and optionally `pane_store`) are given, emits
`created` + `url_acquired` events to the archive and pipes the pane
output to a file under `pane_store`.

Services that want an immediate return should call
`prepare_spawn()` and run `complete_spawn()` in the background
instead — this function is simply the composition of the two.

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)

### xa.transcript_forensics(path)

Extract postmortem-relevant facts from the tail of a transcript.

Walks from the *end* of the file so we find the most recent
`tool_use` / `tool_result` / assistant `text` without reparsing
the whole transcript.

* **Return type:**
  [`TranscriptForensics`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.TranscriptForensics)

### xa.transcript_metadata(path)

Summarise a transcript without holding it all in memory.

Walks forward, counting turns and capturing the first user message /
summary / custom title / fork pointer / cwd / sessionId. Stops reading
content fields once it has what it needs, but still counts turns to
the end. For very large transcripts, callers who only need counts
should use `iter_transcript_events` directly.

* **Return type:**
  [`TranscriptMeta`](_autosummary/xa.claude_fs.html.md#xa.claude_fs.TranscriptMeta)

### xa.transcript_path(cwd, session_id, , claude_home=PosixPath('/home/runner/.claude'))

Return the transcript path for `(cwd, session_id)` if it exists.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

```pycon
>>> from pathlib import Path
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as td:
...     home = Path(td)
...     (home / 'projects' / '-foo-bar').mkdir(parents=True)
...     f = home / 'projects' / '-foo-bar' / 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jsonl'
...     _ = f.write_text('')
...     p = transcript_path('/foo/bar', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', claude_home=home)
...     p == f
True
```

### Modules

| [`archive`](_autosummary/xa.archive.html.md#module-xa.archive)       | Postmortem archive of Claude Code sessions spawned by `xa`.              |
|----------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`claude_cli`](_autosummary/xa.claude_cli.html.md#module-xa.claude_cli) | Spawn, resume, and URL-resolve Claude Code sessions.                     |
| [`claude_fs`](_autosummary/xa.claude_fs.html.md#module-xa.claude_fs)   | Pure filesystem view of `~/.claude/`.                                    |
| [`cli`](_autosummary/xa.cli.html.md#module-xa.cli)               | Command-line interface for `xa`.                                         |
| [`config`](_autosummary/xa.config.html.md#module-xa.config)         | TOML config loader for `xa`.                                             |
| [`hosts`](_autosummary/xa.hosts.html.md#module-xa.hosts)           | Host abstraction — local, SSH, HTTP backends.                            |
| [`revive`](_autosummary/xa.revive.html.md#module-xa.revive)         | Detect — and reconnect — Claude Code panes whose Remote Control dropped. |
| [`service`](_autosummary/xa.service.html.md#module-xa.service)       | FastAPI service for `xa`.                                                |
| [`sessions`](_autosummary/xa.sessions.html.md#module-xa.sessions)     | The unified `Session` domain model and multi-host discovery.             |
| [`store`](_autosummary/xa.store.html.md#module-xa.store)           | Minimal key-value and append-log storage.                                |
| [`tmux`](_autosummary/xa.tmux.html.md#module-xa.tmux)             | Pure tmux wrappers.                                                      |


# _autosummary/xa.revive.html.md

# xa.revive

Detect — and reconnect — Claude Code panes whose Remote Control dropped.

Remote Control gives up on its own: after a few minutes of failed reconnects,
or roughly thirty minutes of failed presence heartbeats, it stops trying and
leaves the local session running but unreachable from claude.ai. The usual
cause is a network change (moving between networks, toggling a VPN) that
produces a transient HTTP 403 — the same change that clears it, usually
within a minute or two.

Two facts shape everything below, and both were verified rather than assumed:

- \*\*Two observables, and both were checked against a live session rather
  than assumed.\*\* The footer carries a `/rc` *pill* that is present while
  Remote Control is connected (`/rc`, or `/rc active` when verbose) and
  **disappears** when it is not — so the pill’s absence, not its presence,
  is what marks a dropped session. The ephemeral session file
  (`~/.claude/sessions/<pid>.json`) agrees from the other side: its
  `bridgeSessionId` is a real id while connected and `null` once it is
  not. The transcript JSONL never mentions Remote Control at all, so there
  is no third source.
- **\`\`tmux send-keys\`\` is the only way to act.** `/remote-control` is a
  built-in slash command; the CLI exposes no subcommand and no control
  request that reconnects a *running* session (checked against
  `claude --help` and `claude remote-control --help`, 2.1.251 — the only
  session-shaped verbs there are `agents`/`attach`/`logs`/`stop`/
  `rm`/`respawn`, all of which are about background agents).

The detection phrases in [`MARKERS`](_autosummary/xa.revive.html.md#xa.revive.MARKERS) are transcribed from the strings
embedded in the `claude` 2.1.251 binary, not guessed from screenshots.
They are data, and [`DEFAULT_RULES`](_autosummary/xa.revive.html.md#xa.revive.DEFAULT_RULES) is injectable, so a wording change
upstream is a one-line edit rather than a rewrite.

Four things this module refuses to do, each deliberate:

- **It never revives a session held elsewhere.** When a session was taken
  over or ended from another device, the TUI says so (WebSocket close code
  4090) and *omits* its “run /remote-control” advice. Sending the command
  > anyway would steal the session back from the user’s phone mid-sentence.

  [`HELD_ELSEWHERE`](_autosummary/xa.revive.html.md#xa.revive.HELD_ELSEWHERE) is therefore tested before [`RECONNECTABLE`](_autosummary/xa.revive.html.md#xa.revive.RECONNECTABLE)
  : and acting on it stays opt-in.
- **It never resends a prompt.** An API-stalled pane is reported and left
  alone: the fix there is a real model call that can duplicate work.
- **It never types into a pane waiting on a human.** A permission dialog or
  a `/login` prompt consumes keystrokes as *answers* — sending
  `/remote-control` into an open “Do you want to proceed?” selector would
  answer it. Those panes classify as [`NEEDS_HUMAN`](_autosummary/xa.revive.html.md#xa.revive.NEEDS_HUMAN) and are skipped.
- **It does nothing at all unless asked.** `apply=True` is explicit;
  every entry point is a dry run by default.

Typical use:

```default
from xa.revive import SessionPanes, revive

for action in revive(panes=SessionPanes()):      # dry run
    print(action.target, action.verdict, action.skipped)
```

Discovery does **not** enumerate tmux and guess which pane holds a claude.
It reads the live ephemeral session files, each of which records its own
pane as `"tmux": "session:@window.%pane"`, so the pane refs are exact and
`pane_current_command` never enters the decision. See [`local_panes()`](_autosummary/xa.revive.html.md#xa.revive.local_panes).

### Module Attributes

| [`NO_CLAUDE`](_autosummary/xa.revive.html.md#xa.revive.NO_CLAUDE)             | No claude process behind the pane — a shell prompt, or a session that died.                                                                                    |
|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`HELD_ELSEWHERE`](_autosummary/xa.revive.html.md#xa.revive.HELD_ELSEWHERE)        | Taken over or ended from another device.                                                                                                                       |
| [`CONNECTED`](_autosummary/xa.revive.html.md#xa.revive.CONNECTED)             | Remote Control is up.                                                                                                                                          |
| [`RECONNECTING`](_autosummary/xa.revive.html.md#xa.revive.RECONNECTING)          | Remote Control is retrying right now.                                                                                                                          |
| [`NEEDS_HUMAN`](_autosummary/xa.revive.html.md#xa.revive.NEEDS_HUMAN)           | Blocked on a human — an open permission dialog, or anything else that is eating keystrokes.                                                                    |
| [`NEEDS_LOGIN`](_autosummary/xa.revive.html.md#xa.revive.NEEDS_LOGIN)           | claude on this host is logged out.                                                                                                                             |
| [`NEEDS_TRUST`](_autosummary/xa.revive.html.md#xa.revive.NEEDS_TRUST)           | the workspace-trust prompt is open.                                                                                                                            |
| [`API_STALLED`](_autosummary/xa.revive.html.md#xa.revive.API_STALLED)           | The API call is wedged.                                                                                                                                        |
| [`SERVER_MODE`](_autosummary/xa.revive.html.md#xa.revive.SERVER_MODE)           | A `claude remote-control` *server* died.                                                                                                                       |
| [`RECONNECTABLE`](_autosummary/xa.revive.html.md#xa.revive.RECONNECTABLE)         | Dropped and recoverable — the one verdict [`revive()`](_autosummary/xa.revive.html.md#xa.revive.revive) acts on.                                                   |
| [`UNKNOWN`](_autosummary/xa.revive.html.md#xa.revive.UNKNOWN)               | The pane says nothing either way.                                                                                                                              |
| [`VERDICTS`](_autosummary/xa.revive.html.md#xa.revive.VERDICTS)              | Every verdict [`DEFAULT_RULES`](_autosummary/xa.revive.html.md#xa.revive.DEFAULT_RULES) can produce.                                                                      |
| [`BLOCKED_ON_HUMAN`](_autosummary/xa.revive.html.md#xa.revive.BLOCKED_ON_HUMAN)      | Verdicts that mean "a human must act on the host".                                                                                                             |
| [`ACTIONABLE`](_autosummary/xa.revive.html.md#xa.revive.ACTIONABLE)            | Verdicts [`revive()`](_autosummary/xa.revive.html.md#xa.revive.revive) will send `/remote-control` to.                                                             |
| [`MARKERS`](_autosummary/xa.revive.html.md#xa.revive.MARKERS)               | Substring markers per verdict.                                                                                                                                 |
| [`RC_PILL`](_autosummary/xa.revive.html.md#xa.revive.RC_PILL)               | The footer *pill*.                                                                                                                                             |
| [`DEFAULT_RULES`](_autosummary/xa.revive.html.md#xa.revive.DEFAULT_RULES)         | Ordered `(verdict, predicate)` pairs; **first match wins**.                                                                                                    |
| [`PROMPT_LINE`](_autosummary/xa.revive.html.md#xa.revive.PROMPT_LINE)           | the prompt glyph, then U+00A0, then whatever the user has typed. <br/><br/>```<br/>``<br/>```<br/><br/>> \`\` is the plain-ASCII rendering some terminals get. |
| [`TYPEABLE_STATUS`](_autosummary/xa.revive.html.md#xa.revive.TYPEABLE_STATUS)       | Statuses in which a keystroke is a *command*.                                                                                                                  |
| [`DFLT_MIN_INTERVAL_SEC`](_autosummary/xa.revive.html.md#xa.revive.DFLT_MIN_INTERVAL_SEC) | Long enough that a cron tick can never turn into a keystroke storm, short enough that a real network change is followed within one settling period.            |
| [`RECONNECT_KEYS`](_autosummary/xa.revive.html.md#xa.revive.RECONNECT_KEYS)        | The keystrokes that reconnect a REPL session.                                                                                                                  |
| [`REPORT_ORDER`](_autosummary/xa.revive.html.md#xa.revive.REPORT_ORDER)          | Verdicts worth a human's eye, in the order a report should present them.                                                                                       |

### Functions

| [`classify`](_autosummary/xa.revive.html.md#xa.revive.classify)(probe, \*[, rules])                      | Return the verdict for `probe`.                                                                              |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
| [`evidence_for`](_autosummary/xa.revive.html.md#xa.revive.evidence_for)(probe, verdict, \*[, rules])         | The marker that earned `verdict`, for a report a human has to trust.                                         |
| [`format_actions`](_autosummary/xa.revive.html.md#xa.revive.format_actions)(actions)                           | One line per pane, most actionable first.                                                                    |
| [`has_live_claude`](_autosummary/xa.revive.html.md#xa.revive.has_live_claude)(ref)                              | Default relevance test: the recorded pid is a *claude* that still exists.                                    |
| [`hint_for`](_autosummary/xa.revive.html.md#xa.revive.hint_for)(verdict, \*[, tmux_name])                | Human fix-it line for a verdict, or `None` when there is nothing to say.                                     |
| [`local_panes`](_autosummary/xa.revive.html.md#xa.revive.local_panes)(\*[, claude_home])                    | Yield one [`PaneRef`](_autosummary/xa.revive.html.md#xa.revive.PaneRef) per live claude that records a tmux pane. |
| [`prompt_is_clear`](_autosummary/xa.revive.html.md#xa.revive.prompt_is_clear)(tail)                             | True when the pane's input line is empty and safe to type into.                                              |
| [`refusal_to_type`](_autosummary/xa.revive.html.md#xa.revive.refusal_to_type)(state)                            | Why typing into `state` would be unsafe, or `None` if it is fine.                                            |
| [`restart_server_mode`](_autosummary/xa.revive.html.md#xa.revive.restart_server_mode)(state, \*[, cwd, apply, ...]) | Restart a died `claude remote-control` server in its own directory.                                          |
| [`revive`](_autosummary/xa.revive.html.md#xa.revive.revive)(\*[, panes, apply, ...])                   | Reconnect every dropped pane.                                                                                |
| [`server_mode_command`](_autosummary/xa.revive.html.md#xa.revive.server_mode_command)(cwd)                          | Keystrokes that restart a Remote Control *server* in `cwd`.                                                  |

### Classes

| [`PaneRef`](_autosummary/xa.revive.html.md#xa.revive.PaneRef)(target[, host, claude_pid, cwd, ...])     | Identity of one candidate pane — everything known *without* reading it.   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`PaneState`](_autosummary/xa.revive.html.md#xa.revive.PaneState)(ref, tail, verdict[, evidence])         | A pane, as read and judged.                                               |
| [`Probe`](_autosummary/xa.revive.html.md#xa.revive.Probe)(text[, ref])                                | The complete, pure input to a classification rule.                        |
| [`RateGuard`](_autosummary/xa.revive.html.md#xa.revive.RateGuard)(\*[, store, min_interval_sec, clock])   | Refuses to touch the same pane twice inside `min_interval_sec`.           |
| [`ReviveAction`](_autosummary/xa.revive.html.md#xa.revive.ReviveAction)(target, verdict[, keys, sent, ...])  | What was (or would be) done to one pane, and why.                         |
| [`SessionPanes`](_autosummary/xa.revive.html.md#xa.revive.SessionPanes)(\*[, panes, is_relevant, tail, ...]) | Live view of the panes worth judging: `{tmux target: PaneState}`.         |

### xa.revive.ACTIONABLE *= frozenset({'reconnectable'})*

Verdicts [`revive()`](_autosummary/xa.revive.html.md#xa.revive.revive) will send `/remote-control` to. `HELD_ELSEWHERE`
is deliberately absent — it is reachable only via `include_held_elsewhere`.

### xa.revive.API_STALLED *= 'api_stalled'*

The API call is wedged. Reported, never acted on (the fix is a new prompt).

### xa.revive.BLOCKED_ON_HUMAN *= frozenset({'needs_human', 'needs_login', 'needs_trust'})*

Verdicts that mean “a human must act on the host”. All are blocked on a
person, none are actionable by [`revive()`](_autosummary/xa.revive.html.md#xa.revive.revive), and each has a fix-it line
in [`hint_for()`](_autosummary/xa.revive.html.md#xa.revive.hint_for).

### xa.revive.CONNECTED *= 'connected'*

Remote Control is up. Nothing to do.

### xa.revive.DEFAULT_RULES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[Probe](_autosummary/xa.revive.html.md#xa.revive.Probe)], [bool](https://docs.python.org/3/builtins/functions.html#bool)]], ...]* *= (('no_claude', <function \_no_claude>), ('held_elsewhere', <function \_any_of.<locals>._pred>), ('reconnecting', <function \_any_of.<locals>._pred>), ('connected', <function \_pill_present>), ('connected', <function \_any_of.<locals>._pred>), ('needs_login', <function \_any_of.<locals>._pred>), ('needs_trust', <function \_any_of.<locals>._pred>), ('needs_human', <function \_any_of.<locals>._pred>), ('api_stalled', <function \_any_of.<locals>._pred>), ('server_mode', <function \_any_of.<locals>._pred>), ('reconnectable', <function \_any_of.<locals>._pred>), ('reconnectable', <function \_bridge_down>), ('unknown', <function \_always>))*

Ordered `(verdict, predicate)` pairs; **first match wins**.

The order is the policy, not a style choice, and two things decide it.

*Safety*: `NO_CLAUDE` leads, so a dead session’s scrollback can never be
read as live state — the pane this was written against still showed a
permission dialog and a disconnect notice above a shell prompt, all true
once and none true now. `HELD_ELSEWHERE` precedes every reconnect
verdict, per the rule that a session picked up on another device is never
taken back without asking.

*Freshness*: the pill rules are **live** state and the phrase rules are
**history**. A session that dropped and recovered still carries
“Remote Control disconnected” in its scrollback for as long as it stays on
screen, so a text rule placed above the pill would report a healthy
session as broken until the line scrolled away.

### xa.revive.DFLT_MIN_INTERVAL_SEC *= 600.0*

Long enough that a cron tick can never turn into a keystroke storm, short
enough that a real network change is followed within one settling period.

### xa.revive.HELD_ELSEWHERE *= 'held_elsewhere'*

Taken over or ended from another device. Never reconnect without opt-in.

### xa.revive.MARKERS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]]* *= {'api_stalled': ('api error', 'retrying in ', 'overloaded'), 'connected': ('remote control active',), 'held_elsewhere': ('ended or archived from another device', 'no longer the active worker for the session', 'already has remote control for this conversation', 'run /remote-control to move it to this terminal'), 'needs_human': ('do you want to proceed?', 'esc to cancel'), 'needs_login': ('run /login', 'please log in', 'select login method', 'oauth token has expired', 'invalid api key', 'unknown command: /remote-control'), 'needs_trust': ('trust this folder', 'do you trust the files'), 'reconnectable': ('remote control disconnected', 'remote control not started here', 'run /remote-control to retry', '/remote-control is no longer active', '/rc failed'), 'reconnecting': ('/rc reconnecting',), 'server_mode': ('re-run \`claude remote-control\` to try again',)}*

Substring markers per verdict. Lowercase; matched against a lowered tail.

### xa.revive.NEEDS_HUMAN *= 'needs_human'*

Blocked on a human — an open permission dialog, or anything else that is
eating keystrokes. See [`NEEDS_LOGIN`](_autosummary/xa.revive.html.md#xa.revive.NEEDS_LOGIN) / [`NEEDS_TRUST`](_autosummary/xa.revive.html.md#xa.revive.NEEDS_TRUST) for the
two causes specific enough to tell the user how to fix.

### xa.revive.NEEDS_LOGIN *= 'needs_login'*

claude on this host is logged out.
Login state is per-machine, so one `/login` fixes every session on it.

* **Type:**
  Blocked on a human, specifically

### xa.revive.NEEDS_TRUST *= 'needs_trust'*

the workspace-trust prompt is open.

* **Type:**
  Blocked on a human, specifically

### xa.revive.NO_CLAUDE *= 'no_claude'*

No claude process behind the pane — a shell prompt, or a session that died.

### xa.revive.PROMPT_LINE *= re.compile('(?m)^(?:❯|>)[\\xa0 ](.\*)$', re.MULTILINE)*

the prompt glyph, then U+00A0, then whatever the user
has typed. 

```
``
```

> \`\` is the plain-ASCII rendering some terminals get.

* **Type:**
  The TUI’s input line

### *class* xa.revive.PaneRef(target, host='local', claude_pid=None, cwd=None, session_id=None, bridge_session_id=None, name=None, status=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Identity of one candidate pane — everything known *without* reading it.

`target` is a tmux target: a full pane ref (`session:@window.%pane`,
which is what the ephemeral session file records) or a bare session name.

#### status *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)* *= None*

`idle` / `busy` / `waiting` as the session last reported it.
`None` on a claude too old to record one.

### *class* xa.revive.PaneState(ref, tail, verdict, evidence=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A pane, as read and judged. The value type of [`SessionPanes`](_autosummary/xa.revive.html.md#xa.revive.SessionPanes).

### *class* xa.revive.Probe(text, ref=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The complete, pure input to a classification rule.

`text` is the pane tail, lowercased once so every rule can use a plain
substring test.

### xa.revive.RC_PILL *= re.compile('(?m)^.\*(?<![\\\\w/])/rc(?: active)?\\\\s\*$', re.MULTILINE)*

The footer *pill*. Present while Remote Control is connected — `/rc` on
its own right-aligned line, or `/rc active` in verbose mode — and gone
the moment it is not. Anchored to a line end because a bare `"/rc" in
text` would also match a file path ending in `src/rc`.

The direction matters and is easy to get backwards: the pill is evidence
of *health*. Verified by disconnecting a live session and watching the
line vanish (claude 2.1.251).

### xa.revive.RECONNECTABLE *= 'reconnectable'*

Dropped and recoverable — the one verdict [`revive()`](_autosummary/xa.revive.html.md#xa.revive.revive) acts on.

### xa.revive.RECONNECTING *= 'reconnecting'*

Remote Control is retrying right now. Leave it alone; it may well succeed.

### xa.revive.RECONNECT_KEYS *= ('/remote-control', 'Enter')*

The keystrokes that reconnect a REPL session. `Enter` submits.

### xa.revive.REPORT_ORDER *= ('reconnectable', 'server_mode', 'held_elsewhere', 'needs_human', 'api_stalled', 'reconnecting', 'no_claude', 'unknown', 'connected')*

Verdicts worth a human’s eye, in the order a report should present them.

### *class* xa.revive.RateGuard(\*, store=None, min_interval_sec=600.0, clock=<built-in function time>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Refuses to touch the same pane twice inside `min_interval_sec`.

Without this a cron tick, a hook and an impatient human can each send
`/remote-control` into the same pane seconds apart. Backed by any
`{key: bytes}` mapping — the default is xa’s on-disk
[`FileStore`](_autosummary/xa.store.html.md#xa.store.FileStore), so the interval survives process exit,
which is the entire point.

```pycon
>>> clock = iter([100.0, 200.0, 800.0]).__next__
>>> guard = RateGuard(store={}, min_interval_sec=600.0, clock=clock)
>>> guard.allow('work:@1.%1')     # never attempted
True
>>> guard.record('work:@1.%1')    # stamped at t=100
>>> guard.allow('work:@1.%1')     # t=200 — 100s later, still held off
False
>>> guard.allow('work:@1.%1')     # t=800 — 700s later, released
True
```

#### wait_remaining(target)

Seconds left before `target` may be touched again (0.0 if now).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### *class* xa.revive.ReviveAction(target, verdict, keys=(), sent=False, skipped=None, evidence=None, cwd=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What was (or would be) done to one pane, and why.

#### *property* would_send *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when only `apply=False` stood in the way.

### xa.revive.SERVER_MODE *= 'server_mode'*

A `claude remote-control` *server* died. See [`restart_server_mode()`](_autosummary/xa.revive.html.md#xa.revive.restart_server_mode).

### *class* xa.revive.SessionPanes(\*, panes=<function local_panes>, is_relevant=<function has_live_claude>, tail=<function \_capture>, rules=(('no_claude', <function \_no_claude>), ('held_elsewhere', <function \_any_of.<locals>._pred>), ('reconnecting', <function \_any_of.<locals>._pred>), ('connected', <function \_pill_present>), ('connected', <function \_any_of.<locals>._pred>), ('needs_login', <function \_any_of.<locals>._pred>), ('needs_trust', <function \_any_of.<locals>._pred>), ('needs_human', <function \_any_of.<locals>._pred>), ('api_stalled', <function \_any_of.<locals>._pred>), ('server_mode', <function \_any_of.<locals>._pred>), ('reconnectable', <function \_any_of.<locals>._pred>), ('reconnectable', <function \_bridge_down>), ('unknown', <function \_always>)))

Bases: [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)

Live view of the panes worth judging: `{tmux target: PaneState}`.

Every source of truth is injected, which is what lets the whole
classification path be tested without a tmux server:

- `panes` — callable returning [`PaneRef`](_autosummary/xa.revive.html.md#xa.revive.PaneRef) objects (default:
  [`local_panes()`](_autosummary/xa.revive.html.md#xa.revive.local_panes));
- `is_relevant` — per-ref filter (default: [`has_live_claude()`](_autosummary/xa.revive.html.md#xa.revive.has_live_claude));
- `tail` — callable reading a ref’s pane text (default: tmux capture);
- `rules` — the classification table (default: [`DEFAULT_RULES`](_autosummary/xa.revive.html.md#xa.revive.DEFAULT_RULES)).

Pane text is read lazily and cached, so building the mapping costs
nothing and iterating it costs one capture per pane.

```pycon
>>> refs = [PaneRef(target='w:@1.%1', claude_pid=7, bridge_session_id='s_1')]
>>> panes = SessionPanes(
...     panes=lambda: refs,
...     is_relevant=lambda ref: True,
...     tail=lambda ref: 'Remote Control disconnected',
... )
>>> list(panes)
['w:@1.%1']
>>> panes['w:@1.%1'].verdict
'reconnectable'
>>> panes.by_verdict()['reconnectable']
['w:@1.%1']
```

#### by_verdict()

`{verdict: [target, ...]}` — reads every pane.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

#### refresh()

Drop every cached ref and pane read. Returns self, for chaining.

* **Return type:**
  [`SessionPanes`](_autosummary/xa.revive.html.md#xa.revive.SessionPanes)

### xa.revive.TYPEABLE_STATUS *= frozenset({'idle', None})*

Statuses in which a keystroke is a *command*. Anything else — `busy`,
`waiting` — means the session is mid-turn or holding a dialog, and typed
text would be queued into it. `None` is a claude too old to say.

### xa.revive.UNKNOWN *= 'unknown'*

The pane says nothing either way. Never acted on.

### xa.revive.VERDICTS *= frozenset({'api_stalled', 'connected', 'held_elsewhere', 'needs_human', 'needs_login', 'needs_trust', 'no_claude', 'reconnectable', 'reconnecting', 'server_mode', 'unknown'})*

Every verdict [`DEFAULT_RULES`](_autosummary/xa.revive.html.md#xa.revive.DEFAULT_RULES) can produce. Custom rules may add more.

### xa.revive.classify(probe, \*, rules=(('no_claude', <function \_no_claude>), ('held_elsewhere', <function \_any_of.<locals>._pred>), ('reconnecting', <function \_any_of.<locals>._pred>), ('connected', <function \_pill_present>), ('connected', <function \_any_of.<locals>._pred>), ('needs_login', <function \_any_of.<locals>._pred>), ('needs_trust', <function \_any_of.<locals>._pred>), ('needs_human', <function \_any_of.<locals>._pred>), ('api_stalled', <function \_any_of.<locals>._pred>), ('server_mode', <function \_any_of.<locals>._pred>), ('reconnectable', <function \_any_of.<locals>._pred>), ('reconnectable', <function \_bridge_down>), ('unknown', <function \_always>)))

Return the verdict for `probe`. Pure — no I/O, no tmux, no clock.

A session whose footer pill is gone and whose session file records no
bridge id has dropped:

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> dropped = PaneRef(target='w:@1.%1', claude_pid=42)
>>> classify(Probe(text='conversation text\n❯ ', ref=dropped))
'reconnectable'
```

One still carrying the pill has not, whatever its scrollback remembers:

```pycon
>>> live = PaneRef(target='w:@1.%1', claude_pid=42, bridge_session_id='s')
>>> classify(Probe(text='remote control disconnected\n     /rc', ref=live))
'connected'
```

A session someone picked up on their phone is never reconnectable, even
though the pane also says it disconnected:

```pycon
>>> taken = 'remote control disconnected\nended or archived from another device'
>>> classify(Probe(text=taken, ref=dropped))
'held_elsewhere'
```

Neither is one sitting on a permission dialog:

```pycon
>>> classify(Probe(text='/rc failed\ndo you want to proceed?', ref=live))
'needs_human'
```

And a healthy, quiet, connected pane is simply left alone:

```pycon
>>> classify(Probe(text='just some conversation\n  /rc', ref=live))
'connected'
```

### xa.revive.evidence_for(probe, verdict, \*, rules=(('no_claude', <function \_no_claude>), ('held_elsewhere', <function \_any_of.<locals>._pred>), ('reconnecting', <function \_any_of.<locals>._pred>), ('connected', <function \_pill_present>), ('connected', <function \_any_of.<locals>._pred>), ('needs_login', <function \_any_of.<locals>._pred>), ('needs_trust', <function \_any_of.<locals>._pred>), ('needs_human', <function \_any_of.<locals>._pred>), ('api_stalled', <function \_any_of.<locals>._pred>), ('server_mode', <function \_any_of.<locals>._pred>), ('reconnectable', <function \_any_of.<locals>._pred>), ('reconnectable', <function \_bridge_down>), ('unknown', <function \_always>)))

The marker that earned `verdict`, for a report a human has to trust.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> ref = PaneRef(target='w:@1.%1', claude_pid=1)
>>> evidence_for(Probe(text='/rc failed', ref=ref), 'reconnectable')
'/rc failed'
```

### xa.revive.format_actions(actions)

One line per pane, most actionable first.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> print(format_actions([
...     ReviveAction('a:@1.%1', RECONNECTABLE, keys=RECONNECT_KEYS,
...                  skipped='dry-run', evidence='/rc failed'),
...     ReviveAction('b:@1.%1', CONNECTED),
... ]))
reconnectable  a:@1.%1  would send /remote-control  [/rc failed]
connected      b:@1.%1
```

### xa.revive.has_live_claude(ref)

Default relevance test: the recorded pid is a *claude* that still exists.

Identity, not just liveness — a stale ephemeral file whose pid got
recycled must never direct keystrokes at an unrelated process.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### xa.revive.hint_for(verdict, , tmux_name=None)

Human fix-it line for a verdict, or `None` when there is nothing to say.

SSOT for the wording shown by the CLI, the web UI and the HTTP API, so
the three can’t drift. Only the verdicts a person can actually clear get
a line; `connected`/`unknown`/`reconnectable` return `None`
(nothing to do, nothing known, or xa’s own job respectively).

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> hint_for(CONNECTED) is None
True
>>> hint_for(NEEDS_TRUST, tmux_name='sess').startswith('Claude is waiting')
True
```

### xa.revive.local_panes(, claude_home=PosixPath('/home/runner/.claude'))

Yield one [`PaneRef`](_autosummary/xa.revive.html.md#xa.revive.PaneRef) per live claude that records a tmux pane.

Derived from the ephemeral session files rather than from
`tmux list-panes`: each file names its own pane, so no heuristic has
to decide whether a pane holds a claude. A session not under tmux (every
session on a machine that runs claude in terminal tabs) simply has no
`tmux` key and is skipped — there is no pane to send keys to.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`PaneRef`](_autosummary/xa.revive.html.md#xa.revive.PaneRef)]

### xa.revive.prompt_is_clear(tail)

True when the pane’s input line is empty and safe to type into.

This is the difference between reconnecting a session and destroying the
instruction its owner left half-typed: `send-keys` appends to whatever
is already in the buffer, so `/remote-control` sent at a pane whose
prompt reads `yes, port it into the repo` submits
`yes, port it into the repo/remote-control`.

Conservative by construction — a pane whose prompt line cannot be found
is reported as not clear, because “I could not see the buffer” and “the
buffer is empty” must never collapse into the same answer.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> prompt_is_clear('---\n❯ \n---')
True
>>> prompt_is_clear('❯ yes, port it into the repo')
False
>>> prompt_is_clear('no prompt line here at all')
False
```

### xa.revive.refusal_to_type(state)

Why typing into `state` would be unsafe, or `None` if it is fine.

Two independent gates, because each catches what the other misses: the
reported status catches a session mid-turn whose buffer happens to be
empty, and the buffer check catches an idle session holding text the
status knows nothing about.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> ref = PaneRef(target='t', claude_pid=1, status='idle')
>>> refusal_to_type(PaneState(ref=ref, tail='❯ ', verdict=RECONNECTABLE))
>>> busy = replace(ref, status='busy')
>>> refusal_to_type(PaneState(ref=busy, tail='❯ ', verdict=RECONNECTABLE))
'session is busy'
```

### xa.revive.restart_server_mode(state, , cwd=None, apply=False, rate_guard=None, send=None, resolve_cwd=None)

Restart a died `claude remote-control` server in its own directory.

The directory is **not** recorded by this package. It is read back from
what already knows it — the pane’s own working directory via tmux, or
the ephemeral session file when a claude is still alive — and the
session identity is left to `claude remote-control -c`, which keeps
its own per-directory record. Adding a fourth place to write it down
would only create something to drift.

Refuses whenever a live claude still owns the pane: this sends a *shell*
command, and a running TUI would take it as prompt text.

* **Return type:**
  [`ReviveAction`](_autosummary/xa.revive.html.md#xa.revive.ReviveAction)

```pycon
>>> ref = PaneRef(target='srv:@1.%1', cwd='/srv/app')
>>> state = PaneState(ref=ref, tail='', verdict=SERVER_MODE)
>>> keyed = []
>>> action = restart_server_mode(
...     state,
...     apply=True,
...     rate_guard=RateGuard(store={}, clock=lambda: 0.0),
...     send=lambda target, *keys: keyed.append((target, keys)),
... )
>>> action.sent, keyed
(True, [('srv:@1.%1', ('cd /srv/app && claude remote-control -c', 'Enter'))])
```

```pycon
>>> live = PaneState(ref=replace(ref, claude_pid=9), tail='', verdict=SERVER_MODE)
>>> restart_server_mode(live, apply=True).skipped
'a claude still owns this pane — keystrokes would land in its prompt'
```

### xa.revive.revive(, panes=None, apply=False, include_held_elsewhere=False, rate_guard=None, send=None, actionable=frozenset({'reconnectable'}))

Reconnect every dropped pane. \*\*Dry run unless `apply=True`.\*\*

Returns one [`ReviveAction`](_autosummary/xa.revive.html.md#xa.revive.ReviveAction) per pane examined — including the ones
left alone, each carrying the reason, because “nothing happened” is only
useful next to what was looked at.

`include_held_elsewhere=True` opts into reconnecting sessions that were
taken over from another device. That steals the session back from
whatever device holds it; it is off by default and should stay a
deliberate keystroke.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`ReviveAction`](_autosummary/xa.revive.html.md#xa.revive.ReviveAction)]

```pycon
>>> refs = [
...     PaneRef(target='a:@1.%1', claude_pid=1),
...     PaneRef(target='b:@1.%1', claude_pid=2, bridge_session_id='s'),
... ]
>>> tails = {'a:@1.%1': '/rc failed\n❯ ', 'b:@1.%1': 'all quiet\n❯ \n   /rc'}
>>> panes = SessionPanes(
...     panes=lambda: refs,
...     is_relevant=lambda ref: True,
...     tail=lambda ref: tails[ref.target],
... )
>>> sent = []
>>> actions = revive(
...     panes=panes,
...     apply=True,
...     rate_guard=RateGuard(store={}, clock=lambda: 0.0),
...     send=lambda target, *keys: sent.append((target, keys)),
... )
>>> [(a.target, a.verdict, a.sent) for a in actions]
[('a:@1.%1', 'reconnectable', True), ('b:@1.%1', 'connected', False)]
>>> sent
[('a:@1.%1', ('/remote-control', 'Enter'))]
```

### xa.revive.server_mode_command(cwd)

Keystrokes that restart a Remote Control *server* in `cwd`.

`claude remote-control -c` reattaches the session the command last
recorded **for that directory** (within ~4h), which is why the directory
— and nothing else — is what a restart needs to know.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> server_mode_command('/root/py/proj')
('cd /root/py/proj && claude remote-control -c', 'Enter')
>>> server_mode_command(None)
('claude remote-control -c', 'Enter')
```


# _autosummary/xa.service.html.md

# xa.service

FastAPI service for `xa`.

`build_api(...)` returns a mountable `FastAPI` app that reimplements
the `edualc` route surface on top of the `xa` primitives. The service
is deliberately a function (not a module that imports-and-runs) so it can
be embedded into larger hosts (enlace / tw_platform) or launched
standalone via `xa serve`.

FastAPI is an optional dependency. Import `xa.service` only when you
have installed `xa[service]` (pulls in `fastapi` + `uvicorn`).

### Module Attributes

| [`RC_ENABLE_TIMEOUT_SEC`](_autosummary/xa.service.html.md#xa.service.RC_ENABLE_TIMEOUT_SEC)   | How long POST /sessions/{id}/remote-control waits for claude to write its bridge id after the command is sent.   |
|--------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|

### Functions

| [`allow_all`](_autosummary/xa.service.html.md#xa.service.allow_all)()                              | No-op auth dependency.                                            |
|-------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`build_api`](_autosummary/xa.service.html.md#xa.service.build_api)(\*[, auth, events_store, ...]) | Return a `FastAPI` app exposing `xa`'s session + archive surface. |
| [`make_basic_auth`](_autosummary/xa.service.html.md#xa.service.make_basic_auth)(username, password)      | Build a FastAPI dependency that enforces HTTP Basic auth.         |

### Classes

| [`Captcha`](_autosummary/xa.service.html.md#xa.service.Captcha)(\*, key[, ttl_sec])   | Stateless 4-letter captcha with an HMAC-signed token.   |
|--------------------------------------------------------------------------------|---------------------------------------------------------|

### *class* xa.service.Captcha(, key, ttl_sec=120)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Stateless 4-letter captcha with an HMAC-signed token.

The signing key never leaves the server. Tokens are
`b64(CHALLENGE.EXPIRY).SIG` where SIG = HMAC-SHA256(key, payload).
Multi-worker safe; no server-side state. Pass one instance of this
class to [`build_api()`](_autosummary/xa.service.html.md#xa.service.build_api) to enable captcha-gated deletes.

#### issue()

Return `(token, challenge, ttl_sec)`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### xa.service.RC_ENABLE_TIMEOUT_SEC *= 15.0*

How long POST /sessions/{id}/remote-control waits for claude to write
its bridge id after the command is sent. The TUI acks well before the
session file is rewritten, so a bare read would always miss.

### xa.service.allow_all()

No-op auth dependency. Useful when the service is behind an external
auth layer (enlace, reverse-proxy mTLS) that already gated the request.

FastAPI inspects the signature to decide what to inject; keep this
nullary so it isn’t treated as query-parameter binding.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### xa.service.build_api(\*, auth=<function allow_all>, events_store=None, pane_store=None, captcha=None, claude_home=PosixPath('/home/runner/.claude'), claude_bin='claude', session_prefix='xa-', title='xa', version='0.1', include_webui=False, default_folder=None)

Return a `FastAPI` app exposing `xa`’s session + archive surface.

The caller composes this app into whatever process they have: mount
under `/api/xa/` inside enlace, or run standalone via `uvicorn`.

### xa.service.make_basic_auth(username, password)

Build a FastAPI dependency that enforces HTTP Basic auth.

Returns a callable suitable for `Depends(...)`. Uses
`secrets.compare_digest` to avoid timing leaks.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]


# _autosummary/xa.sessions.html.md

# xa.sessions

The unified `Session` domain model and multi-host discovery.

This layer is the single thing CLI / HTTP / UI renderers iterate over.
Discovery and actions are delegated to [`xa.hosts`](_autosummary/xa.hosts.html.md#module-xa.hosts); this module
composes them into a host-agnostic API.

### Functions

| [`get_session`](_autosummary/xa.sessions.html.md#xa.sessions.get_session)(session_id, \*[, hosts, claude_home])   | Find one session by full ID or unique prefix.                         |
|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`iter_local_sessions`](_autosummary/xa.sessions.html.md#xa.sessions.iter_local_sessions)(\*[, claude_home, ...])         | Legacy local-only iterator.                                           |
| [`kill_session`](_autosummary/xa.sessions.html.md#xa.sessions.kill_session)(session, \*[, hosts])                  | Kill a live `Session`, scoped to what actually belongs to it.         |
| [`list_sessions`](_autosummary/xa.sessions.html.md#xa.sessions.list_sessions)(\*[, hosts, project, ...])            | Return sessions sorted by recency (live first, then newest-modified). |
| [`resume`](_autosummary/xa.sessions.html.md#xa.sessions.resume)(session, \*[, cwd, name, hosts])             | Resume a transcript / archived session on its originating host.       |

### Classes

| [`Session`](_autosummary/xa.sessions.html.md#xa.sessions.Session)(id, claude_session_id, ...[, ...])   | Canonical session record — valid across hosts and states.   |
|-----------------------------------------------------------------------------------------------|-------------------------------------------------------------|

### *class* xa.sessions.Session(id, claude_session_id, bridge_session_id, host, cwd, project_slug, state, live_pid, tmux_name, name, summary, first_user_message, turn_count, forked_from, created, modified, url, url_source, transcript_path, pre_first_turn=False, attention=None, attention_hint=None, tmux_pane=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Canonical session record — valid across hosts and states.

Fields that don’t apply to a given state are `None`. Discovery
functions return fresh instances; callers should treat it as
immutable.

### xa.sessions.get_session(session_id, , hosts=None, claude_home=None)

Find one session by full ID or unique prefix.

Raises `LookupError` if the prefix is ambiguous.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Session`](_autosummary/xa.sessions.html.md#xa.sessions.Session)]

### xa.sessions.iter_local_sessions(, claude_home=PosixPath('/home/runner/.claude'), project_slug=None, include_live=True, tmux_bin=None)

Legacy local-only iterator.

Retained for backward compatibility and because some tests construct
sessions directly from a fake `~/.claude/`. Wraps a fresh
`LocalHost`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Session`](_autosummary/xa.sessions.html.md#xa.sessions.Session)]

### xa.sessions.kill_session(session, , hosts=None)

Kill a live `Session`, scoped to what actually belongs to it.

Kills the backing tmux session only when it is *dedicated* to this
claude (single pane, claude verified in it) — a claude living in one
window of a shared multi-window workspace must not take the whole
workspace down, so there we SIGTERM the identity-verified pid
instead. Remote hosts keep the name-kill contract (their server
applies its own scoping). Returns a short description of what was
killed (e.g. `"tmux:mysess"` / `"pid:1234"`).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### xa.sessions.list_sessions(, hosts=None, project=None, include_forks=True, include_live=True, state=None, limit=None, claude_home=None, tmux_bin=None)

Return sessions sorted by recency (live first, then newest-modified).

When `hosts` is `None` we default to a single `LocalHost`.
If `claude_home` or `tmux_bin` are given, they’re forwarded into
that default `LocalHost` — handy for fixture-based tests.

Filters:

- `project` — substring match against `cwd` (case-insensitive)
- `include_forks=False` — drop sessions with a `forked_from`
- `include_live=False` — skip live discovery (per host)
- `state` — one of `"live"` / `"archived"` / `"transcript_only"`

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Session`](_autosummary/xa.sessions.html.md#xa.sessions.Session)]

### xa.sessions.resume(session, , cwd=None, name=None, hosts=None, \*\*opts)

Resume a transcript / archived session on its originating host.

* **Return type:**
  [`SpawnResult`](_autosummary/xa.claude_cli.html.md#xa.claude_cli.SpawnResult)


# _autosummary/xa.store.html.md

# xa.store

Minimal key-value and append-log storage.

This is deliberately thin and stdlib-only — enough to back `xa.archive`’s
event log and per-session pane captures without pulling in `dol`. Two
types:

- `JsonLinesStore` — append-only JSONL. Iterable, multi-writer safe on
  POSIX (one line == one atomic `write`), no random access.
- `FileStore` — dict-like view over a directory of flat files. Keys
  must match a strict allowlist regex so callers can’t escape `root`.

Later phases may swap these for `dol` equivalents without changing
the archive API.

### Functions

| [`default_events_store`](_autosummary/xa.store.html.md#xa.store.default_events_store)([state_dir])   | The event log at `<state_dir>/events.jsonl`.                                                                                 |
|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| [`default_pane_store`](_autosummary/xa.store.html.md#xa.store.default_pane_store)([state_dir])     | Per-session pane logs at `<state_dir>/panes/<id>.log`.                                                                       |
| [`default_revive_store`](_autosummary/xa.store.html.md#xa.store.default_revive_store)([state_dir])   | Per-pane last-reconnect stamps for [`xa.revive.RateGuard`](_autosummary/xa.revive.html.md#xa.revive.RateGuard). |

### Classes

| [`FileStore`](_autosummary/xa.store.html.md#xa.store.FileStore)(root, \*[, suffix])   | Directory of files, accessed by key.              |
|----------------------------------------------------------------------------------|---------------------------------------------------|
| [`JsonLinesStore`](_autosummary/xa.store.html.md#xa.store.JsonLinesStore)(path)            | Append-only JSONL file with an `Iterable` reader. |

### *class* xa.store.FileStore(root, , suffix='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Directory of files, accessed by key.

Values are `bytes`. Keys must match `[A-Za-z0-9_.-]+` — this
rejects path traversal attempts like `../etc/passwd`.

#### path_for(key)

Public path accessor (for callers that need to hand the path to tmux).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### *class* xa.store.JsonLinesStore(path)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Append-only JSONL file with an `Iterable` reader.

```pycon
>>> import tempfile, pathlib
>>> with tempfile.TemporaryDirectory() as td:
...     s = JsonLinesStore(pathlib.Path(td) / 'log.jsonl')
...     s.append({'a': 1})
...     s.append({'b': 2})
...     list(s) == [{'a': 1}, {'b': 2}]
True
```

### xa.store.default_events_store(state_dir=PosixPath('/home/runner/.xa'))

The event log at `<state_dir>/events.jsonl`.

* **Return type:**
  [`JsonLinesStore`](_autosummary/xa.store.html.md#xa.store.JsonLinesStore)

### xa.store.default_pane_store(state_dir=PosixPath('/home/runner/.xa'))

Per-session pane logs at `<state_dir>/panes/<id>.log`.

* **Return type:**
  [`FileStore`](_autosummary/xa.store.html.md#xa.store.FileStore)

### xa.store.default_revive_store(state_dir=PosixPath('/home/runner/.xa'))

Per-pane last-reconnect stamps for [`xa.revive.RateGuard`](_autosummary/xa.revive.html.md#xa.revive.RateGuard).

On disk rather than in memory on purpose: the guard exists to stop a
cron tick, a hook and a human from each sending `/remote-control`
into the same pane seconds apart, and every one of those is a
different process.

* **Return type:**
  [`FileStore`](_autosummary/xa.store.html.md#xa.store.FileStore)


# _autosummary/xa.tmux.html.md

# xa.tmux

Pure tmux wrappers.

No Claude Code knowledge lives here. Every public function takes a
`binary` keyword so callers can override the tmux executable (tests,
cross-platform installs, remote-host bridges).

Key gotchas encoded below:

- `session_target(name)` returns `f"{name}:"` — the trailing colon is
  essential. A bare session name can be silently resolved as a window or
  pane spec and mis-target a different session.
- `list_sessions` returns an empty list (not raises) when the tmux
  server isn’t running; tmux exits non-zero in that case and we absorb it.

### Functions

| [`capture_pane`](_autosummary/xa.tmux.html.md#xa.tmux.capture_pane)(name, \*[, lines, binary])          | Return the last `lines` of the targeted pane, or '' on failure.                |
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`descendants`](_autosummary/xa.tmux.html.md#xa.tmux.descendants)(pid)                                 | All transitive descendant PIDs of `pid`.                                       |
| `kill_session`(name, \*[, binary])                                                                |                                                                                |
| [`list_panes`](_autosummary/xa.tmux.html.md#xa.tmux.list_panes)(\*[, binary])                         | Every pane on the server, or `[]` when no server is running.                   |
| [`list_sessions`](_autosummary/xa.tmux.html.md#xa.tmux.list_sessions)(\*[, binary])                      | Return all live tmux sessions; empty list if server isn't running.             |
| [`new_session`](_autosummary/xa.tmux.html.md#xa.tmux.new_session)(name, \*, command[, binary])         | Create a detached tmux session running `command` as its pane's program.        |
| [`pane_count`](_autosummary/xa.tmux.html.md#xa.tmux.pane_count)(name, \*[, binary])                   | Number of panes (across all windows) in the session; 0 if gone.                |
| [`pane_current_path`](_autosummary/xa.tmux.html.md#xa.tmux.pane_current_path)(name, \*[, binary])            | Working directory of the targeted pane's program, or None.                     |
| [`pane_pid`](_autosummary/xa.tmux.html.md#xa.tmux.pane_pid)(name, \*[, binary])                     | Return the pid of the first pane's program, or None if the session is gone.    |
| [`pane_pids`](_autosummary/xa.tmux.html.md#xa.tmux.pane_pids)(name, \*[, binary])                    | PIDs of every pane program in the session (all windows, all panes).            |
| [`pane_target`](_autosummary/xa.tmux.html.md#xa.tmux.pane_target)(ref)                                 | Target string for pane-scoped commands (capture, send-keys).                   |
| [`pipe_pane_to_file`](_autosummary/xa.tmux.html.md#xa.tmux.pipe_pane_to_file)(name, \*, path[, binary])      | Start streaming the pane's output to `path` (append mode).                     |
| [`proc_comm`](_autosummary/xa.tmux.html.md#xa.tmux.proc_comm)(pid)                                   | Return the `comm` name of a pid (kernel-level process name), '' if unreadable. |
| [`rename_session`](_autosummary/xa.tmux.html.md#xa.tmux.rename_session)(old_name, new_name, \*[, binary]) | Rename a live tmux session.                                                    |
| [`send_keys`](_autosummary/xa.tmux.html.md#xa.tmux.send_keys)(name, \*keys[, binary])                | Send one or more keys/strings to the targeted pane.                            |
| [`session_target`](_autosummary/xa.tmux.html.md#xa.tmux.session_target)(name)                             | Return the canonical target string for a session.                              |

### Classes

| [`TmuxPane`](_autosummary/xa.tmux.html.md#xa.tmux.TmuxPane)(target, pid, current_command, ...)    | Minimal view of one tmux pane, from `list-panes -a`.    |
|-------------------------------------------------------------------------------------------------|---------------------------------------------------------|
| [`TmuxSession`](_autosummary/xa.tmux.html.md#xa.tmux.TmuxSession)(name, created, activity, attached) | Minimal view of one tmux session, from `list-sessions`. |

### *class* xa.tmux.TmuxPane(target, pid, current_command, current_path)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Minimal view of one tmux pane, from `list-panes -a`.

### *class* xa.tmux.TmuxSession(name, created, activity, attached)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Minimal view of one tmux session, from `list-sessions`.

### xa.tmux.capture_pane(name, , lines=200, binary='tmux')

Return the last `lines` of the targeted pane, or ‘’ on failure.

`name` may be a session name (targets its active pane) or a full
pane ref (`session:@w.%p`) for an exact pane.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### xa.tmux.descendants(pid)

All transitive descendant PIDs of `pid`.

Uses `/proc/*/status` where available and `ps` elsewhere (macOS),
so the process-tree walk behaves the same on every platform xa runs
on. Silently tolerates races (processes dying mid-scan) and returns
`[]` when neither source can be read.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

### xa.tmux.list_panes(, binary='tmux')

Every pane on the server, or `[]` when no server is running.

`target` is the full pane ref, the same shape claude records in its
ephemeral session file, so the two views join without parsing.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TmuxPane`](_autosummary/xa.tmux.html.md#xa.tmux.TmuxPane)]

### xa.tmux.list_sessions(, binary='tmux')

Return all live tmux sessions; empty list if server isn’t running.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`TmuxSession`](_autosummary/xa.tmux.html.md#xa.tmux.TmuxSession)]

### xa.tmux.new_session(name, , command, binary='tmux')

Create a detached tmux session running `command` as its pane’s program.

`command` is passed to a shell: the caller is responsible for quoting.
Use `shlex.quote` for untrusted parts.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.tmux.pane_count(name, , binary='tmux')

Number of panes (across all windows) in the session; 0 if gone.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### xa.tmux.pane_current_path(name, , binary='tmux')

Working directory of the targeted pane’s program, or None.

This is where a pane’s shell would run a command, which is what
`claude remote-control -c` keys its per-directory record on.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### xa.tmux.pane_pid(name, , binary='tmux')

Return the pid of the first pane’s program, or None if the session is gone.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

### xa.tmux.pane_pids(name, , binary='tmux')

PIDs of every pane program in the session (all windows, all panes).

Empty list when the session is gone. `name` may be a session name
or a full pane ref — `-s` scopes to the containing session either way.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]

### xa.tmux.pane_target(ref)

Target string for pane-scoped commands (capture, send-keys).

Accepts either a bare session name or a full tmux pane ref
(`session:@window.%pane` — the shape claude records in its ephemeral
session file). A bare name targets the session’s *active* pane, which
may not be the claude pane in a multi-window session — pass the full
ref whenever you have one. Session names in xa match
`[A-Za-z0-9_.-]` so they can never contain a `:`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> pane_target('foo')
'foo:'
>>> pane_target('foo:@1.%2')
'foo:@1.%2'
```

### xa.tmux.pipe_pane_to_file(name, , path, binary='tmux')

Start streaming the pane’s output to `path` (append mode).

Uses `-o` so a duplicate call toggles the pipe off, matching edualc’s
behavior. tmux stops piping automatically when the pane dies.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.tmux.proc_comm(pid)

Return the `comm` name of a pid (kernel-level process name), ‘’ if unreadable.

Linux reads `/proc/<pid>/comm`, which is a bare name (`claude`).
`ps -o comm=` on macOS reports a *path* (`/usr/local/bin/claude`),
so the fallback takes the basename — callers compare against bare
names (see `xa.claude_fs._looks_like_claude()`) and must not have
to care which platform answered.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### xa.tmux.rename_session(old_name, new_name, , binary='tmux')

Rename a live tmux session.

Both names must match the strict `[A-Za-z0-9_.-]{1,48}` pattern
used elsewhere in `xa`; callers should validate before calling.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.tmux.send_keys(name, \*keys, binary='tmux')

Send one or more keys/strings to the targeted pane.

Always pass `"Enter"` for newline. `name` may be a session name
(targets its active pane) or a full pane ref for an exact pane.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### xa.tmux.session_target(name)

Return the canonical target string for a session.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> session_target('foo')
'foo:'
```


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-09-22 13:02 UTC** from commit <a href="https://github.com/thorwhalen/xa/commit/70fba9709483acd9c96fc1120f616657d9faba8a"><code>70fba97</code></a> on branch <code>main</code>, for **xa 0.1.12** (from <code>pyproject.toml</code>).

#### NOTE
Nothing suggests a mismatch: the tree was clean at the commit above, and the documented version is the one on PyPI.

## Source

|                     |                                                                                                                                                      |
|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/thorwhalen/xa/commit/70fba9709483acd9c96fc1120f616657d9faba8a"><code>70fba9709483acd9c96fc1120f616657d9faba8a</code></a> |
| Branch              | <code>main</code>                                                                                                                                    |
| Tags at this commit | <code>0.1.12</code>                                                                                                                                  |
| Working tree        | clean                                                                                                                                                |
| Remote              | <code>https://github.com/thorwhalen/xa</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>thorwhalen/xa</code>                                                                 |
| Run          | <a href="https://github.com/thorwhalen/xa/actions/runs/35730744333">35730744333</a>        |
| Ref          | <code>refs/heads/main</code>                                                               |
| Event commit | <code>2e31bccddf652c5d9b23cc3f96be83ef5ba38f40</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.12  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.14 |

## Configuration as resolved

|               |                                                                  |
|---------------|------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>furo</code>)               |
| accent        | <code>#93342a</code>                                             |
| api_generator | <code>autosummary</code>                                         |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code> |
| agent_outputs | <code>true</code>                                                |
| aggregates    | <code>md</code>                                                  |
| ai_artifacts  | <code>true</code>                                                |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/xa/0.1.12/">0.1.12</a>, the same as the documented version.

## Reproduce

```bash
git clone https://github.com/thorwhalen/xa && cd xa
git checkout 70fba9709483acd9c96fc1120f616657d9faba8a
pip install "epythet==0.2.12"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`xa` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `xa-install`

Use when the user wants to install, configure, provision, secure, or troubleshoot `xa` — the cross-machine Claude Code session manager. Triggers on “install xa”, “set up xa on this server”, “configure xa hosts”, “start xa serve”, “xa from my phone”, “make xa accessible from my phone”, “secure xa”, “xa config”, or any request to use xa for the first time. Also use if the user asks why `xa list` is empty, why a remote host isn’t showing, why they can’t resume a session, or whether their xa deployment is safe.

```bash
gh skill install thorwhalen/xa xa-install --agent claude-code
```

Source: [`xa/data/skills/xa-install`](https://github.com/thorwhalen/xa/tree/HEAD/xa/data/skills/xa-install) (bundled with the pip package).

### `xa-dev`

Use when developing on or diagnosing the `xa` package (Claude Code session manager). Triggers on work under /root/py/proj/t/xa, edits to xa/xa/\*.py, failing tests, or questions about xa’s architecture, layering, or extension points. Not for using xa as an end user — that’s the shipped xa-install skill.

Source: [`.claude/skills/xa-dev`](https://github.com/thorwhalen/xa/tree/HEAD/.claude/skills/xa-dev).

The bundled skills are also on disk after `pip install xa`, under the package’s `data/skills/` directory; link them into an agent without network access with `skill link-skills <that directory>`.

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://thorwhalen.github.io/xa/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`xa.md`](https://thorwhalen.github.io/xa/xa.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://thorwhalen.github.io/xa/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`xa`](_autosummary/xa.html.md#module-xa)   | xa — tools for managing remote Claude Code sessions.   |
|-----------------------------------------------------------------|--------------------------------------------------------|


