---
name: strix
version: "1.7"
updated: "2026-07-04"
description: >
  Drive StrixApp and strixd nodes from an AI coding agent. Here a "node" is a paired strixd
  worker machine (not Node.js). Spawn and steer terminal / PTY sessions over MCP (sessions_spawn
  / read_output / send_input / kill), list and add worker nodes, manage tunnels, and run the
  strix and strixd CLIs. Use it to orchestrate shells and agents across local and remote Strix
  machines, register a CLI with the Strix hub over MCP, or set up and pair a new strixd node.
when_to_use: >
  Load when the user mentions Strix, StrixApp, strixd, or the Strix hub; asks to run, spawn,
  attach, or watch a terminal / PTY session on a strixd worker node; wants to add, pair, or list
  Strix worker nodes or tunnels; asks to register or connect an agent to Strix over MCP; or is
  setting up a strixd node. Do NOT load for generic process spawning, Node.js, SSH, or database
  work that does not involve Strix. Trigger phrases: "spawn a session on <strix node>", "drive
  Strix", "register the Strix MCP server", "pair this node", "run this across my Strix nodes".
---

# Strix - AI Agent Skill

**Version 1.7 - updated 2026-07-04**

StrixApp is a command center for AI coding agents and shells across local and remote machines.
A local hub owns the overlay; you drive it two ways, both backed by the same hub.

## At a glance

- **MCP tools** (19 of them) - structured calls for orchestration. Register once (below), then
  use the drive loop. Best for autonomous, multi-step session work.
- **`strix` CLI** - plain shell commands, zero setup, the hub auto-starts. Best when you already
  have a terminal.

The MCP drive loop is four calls: `sessions_spawn` returns a `uuid`; poll
`sessions_read_output` until `exited`; `sessions_send_input` to steer; `sessions_kill` to stop.
All PTY data is base64url-nopad. For remote work, pass `nodeId` (from `sessions_list_all_nodes`).

---

## Quick start - connect over MCP

The Strix hub must be running (open StrixApp, or run `strix hub status`). It writes two files
at start:

```sh
cat ~/.strix/mcp.port    # actual bound port (default 7843)
cat ~/.strix/mcp.token   # bearer token (format: mcp-stx-<43 base64url>, mode 0600)
```

Endpoint: `http://127.0.0.1:<port>/mcp` - Streamable HTTP, JSON-RPC 2.0. Two auth gates, in
order: (1) `Host` must be loopback (`127.0.0.1` / `localhost`), (2) `Authorization: Bearer
<token>`. Loopback HTTP clients send the correct `Host` automatically.

**One command does everything** - installs this skill into your agent and registers the MCP
server (when the hub is up):

```sh
curl -fsSL https://onestrix.com/register-strix.mjs | node --input-type=module -
```

Idempotent; safe to re-run after token rotation or hub restart. Flags: `--project` (current
project instead of user-global), `--agent <name>` (one agent), `--uninstall` (remove the skill
+ MCP registration from your agents). To wire a CLI by hand instead, see "Manual MCP
registration" below.

---

## MCP drive loop

The standard pattern for running a command and reading its output:

```
1. sessions_spawn({kind: "term", cwd: "/absolute/path", execCmd?: "cmd", nodeId?: "<node-id>"})
   -> SessionView { uuid, shellPid, ... }   <- uuid drives all subsequent calls

2. loop: sessions_read_output({uuid, cursor: nextCursor, mode: "raw"})
   -> { frames: [{cursor, dataBase64, droppedBefore}], nextCursor, exited, exitCode,
        liveness, processAlive }
   -> decode each frame's dataBase64 with base64url-nopad (no trailing '=')
   -> advance cursor to nextCursor; stop looping when exited: true

3. sessions_send_input({uuid, text?: "cmd", dataBase64?, submit?: true, paste?: true})
   -> { sent: N }   <- one of text (UTF-8) OR dataBase64 (raw bytes); submit appends \r

4. sessions_kill({uuid})                     <- terminate early
   -> { killed: true }
```

`execCmd` runs IMMEDIATELY - no trailing newline needed. The hub auto-appends Enter when the
string is missing one, so `execCmd: "ping localhost"` executes right away instead of sitting
typed-but-unsubmitted at the prompt. To prefill the PTY WITHOUT submitting, end the string
with a trailing space or tab (e.g. `"ssh "` awaiting manual input) or leave it empty -
trailing whitespace is the prefill signal and is never auto-submitted. This behavior, and
`sessions_read_output`'s `liveness`/`processAlive` fields, work the same for a remote (`nodeId`
set) session as for a local one - and the same for a remote session started via the `strix`
CLI's `sessions spawn --node`, not just via this tool's own `sessions_spawn`.

### base64url-nopad encoding

All PTY data in `dataBase64` uses base64url WITHOUT padding (`URL_SAFE_NO_PAD`), for both
reading (decode) and sending (encode). Strip any trailing `=`. Standard padded base64 returns
`BadRequest("invalid_base64")`.

### Sending interactive input

Ordinary commands: send `text` with `submit: true` - it appends the Enter for you, no base64:

```
sessions_send_input({uuid, text: "npm test", submit: true})
```

Raw control / navigation keys (Ctrl-C, arrows, Esc, Tab): encode them first with
`sessions_encode_input`, then send the returned `dataBase64`:

```
sessions_encode_input({keys: ["ctrl-c"]})       -> { dataBase64: "Aw", byteLen: 1 }
sessions_send_input({uuid, dataBase64: "Aw"})   <- delivers Ctrl-C
```

### sessions_encode_input - build dataBase64 without hand-rolling

Pure and stateless: it assembles bytes and returns `{ dataBase64, byteLen }` (base64url-nopad),
touching no session. Order: `text` (UTF-8) first, then each `keys` entry in array order.

```
sessions_encode_input({text?: "y", keys?: ["ctrl-c", "enter"], submit?: bool, paste?: bool})
```

Supported keys (case-insensitive): ctrl-a..z (and ctrl-[ \ ]), up/down/left/right, home/end,
pageup/pagedown, f1..f12, enter, tab, esc, backspace, delete, insert, space.

- `paste: true` wraps the whole payload as literal bracketed-paste text, so control / nav keys
  inside it arrive as text, not keypresses - do not combine paste with control keys.
- `submit` / `paste` are also applied by sessions_send_input. Apply them on ONE call, not both,
  or you double the trailing Enter / nest the paste markers.

### sessions_send_script - large payloads (> ~4 KiB)

For scripts too big to send as one keystroke burst. Local sessions: writes `text` to a
`<cwd>/.strix/prompts/` file and runs `bash <path>` (avoids flooding the PTY echo and line
editor). Remote sessions: chunks the payload into 4 KiB bracketed-paste pieces over the overlay.

```
sessions_send_script({uuid, text: "<script>", submit?: true})
-> { sent: N, method: "temp_file" | "chunked_paste", path?: "<local temp path>" }
```

`submit` defaults to true (runs the script). Use sessions_send_input for short input.

### Key constraints

| Constraint | Detail |
|------------|--------|
| `cwd` required | Local spawn: `cwd` must be a non-empty absolute path to an EXISTING directory, else `BadRequest("cwd is required: ...")`. |
| `mode` must be `"raw"` | Only `"raw"` is implemented; other values are rejected, not downgraded. Strip ANSI client-side if needed. |
| Buffer retained after exit | When `exited: true`, the full buffer stays readable - read after exit to collect one-shot output. |
| Empty frames is not an error | Empty `frames` = no new output since the cursor. Keep polling until `exited: true`. |
| `hub_stop` requires confirm | `hub_stop({confirm: true})`; without it, `BadRequest("confirm_required")`. |
| `account_login` is two-step | `{step: "request", email}` sends an OTP; `{step: "verify", email, code}` signs in. The device token is NEVER returned over MCP. |

### Fleet-wide session listing

```
sessions_list_all_nodes()
-> { nodes: [{ nodeId: string|null, nodeOnline: boolean, sessions: [...] }] }
```

`nodeId: null` = local hub. `nodeOnline: false` + empty sessions = remote node timed out
(1500ms per-node deadline). Use `nodeId` from this call to target remote spawns.

### All 19 tools

| Group | Tools |
|-------|-------|
| nodes | `nodes_list`, `nodes_add`, `nodes_remove` |
| tunnels | `tunnels_list`, `tunnels_add`, `tunnels_remove` |
| sessions | `sessions_list`, `sessions_spawn`, `sessions_kill`, `sessions_read_output`, `sessions_send_input`, `sessions_encode_input`, `sessions_send_script`, `sessions_list_all_nodes` |
| account | `account_status`, `account_login`, `account_logout` |
| hub | `hub_status`, `hub_stop` |

---

## strix + strixd CLI reference

`strix` global flags: `--json` (machine-readable), `--quiet` (exit code only; conflicts with
`--json`). Exit codes: `0` OK, `1` ERROR, `2` USAGE (bad args), `5` HUB_UNAVAILABLE.

### strix (app / hub)

```sh
# Hub
strix hub status           # running state, version, pid, session count
strix hub version
strix hub peers            # connected relay peers

# Account
strix account status       # sign-in state (email, plan)
strix account login        # interactive email OTP sign-in
strix account logout       # revoke device token + sign out

# Nodes - inverted pairing: hub generates code, node redeems it
strix nodes list
strix nodes add <name>     # min 3 chars; generates XXXX-XXXX-XXXX, polls until redeemed
strix nodes remove <id>    # id is the node ID from nodes list

# Sessions
strix sessions list [--node <id>] [--all-nodes]
strix sessions spawn [--node <id>] [--cmd <command>] [--cwd <directory>]
strix sessions kill <uuid> [--node <id>]
strix sessions attach <uuid>    # raw-TTY passthrough; local only
strix sessions adopt <uuid>     # claim writer role on a shared session; local only

# Tunnels
strix tunnels list
strix tunnels add --kind <tcp|unix> --local <bind> --remote <target> --node <id> [--name <label>]
strix tunnels remove <name>

# Install
strix install              # put the `strix` CLI on your PATH (idempotent)
```

`--node` routes to a remote node (omit for local hub, and works the same as an MCP
`nodeId`-routed spawn); `--all-nodes` fans out fleet-wide. `--cmd` runs IMMEDIATELY (the hub
auto-appends Enter when missing) - no trailing newline needed.

`strix install` puts the `strix` binary itself on your `PATH`: a symlink at `~/.local/bin/strix`
on macOS/Linux (tracks app updates automatically), or a copy under
`%LOCALAPPDATA%\Strix\bin\strix.exe` plus a user-PATH entry on Windows. Safe to re-run - a
no-op once already installed.

### strixd (node)

```sh
strixd --init [--relay <url>] [--no-service] [--no-copy] [--force] [--no-account]
strixd login               # interactive email OTP
strixd logout              # revoke token (identity key preserved)
strixd pair XXXX-XXXX-XXXX # redeem a pair code from the app (60s window)
strixd service status | restart | install [--no-start] [--no-copy] | uninstall
strixd peers list [--json]
strixd peers remove <node-id|prefix>
strixd terms list [--json]
strixd update [--check] [--force] [--no-restart]
```

---

## Node setup + pairing

### Install and initialize

```sh
curl -fsSL https://onestrix.com/install-node.sh | sh   # one-liner (recommended)
# or manual:
tar xzf strixd-*.tar.gz && ./strixd --init
```

`--init` writes `~/.strix/strixd.json`, creates the node identity key `~/.strix/strixd.key`
(preserved across re-runs), installs a per-user OS service (macOS LaunchAgent / Linux systemd
user unit), and copies the binary to `~/.local/bin/strixd`. Useful flags: `--relay <url>`
(default `relay.onestrix.com`), `--no-service`, `--no-copy`, `--force`/`-y`, `--no-account`.

### Sign in (email OTP)

```sh
strixd login
# Account email: you@example.com  -> code emailed
# Enter code: 093-532             -> Node registered (free plan)
```

Code is 6 digits (dash optional), valid 10 minutes, max 5 attempts. The signed device token +
entitlement are written to `strixd.json` (atomic 0600). Free plan: **3 devices** total (apps +
nodes); cross-account pairing needs a Team plan.

### Pair the node to an app (inverted pairing)

Pairing is separate from sign-in: the app generates the code, the node redeems it.

- **In the app:** Worker nodes -> Add a node -> Name -> Generate. The code is valid 60 seconds.
- **On the node (signed in):** `strixd pair XXXX-XXXX-XXXX` (dashes / case ignored). Success:
  "Paired. The app will connect to this node shortly."

### Manage peers and files

```sh
strixd peers list [--json]               # authorized apps + live state
strixd peers remove <node-id|prefix>  # then restart to reload allowlist
strixd service restart                   # reload peers.json (cached at boot)
```

| File (`~/.strix/`) | Mode | Purpose |
|------|------|---------|
| `strixd.key` | 0600 | node identity key (stable across re-init and logout) |
| `strixd.json` | 0644 | service config + signed token + entitlement |
| `peers.json` | 0644 | inbound app allowlist (set of node IDs) |
| `daemon.sock` | - | local AF_UNIX IPC |

---

## Manual MCP registration

Only needed if you skip the `register-strix.mjs` one-liner above. Read `~/.strix/mcp.port` and
`~/.strix/mcp.token` for `<port>` and `<token>`.

**Claude Code** (native HTTP MCP):

```sh
claude mcp add --transport http strix \
  "http://127.0.0.1:$(cat ~/.strix/mcp.port)/mcp" \
  --header "Authorization: Bearer $(cat ~/.strix/mcp.token)"
```

Manage with `claude mcp list | get strix | remove strix`. For a git-tracked project `.mcp.json`,
use a `${STRIX_MCP_TOKEN}` env-var placeholder for the bearer value instead of the literal token.

**OpenCode** (`~/.config/opencode/opencode.json`): add under `mcp.strix` -
`{ "type": "remote", "url": "http://127.0.0.1:<port>/mcp", "enabled": true, "headers": { "Authorization": "Bearer <token>" } }`.

**Codex** (`~/.codex/config.toml`): add `[mcp_servers.strix]` with `url = "http://127.0.0.1:<port>/mcp"`
and `http_headers = { Authorization = "Bearer <token>" }`. Some builds need
`experimental_use_rmcp_client = true`.

**Universal stdio bridge** (agy, older Codex, any stdio-only CLI): run via `npx -y mcp-remote
http://127.0.0.1:<port>/mcp --header "Authorization: Bearer <token>"`. The bridge sends a
loopback `Host`, so both auth gates pass; needs `npx` + internet on first run.

---

## Install this skill

The `register-strix.mjs` one-liner above also installs this file into each detected agent's
skills directory (`<config-home>/skills/strix/SKILL.md`). To install by hand for one agent:

```sh
mkdir -p ~/.claude/skills/strix && curl -fsSL https://onestrix.com/SKILL.md -o ~/.claude/skills/strix/SKILL.md
```

Swap the path for another agent's config home (`~/.codex`, `~/.config/opencode`, `~/.agy`, or a
project-local `.claude`). Paths for Codex / OpenCode / agy follow the cross-agent convention
`<config-home>/skills/<name>/SKILL.md`; verify against your version if it does not auto-load.
