---
name: hermes-relay-pair
description: Generate a signed setup QR for Hermes-Relay Android, preserving Dashboard/Gateway ownership and optional Relay or API routes.
version: 1.0.0
author: Axiom Labs
license: MIT
platforms: [linux, macos, windows]
metadata:
  hermes:
    tags: [pairing, qr, android, relay, setup, hermes-relay]
    category: devops
    homepage: https://github.com/Codename-11/hermes-relay
    related_skills: []
---

# Hermes-Relay Pairing

[Hermes-Relay](https://github.com/Codename-11/hermes-relay) is a native Android client for Hermes. Standard Chat, Manage, and voice belong to upstream Dashboard/Gateway. API-only operation is explicit compatibility, never an automatic fallback for a Gateway-owned conversation. Relay adds optional terminal/bridge and other extensions. This skill generates a signed setup QR using `plugin.pair`.

## When to Use

Invoke this skill when any of the following happens:

- User runs the `/hermes-relay-pair` slash command.
- User asks to "pair my phone", "connect the Hermes-Relay app", "scan a QR for Hermes", or anything equivalent.
- User is setting up the Hermes-Relay Android app for the first time, or re-pairing after uninstall / token loss.
- User reports terminal tab asking for a pairing code (the app needs a fresh relay code embedded in a QR).

Do NOT use this skill to start or install the relay server itself — that is a prerequisite. Reference `hermes relay start` and stop.

### Dashboard alternative

Operators with the Hermes dashboard open can also mint the same QR from the web UI: **Relay tab → Management → "Pair new device"** (Mode + Prefer dropdowns), or **Relay tab → Remote Access → "Regenerate QR"** for the fuller preview + probe view. Both UIs call the same `handle_pairing_mint` endpoint this skill shells into. Prefer this skill when you're already in a terminal (faster + scriptable); prefer the dashboard when the operator needs to see + probe candidate endpoints before minting. The dashboard's "Advanced · API-server override" field should stay blank in almost every case — pinning a forward-auth-gated hostname there (e.g. Authelia-fronted FQDN) pairs WSS but breaks the API side. See `docs/remote-access.md` § "Forward-auth gateways".

## Prerequisites

1. **Hermes-Relay plugin installed and enabled.** Verify with `hermes pair --help`. If the command is unavailable, run `hermes plugins install Codename-11/hermes-relay/plugin --enable`. Use the full `install.sh` path instead only when the host also needs the relay service, editable package, and shell shims.
2. **Dashboard/Gateway reachable** at its configured origin for standard Chat, Manage, and voice. The API server is required only when explicitly including API-only compatibility; `plugin.pair` reads optional API configuration from the Hermes configuration/environment.
3. **Relay server running** on `RELAY_HOST:RELAY_PORT` (default `0.0.0.0:8767`) if the user wants terminal/bridge channels. The Relay may stay host-internal: current Android pairing normally reaches it through the Dashboard's same-origin plugin transport. Tailscale Serve normally exposes dedicated HTTPS `10443` and proxies the host-local Dashboard on `9119`; a raw LAN/tailnet route may reach `9119` directly. Listener `443` is an advanced explicit override only when it is free. Without a live relay, the QR will configure chat only.
4. **Check optional host capabilities.** Standard Dashboard setup and QR generation are independent of PTY support. Terminal requires the supported POSIX backend; do not present a Windows host as terminal-capable merely because pairing succeeded.

## Procedure

For Secure Link, run `hermes relay secure-link --host <reachable-server-address>`
before minting. It is a read-only report shared with Dashboard and Desktop setup.
Resolve its blockers and have the operator apply the reviewed settings/restart
through the existing service owner; this pairing skill must not guess a service
or change it. Re-check until the requested listener is active, then use
`hermes pair --png`. Keep the certificate and pin in the signed invite. QR trust,
Dashboard sign-in, and Chat readiness are separate steps; never learn a pin from
an untrusted health response or downgrade the route after a TLS failure.

1. **Probe the relay** — `curl -sf http://127.0.0.1:8767/health` (or `$RELAY_PORT`). If it returns 200, Relay is up; that alone does not prove Dashboard sign-in or Gateway readiness. If it fails, standard setup can still use Dashboard's **Connect mobile app** QR. Optional Relay pairing requires a running Relay. Do not offer API-only as automatic recovery for a Gateway connection, and do not start Relay unless the user asks.

2. **Generate the QR** — run via the `terminal` tool:

   ```bash
   hermes pair
   ```

   When the current Hermes surface exposes an exact Dashboard origin, pass it
   rather than guessing from an API hostname. This is especially important for
   an external HTTPS origin or a Dashboard mounted below a path prefix:

   ```bash
   python -m plugin.pair --dashboard-url https://hermes.example.com
   ```

   A trusted configured `HERMES_DASHBOARD_PUBLIC_URL` / Dashboard public URL is
   also suitable. If no exact origin is available, omit the flag; the pair
   backend will construct conventional LAN/raw-tailnet Dashboard candidates on
   `9119`, or advertise the HTTPS `10443` listener when Tailscale Serve maps it
   to local Dashboard `9119`. HTTPS `443` is used only after the operator
   explicitly selects that advanced listener and confirms it is free. Never
   substitute the API server URL or infer public port `8767`.

   On a full-relay or legacy editable install where an older Hermes host cannot register plugin CLI commands, use the Hermes venv explicitly:

   ```bash
   ~/.hermes/hermes-agent/venv/bin/python -m plugin.pair
   ```

3. **Useful flags** (pass only when needed, not by default):
   - `--png` — save PNG to `/tmp/hermes-pairing-qr.png` and skip the Unicode terminal QR. Use when the user reports the terminal QR won't scan (small font, dark mode, non-Unicode terminal).
   - `--no-qr` — text only, no QR at all. Use when the agent is running in a non-TTY context and QR output would be wasted.
   - `--no-relay` — skip relay pre-pairing, render API-only QR. Use if the relay is intentionally offline.
   - `--host <ip>` / `--port <n>` — override the API server host or port when config auto-detection picks the wrong values.
   - `--dashboard-url <url>` — exact Dashboard origin to preserve in the pairing invite, including any path prefix. Prefer the current trusted Dashboard origin when the TUI/chat surface knows it; do not invent one from the API endpoint.
   - `--mode {auto,lan,tailscale,public}` — endpoint discovery mode (ADR 24). Default `auto` probes LAN + Tailscale (if the helper is installed) + `--public-url` (if passed) and bakes them into the QR as an ordered candidate list so the phone switches networks automatically. Each normal candidate uses its Dashboard origin plus `/api/plugins/hermes-relay/transport`; `lan` / `tailscale` / `public` emit just that role. Example: `python -m plugin.pair --mode auto --public-url https://hermes.example.com`.
   - `--public-url <url>` — public **Dashboard origin** for a reverse proxy / Cloudflare Tunnel. Must be `http://` or `https://`; an external HTTPS Dashboard such as `https://hermes.example.com` keeps Relay on that same authority and plugin transport path. It does not imply or expose public port `8767`.
   - `--prefer <role>` — promote the named role to priority 0 in the endpoint list. Open vocab — commonly `lan` / `tailscale` / `public`. Useful when the user wants to force a specific path during testing without re-ordering defaults. Example: `python -m plugin.pair --mode auto --prefer tailscale` emits all detected modes but with Tailscale as the first-probed endpoint. Warns (non-fatal) if the named role isn't detected.
   - `--legacy-direct-relay` — also advertise the direct Relay port for older Hermes Relay Desktop CLI/UI clients, which cannot yet authenticate through Dashboard WebSocket ingress. Use only when pairing one of those clients and when that direct route is deliberately reachable; do not use it for normal Android pairing or infer it from a public Dashboard URL.
   - `--register-code <code>` — **manual fallback**. Skip QR rendering entirely and just pre-register a 6-char code the user is reading off the phone screen. See "Manual fallback" below.

4. **Show the output verbatim.** `plugin.pair` prints, in order:
   - Text block with `Server` URL, masked `API Key`, and (if relay is up) a `Relay (terminal + bridge)` section with `URL` and `Code`.
   - `Copy/paste pairing invite` section with a `hermes-relay://pair?payload=...` URL. This is the preferred desktop GUI/CLI fallback when QR scanning is unavailable.
   - Unicode half-block QR (when stdout is a TTY).
   - `PNG: /tmp/hermes-pairing-qr.png` line.
   - `WARNING: This QR contains credentials ...` line (whenever an API key or relay code is present).

   Relay the full output back to the user. Do NOT redact the QR — the user needs to scan it. DO repeat the credentials warning in your own words.

5. **Tell the user how to scan.** Give them these exact steps:
   1. Open the Hermes-Relay Android app.
   2. If onboarding: tap `Scan Pairing QR` on the Connect page.
   3. If already onboarded: go to `Settings` → `Connection` → `Scan Pairing QR`.
   4. Point the camera at the terminal (or the PNG file) until it auto-detects.
   5. Watch for the success toast and the status summary.

   For Desktop pairing, tell the user to copy the `hermes-relay://pair?...`
   invite URL into **Hermes Relay Desktop → Pair → Paste invite**, or run the
   host mint with the explicit compatibility route and then paste its invite:

   ```bash
   hermes pair --legacy-direct-relay
   hermes-relay pair --pair-qr 'hermes-relay://pair?payload=...'
   ```

   The current Desktop client does not yet obtain Dashboard WebSocket tickets,
   so a default Android-focused invite containing only same-origin Dashboard
   transport routes is intentionally not sufficient for Desktop pairing.

6. **Time constraint.** The relay pairing code expires 10 minutes after generation and is single-use. If the user won't scan within that window, re-run the skill to mint a fresh code.

## Manual fallback (`--register-code`)

Use this when QR scanning is **physically impossible**:

- The user is SSH'd into the host from their phone (the only camera-equipped device) and there's no second device to point at the screen.
- The host has no display attached (a headless server you ssh into from a terminal that can't render Unicode QR blocks, or where copying a PNG off-host is awkward).
- The user's phone is the device with the camera *and* the device that needs to pair — there's no way to scan its own screen.

**Workflow:**

1. Tell the user to open the Hermes-Relay app → **Settings** → **Connection** → **Manual pairing code (fallback)**. The app displays a locally-generated 6-char code (A-Z / 0-9). They read it to you (or paste it into the chat).
2. On the host, run:

   ```bash
   hermes-pair --register-code ABCD12
   ```

   Replace `ABCD12` with the code from the phone. The command pre-registers it with the relay via the same loopback `/pairing/register` endpoint the QR flow uses, then prints a confirmation block listing the code, transport hint, session TTL, and what the user should tap next.

3. Tell the user to tap **Connect** in that same Manual pairing code card. The relay accepts the code, mints a session token, and the phone is paired.

**TTL + grants compose with `--register-code` exactly like they do with the QR flow:**

```bash
hermes-pair --register-code ABCD12 --ttl 30d --grants chat:never,bridge:7d
hermes-pair --register-code ABCD12 --ttl never
hermes-pair --register-code ABCD12 --transport-hint wss
```

Pass `--transport-hint wss` only when you know the relay is actually running behind TLS (e.g. an external reverse proxy) but the host-side check can't tell. Otherwise it's auto-detected from `RELAY_SSL_CERT`.

**Exit codes:**

- `0` — code accepted, pairing pre-registered. Phone can now tap Connect.
- `1` — relay was unreachable, OR relay was reachable but rejected the code (loopback-only endpoint — confirm you're on the same host as the relay).
- `2` — argument validation failed (bad code format, wrong length, invalid TTL/grants spec).

**Same 10-minute expiry rules apply** — once the operator runs `hermes-pair --register-code`, the user has 10 minutes to tap Connect or the code is invalidated and they need a fresh one from the app + a fresh `--register-code` invocation.

## Pitfalls

- **Relay not running.** `plugin.pair` prints `[info] Relay not running ... QR will configure chat only` and renders an API-only QR. Terminal tab will then ask the user to paste a pairing code manually. Fix: start the relay first (`hermes relay start`) and re-run.
- **Plugin not installed.** If `hermes pair --help` is unavailable, run `hermes plugins install Codename-11/hermes-relay/plugin --enable`. Use `install.sh` for the full relay/service/shim setup rather than constructing a manual editable install.
- **Wrong venv.** If `hermes` CLI is global but plugin is in the Hermes venv, `python -m plugin.pair` may resolve to the wrong Python. Call the venv Python explicitly: `~/.hermes/hermes-agent/venv/bin/python -m plugin.pair`.
- **Pairing code expired.** 10-minute TTL, one-shot. Re-run `python -m plugin.pair` to mint a fresh code; the previous code is automatically invalidated on the next run.
- **QR won't scan on terminal.** Likely causes: terminal font too small (zoom in), dark-mode color inversion mangling the blocks, or terminal lacks Unicode half-block support. Fix: re-run with `--png` and point the camera at the saved image, or open the PNG in an image viewer on a second screen.
- **Host resolves to `127.0.0.1`.** The phone on the LAN can't reach loopback. `plugin.pair` auto-detects the outbound LAN IP via a UDP socket trick, but if that fails set `API_SERVER_HOST=0.0.0.0` in `~/.hermes/.env` or pass `--host <lan-ip>` explicitly.
- **Relay is running but `/pairing/register` rejected.** Printed as `[warn] Relay is running but /pairing/register was rejected`. The endpoint is gated to loopback callers — the relay must be on the same host as the agent running this skill. If it's on a different host, pairing has to be done there.

## Verification

After the user scans, confirm all three of the following:

1. **Phone side.** Ask the user to open `Settings` → `Connection`. They should see:
   - `API Server`: reachable, green status.
   - `Relay`: connected, green status.
   - `Session`: paired.
2. **Relay side.** Run `curl -s http://127.0.0.1:8767/health` — `clients` should be `>= 1` after the phone connects.
3. **Functional check.** Ask the user to send a test message from the chat tab and switch to the terminal tab — the terminal should attach without prompting for a pairing code.

If any of those fail, fall back to the Pitfalls section and re-run the skill with appropriate flags.
