---
name: hermes-remote-desktop
description: "Deploy Hermes across machines — install Hermes Desktop on a client, connect it in remote mode to a gateway running on another host, and completely remove Hermes installations."
version: 1.2.0
author: agent
tags: [hermes, desktop, remote, gateway, deployment, cross-machine, uninstall]
---

# Hermes Remote Desktop

Setting up the Hermes Desktop app (Electron GUI) to connect to a Hermes gateway running on a **different machine**. The desktop app has two modes:

- **Local mode** (default): runs its own Python backend on the same machine.
- **Remote mode**: connects to a remote gateway's WebSocket endpoint at `/api/ws`.

This skill covers: installation, full removal, understanding the two server types, and wiring the remote connection.

---

## Hermes Server Types

A running Hermes instance can expose two different servers, and they are NOT the same:

| Aspect | Gateway (aiohttp API server) | Dashboard (FastAPI) |
|--------|------------------------------|---------------------|
| How started | `hermes gateway run` | `hermes dashboard` |
| Ports | 18789 (configurable) | 9119 (default) |
| Endpoints | `/v1/chat/completions`, `/api/sessions`, `/health` | Full SPA web UI + `/api/ws` |
| WebSocket for desktop? | No | Yes (auto-enabled after commit `96cd37e21`; earlier versions required `--tui`) |
| Auth | API key in `X-Api-Key` header | Session token (ephemeral, per-startup) or OAuth |
| CORS | Localhost-only by default | Localhost-only by default |
| Use case | Agent runtime, messaging platforms | Web dashboard, desktop remote connection |

**Key rule:** The desktop app's remote mode connects to the **dashboard** (FastAPI), NOT the gateway (aiohttp). If you only have `hermes gateway run` on the host, the desktop client cannot connect — you must also start the dashboard.

---

## Starting the Dashboard for Remote Desktop

On the **host machine** (where the Hermes agent runs):

```bash
# Install web dependencies first (one-time)
cd ~/.hermes/hermes-agent
uv pip install -e ".[web]"

# Start the dashboard for remote desktop access
# After updating Hermes (see basic auth section): no --insecure needed, auth gate auto-engages
# Pre-update fallback: add --insecure for LAN binding
# --host 0.0.0.0 binds to all interfaces
hermes dashboard --port 9119 --host 0.0.0.0
```

This runs alongside the existing gateway (port 18789). Port 9119 is the default.

### Making the Dashboard Persistent

Create a systemd user service so the dashboard starts on boot and restarts on failure:

```ini
# ~/.config/systemd/user/hermes-dashboard.service
[Unit]
Description=Hermes Agent Dashboard - Remote WebSocket Gateway
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/path/to/venv/bin/python -m hermes_cli.main dashboard --port 9119 --host 0.0.0.0
WorkingDirectory=/home/user/.hermes
Environment=HERMES_HOME=/home/user/.hermes
EnvironmentFile=%h/.hermes/.env
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
```

**Pitfall — `--tui` flag removed in newer Hermes versions:** The `--tui` flag was removed from the `dashboard` subcommand after commit `96cd37e21`. Running `hermes dashboard --help` shows only `--port`, `--host`, `--no-open`, `--insecure`, `--stop`, `--status`. Remove `--tui` from any existing systemd service file when updating Hermes, or the dashboard will fail with `unrecognized arguments: --tui`.

**Pitfall — `--insecure` required when basic auth provider is missing:** Before updating Hermes to include the basic auth provider (commit `acb0e2bac`), the dashboard **cannot** start on `0.0.0.0` without `--insecure` — it checks for registered auth providers and exits with `Refusing to bind dashboard to 0.0.0.0 — the OAuth auth gate engages on non-loopback binds, but no auth providers are registered`. After updating and verifying the basic provider loads, remove `--insecure` to engage the auth gate.

```bash
systemctl --user daemon-reload
systemctl --user enable --now hermes-dashboard.service
systemctl --user status hermes-dashboard.service
```

**Pitfall:** The session token changes every restart. A persistent dashboard that rarely restarts is fine, but if you restart it, update the token in the client's `connection.json`.

**Security:** `--insecure` exposes the dashboard on the network. Safe patterns:
- **Tailscale:** Bind `0.0.0.0` — only Tailnet devices can reach it.
- **SSH tunnel:** Keep on `127.0.0.1`, tunnel from the client: `ssh -L 9119:localhost:9119 user@host`
- **UFW/LAN:** Open port 9119 only to client subnet:
  ```bash
  sudo ufw allow from 192.168.1.0/24 to any port 9119 proto tcp comment 'Hermes dashboard WS'
  ```

**Pitfall — WebSocket auto-enabled in newer versions:** After commit `96cd37e21`, the `--tui` flag was removed and the `/api/ws` WebSocket endpoint is auto-enabled. If you're on an older version (before this commit), you must still use `--tui` to enable WebSocket connections. Check your version with `git log -1` and verify with `hermes dashboard --help` — if `--tui` doesn't appear in the help output, it's auto-enabled.

---

## Configuring the Desktop for Remote Mode

On the **client machine** (CachyOS, laptop, etc.):

1. Launch Hermes Desktop:
   ```bash
   hermes desktop
   ```

2. Go to **Settings → Gateway** (gear icon → Gateway)

3. Switch from **Local** to **Remote** mode

4. Enter the gateway URL:
   - If dashboard is on `0.0.0.0:9119` with Tailscale: `http://100.x.x.x:9119`
   - If SSH tunnel: `http://localhost:9119`
   - If LAN direct: `http://192.168.1.50:9119`

5. **Authentication** — two possible paths:
   - **Token auth** (default): Get the session token from the host machine:
     ```bash
     curl -s http://127.0.0.1:9119/ | grep -oP '__HERMES_SESSION_TOKEN__="\K[^"]+'
     ```
     Paste this token into the desktop's gateway settings.
   - **OAuth auth**: If the gateway has an OAuth provider configured, a "Sign in" button appears instead.

6. Click **Test Connection** — the desktop should connect via WebSocket.

### Manual Config (Skip the Settings UI)

Write the config file directly on the client:

```json
# ~/.config/Hermes/connection.json
{
  "mode": "remote",
  "remote": {
    "url": "http://192.168.1.50:9119",
    "authMode": "token",
    "token": {
      "encoding": "plain",
      "value": "SESSION_TOKEN_HERE"
    }
  }
}
```

The next launch of the desktop app picks this up automatically — no need to navigate settings. The token is stored plaintext (`encoding: plain`); the first save via the UI re-encrypts it with Electron's `safeStorage`.

**Pitfall — `~/.config/Hermes/` must exist before writing `connection.json`:** The directory is not created automatically. Create it first:
```bash
mkdir -p ~/.config/Hermes
```

**Pitfall — the desktop app requires a display server:** The Electron GUI needs either `$DISPLAY` (X11) or `$WAYLAND_DISPLAY` (Wayland) to be set. On a headless/server machine, `hermes-desktop` will fail to launch. Remote desktop via SSH X forwarding (`ssh -X`) works for temporary use.

**Pitfall — session token is ephemeral:** The `_SESSION_TOKEN` is generated fresh every time the dashboard process starts. If you restart the dashboard on the host, you need a new token. This is by design — the token dies when the process exits.

**Pitfall — no dashboard means no remote:** If the host is only running `hermes gateway run` (aiohttp), the desktop will fail to connect. The WebSocket endpoint does not exist on the aiohttp API server. Start the dashboard with `--tui` as described above.

**Pitfall — `~/.config/Hermes/` (capital H) not `~/.config/hermes/`:** Electron's `userData` directory on Linux is `~/.config/Hermes/` (capital H), not lowercase `~/.config/hermes/`. If you write `connection.json` to the lowercase path, the desktop app ignores it and falls back to local mode. Always use the capital-H path:
```bash
mkdir -p ~/.config/Hermes
```
Then write `connection.json` there.

**Basic auth (username/password) — requires updating Hermes:** The Hermes docs describe `HERMES_DASHBOARD_BASIC_AUTH_USERNAME`, `HERMES_DASHBOARD_BASIC_AUTH_PASSWORD`, and `HERMES_DASHBOARD_BASIC_AUTH_SECRET` environment variables for a username/password "Sign in" flow. However, the basic auth provider (`plugins/dashboard_auth/basic/`) was only added in commit `acb0e2bac`, which is NOT in v0.15.1 tagged releases. If the installed version lacks it:

1. Update the Hermes repo:
   ```bash
   cd ~/.hermes/hermes-agent
   git pull --ff-only origin main
   ```
2. Rebuild the venv to register the new plugins:
   ```bash
   uv pip install -e ".[all]"
   ```
3. Verify the provider loads:
   ```bash
   venv/bin/python -c "from plugins.dashboard_auth.basic import hash_password; print('OK')"
   ```
4. Restart the dashboard **without `--insecure`** — the non-loopback bind auto-engages the auth gate, and the basic auth provider handles login. Verify:
   ```bash
   curl -s http://127.0.0.1:9119/api/status | python3 -m json.tool
   # Should show: auth_required=true, auth_providers=["basic"]
   ```

**Important:** The `HERMES_DASHBOARD_BASIC_AUTH_*` env vars must be in `~/.hermes/.env` AND the systemd service must include `EnvironmentFile=%h/.hermes/.env` for them to be available at boot.

**Fallback (pre-update):** Keep `--insecure` and use session token auth. Works in any version but lacks credential gating.

**Pitfall — session token extraction via basic auth uses `/auth/password-login` not `/api/auth/signin`:** The dashboard's basic auth provider registers a custom endpoint at `/auth/password-login` (NOT `/api/auth/signin`). The request body MUST include `"provider": "basic"` or the endpoint returns `Field required`. The response sets two HttpOnly cookies: `hermes_session_rt` (refresh token) and `hermes_session_at` (access token). The session token is the `hermes_session_at` value. See "Method 2" in the Session Token Extraction section below.

**Pitfall — fish-shell SSH heredocs break:** When writing `connection.json` to a fish-shell client (CachyOS/Arch), heredocs (`cat > file << 'EOF'`) fail because fish parses `<<` differently. Use base64 piping instead:

```bash
B64=*** config.json | base64 -w0)
ssh user@host "echo $B64 | base64 -d > ~/.config/Hermes/connection.json"
```

This is the most portable approach — works with any shell on the remote.

**Pitfall — `authMode: "oauth"` when dashboard has `auth_providers: ["basic"]` causes silent flap:** The desktop tries OAuth flow → fails → falls back to local → infinite flap. The connection.json `remote.authMode` must be `"token"` when the dashboard uses basic auth. Verify the dashboard's auth providers: `curl -s http://<host>:9119/api/status | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('auth_providers',[]))"`

**Pitfall — "Unknown log file: gui" error is cosmetic:** The desktop app queries `GET /api/logs/gui` on the dashboard when connecting. On a remote backend, this log file doesn't exist, producing:
```
Error: 400: {"detail":"Unknown log file: gui"}
```
This error is **harmless** — it does not block chat, agent execution, or WebSocket communication. Tell the user to ignore it. The GUI log feature works only when the desktop app manages a local backend on the same machine.

**Pitfall — token extraction grep can fail on special characters:** The `grep -oP` pattern for extracting the session token from the dashboard HTML can break when the token contains characters that grep interprets as regex operators (e.g. underscores, dots, dashes inside `[...]` character classes). Safer approach:
```bash
curl -s http://127.0.0.1:9119/ > /tmp/dash.html
grep -oP '__HERMES_SESSION_TOKEN__="\K[^"]+' /tmp/dash.html
```
The `\K` escape discards the match prefix. `[^"]+` matches everything up to the closing quote without needing a lookahead.

---

## Installing Hermes + Desktop on a Client Machine

```bash
# Fresh install with desktop app (includes Electron build)
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --include-desktop

# Or if Hermes CLI already installed:
hermes desktop    # Builds and launches the GUI
```

The `--include-desktop` flag:
1. Installs the Hermes CLI + Python backend
2. Installs Node.js workspace dependencies (~150MB Electron)
3. Builds the desktop app from `apps/desktop/`
4. Binary ends up at: `~/.hermes/hermes-agent/apps/desktop/release/linux-unpacked/Hermes`

**Requirements:** Node.js >= 20.19, Python >= 3.11, Git.

---

## Completely Removing Hermes (All Traces)

When removing Hermes from a machine, delete ALL of these:

```bash
# 1. Stop and disable the gateway service
systemctl --user stop hermes-gateway.service       # user service
systemctl --user disable hermes-gateway.service
sudo systemctl stop hermes-gateway.service          # system service (if applicable)
sudo systemctl disable hermes-gateway.service

# 2. Main config and data directory
rm -rf ~/.hermes/

# 3. Desktop app config (Electron cache)
rm -rf ~/.config/Hermes/

# 4. CLI entry points
rm -f ~/.local/bin/hermes
rm -f ~/.local/bin/hermes-agent

# 5. Systemd service files
rm -f ~/.config/systemd/user/hermes-gateway.service
sudo rm -f /etc/systemd/system/hermes-gateway.service
sudo rm -f /etc/systemd/system/hermes-dashboard.service

# 6. Verify it's gone
which hermes   # should return nothing
```

**Pitfall — CachyOS/Arch uses fish shell:** Wildcard globs fail silently when there's no match. Use `find` or explicit paths instead of `ls *.hermes*` patterns. SSHing to a fish-default machine breaks heredocs and complex quoting — use `ssh user@host 'bash -s' < script.sh` instead of inline heredocs.

**Pitfall — Writing files to fish-shell machines:** Heredocs fail because fish parses `<<` differently. Workaround: base64-encode the content, pipe it, and decode on the remote:

```bash
echo 'BASE64_ENCODED_CONTENT' | ssh user@host 'base64 -d > /path/to/file'
```

Or use `scp` with an absolute path (tilde expansion fails in scp):

```bash
mkdir -p /tmp/work && echo "data" > /tmp/work/file
scp /tmp/work/file user@host:/home/user/absolute/path/file
```

**Pitfall — Double-hop SSH quoting:** When SSHing through a jump box (`client → jump → target`), quoting compounds at every level. Three reliable patterns:

1. **Piped script (best):** Write a bash script locally, pipe it through both SSHs:
   ```bash
   ssh jumpbox_user@jumpbox 'ssh target_user@target bash -s' < /path/to/script.sh
   ```

2. **Inline Python/expect:** Write the script as a Python file and copy it around with scp:
   ```bash
   scp script.sh jumpbox_user@jumpbox:/tmp/
   ssh jumpbox_user@jumpbox 'ssh target_user@target bash /tmp/script.sh'
   ```

3. **Base64 inline (for simple commands):** Encode the command, decode on the jump box, pipe to the target:
   ```bash
   CMD=$(echo 'command_on_target' | base64 -w0)
   ssh jumpbox_user@jumpbox "echo '$CMD' | base64 -d | ssh target_user@target bash"
   ```

**Pitfall — fish shell doesn't inherit PATH from .bashrc:** When running `bash /tmp/script.sh` via SSH on a fish-default machine, `python3`, `node`, etc. may not be in PATH. Use absolute paths or explicitly set PATH in the script:
```bash
export PATH="/usr/bin:/usr/local/bin:$HOME/.local/bin:$PATH"
```

**Pitfall — Wiping all sessions to start fresh:** To completely reset the session database (all chat history), it's not enough to `DELETE FROM sessions` and `DELETE FROM messages` — the FTS5 indexes retain their page allocations and the DB file won't shrink. The reliable method:

```bash
rm -f ~/.hermes/state.db
```

Hermes recreates it with an empty schema on the next `sessions stats` call or chat interaction. This drops the file from ~585MB to ~64KB instantly. Memory, skills, config, and cron are untouched — only chat transcripts are lost.

Alternative (if you want to keep the file but empty it): DELETE all data then use `VACUUM` via Python sqlite3, but `rm -f` is simpler and guaranteed clean.

**Pitfall — standalone launcher for Electron-only clients:** When you strip Hermes to just the Electron desktop binary (remove Python backend, CLI, venv, git repo on the client), create a standalone launcher:

```bash
cat > ~/.local/bin/hermes-desktop << 'EOF'
#!/usr/bin/env bash
exec "$HOME/.hermes/hermes-agent/apps/desktop/release/linux-unpacked/Hermes" "$@"
EOF
chmod +x ~/.local/bin/hermes-desktop
```

No `hermes` CLI exists on the client — only `hermes-desktop` launches the GUI. The desktop app connects in remote mode to the host dashboard via `~/.config/Hermes/connection.json`.

**Pitfall — website docs may describe features ahead of code:** The official Hermes docs at `hermes-agent.nousresearch.com/docs/` reference features (like the basic auth provider) that may not exist in the installed version. Before spending time debugging a documented feature that doesn't work:
1. Check the installed version: `git log -1` in `~/.hermes/hermes-agent/`
2. Search the codebase: `grep -r "FEATURE_NAME" ~/.hermes/hermes-agent/ --include="*.py"`
3. Check git history: `git log --all --oneline --grep="basic\|FEATURE"` to see if the feature was added in a later commit
4. If missing, `git pull --ff-only origin main` + `uv pip install -e ".[all]"` to update

**Pitfall — WebSocket error codes:** The dashboard's `/api/ws` can reject connections with different codes depending on the problem:\n- **HTTP 403 / connection refused:** CORS restriction or firewall — the dashboard may not be accessible from the client's network. Check firewall rules and dashboard bind address (`0.0.0.0` needed for LAN).\n- **WS close 4401:** The session token is missing or invalid — the `_ws_auth_ok()` check failed. The token has changed since the dashboard restarted.\n- **Older versions only (before `--tui` removal):** WS close 4403 meant `_DASHBOARD_EMBEDDED_CHAT_ENABLED` was false — the dashboard was started without `--tui`. In current versions the WebSocket is auto-enabled and this code should not appear.\n- **HTTP 401 on `/api/status`:** When basic auth is active, unauthenticated requests return 401. This is expected — the desktop app handles this by showing a "Sign in" button.

**Pitfall — authMode mismatch causes silent connect/disconnect flap:** The desktop's `connection.json` has `remote.authMode` which must match what the dashboard supports. The dashboard reports its providers in `hermes dashboard --status` as `auth_providers: ["basic"]` (basic auth) or similar. If `authMode: "oauth"` is set but the dashboard only registers `basic`, the desktop tries OAuth -> fails silently -> falls back to local backend -> endless flap. **Fix:** Set `authMode: "token"` with a valid session token, or configure `authMode: "basic"` if the dashboard has basic auth credentials set up and the desktop supports it (requires updated Hermes). Verify mismatch by comparing `connection.json` against `curl -s http://<host>:9119/api/status | python3 -m json.tool | grep auth_providers`.

**Pitfall — empty session token causes immediate WS close:** The desktop silently stores an empty `token.value` in a freshly-written `connection.json` with `authMode: "token"`. An empty token passes JSON validation but fails WS auth with close code 4401. The desktop then falls back to local mode, creating a connect/disconnect cycle even though the rest of the config is correct. **Fix:** Always extract a real session token from the dashboard: `curl -s http://127.0.0.1:9119/ | grep -oP '__HERMES_SESSION_TOKEN__="\K[^"]+'`

**Pitfall — GitHub #38873: desktop validates remote readiness then spawns local backend (the "flap"):** This is a known P2 bug (8+ reporters, Jun 2026) where the desktop's boot orchestration confirms remote gateway readiness via HTTP but then immediately starts a local backend instead of connecting via WebSocket. The user sees "Could not connect to Hermes gateway" -> remote -> local -> remote -> local. **Root cause:** The renderer's boot handshake has a 15-second timeout (`Timed out connecting to Hermes backend after 15000ms`) that fires before the WS ticket exchange completes. **Fix:** Update Hermes on the host to get PR #39522 (`fix(desktop): validate remote gateway websocket readiness`): `cd ~/.hermes/hermes-agent && git pull --ff-only origin main && uv pip install -e ".[all]"`. Even with the fix, check the authMode mismatch first (pitfall above) — that is a separate issue that also causes flaps. Workaround: access the dashboard directly in a browser at `http://<host>:9119` to verify the dashboard is actually serving WebSocket connections.

**Pitfall — user vs system service:** Always check both `systemctl --user list-units | grep hermes` AND `systemctl list-units | grep hermes`. The gateway typically runs as a user service.

**Pitfall — .env is a protected credential file:** Do not attempt to write `.env` with `write_file` or `sed` from within a Hermes session — it's gated. Use `sed` from a direct terminal shell on the machine, or `hermes config set` for YAML settings.

---

## Session Token Extraction

On the **host machine**, after starting the dashboard:

### Method 1: Unprotected dashboard (no auth, `--insecure` or pre-auth-gate)

The token is embedded in the SPA HTML:
```bash
curl -s http://127.0.0.1:9119/ | grep -oP '__HERMES_SESSION_TOKEN__="\\K[^"]+'
```

### Method 2: Basic-auth-protected dashboard (`auth_providers: ["basic"]`)

When the dashboard redirects `/` to `/login`, the inline token is gone. Extract it via the password-login API:

```bash
# 1. Get basic auth credentials
USERNAME=$(grep "BASIC_AUTH_USERNAME" ~/.hermes/.env | cut -d= -f2)
PASSWORD=$(grep "BASIC_AUTH_PASSWORD" ~/.hermes/.env | cut -d= -f2)

# 2. Login — "provider": "basic" is REQUIRED or endpoint returns Field missing error
curl -s -c /tmp/dash_cookies.txt \
  -H "Content-Type: application/json" \
  -d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\",\"provider\":\"basic\"}" \
  http://127.0.0.1:9119/auth/password-login
# Expected: {"ok":true,"next":"/"}

# 3. Extract session token from the hermes_session_at cookie
python3 -c "
import http.cookiejar
cj = http.cookiejar.MozillaCookieJar()
cj.load('/tmp/dash_cookies.txt')
for c in cj:
    if 'session_at' in c.name:
        print(c.value)
"
```

### Token format and lifecycle

| Property | Value |
|----------|-------|
| Format | Base64-encoded JWT (~114 chars, starts with `eyJ`) |
| Source | `hermes_session_at` cookie or `__HERMES_SESSION_TOKEN__` SPA variable |
| Persistence | Lost on dashboard restart (in-memory) |
| Changes | Every dashboard restart, even with same credentials |

The token changes every time the dashboard starts. Always re-extract after restart.

---

## Verification

Check that the remote connection is working:

- **On the host:** `hermes dashboard --status` should show the dashboard running
- **On the client:** The desktop app should show "Connected" in the gateway indicator (top-right or status bar)
- **Test:** Send a message in the desktop chat — it should route through the host's agent and return a response

If the desktop shows "Connecting..." indefinitely, check:
1. Is the dashboard running on the host? (`curl http://host:9119/health`)
2. Is the WebSocket enabled? (dashboard started with `--tui`?)
3. Is the session token correct? (dashboard restart = new token)
4. Is the port accessible? (firewall, Tailscale ACLs, SSH tunnel)

## Cleaning Up the Client (Optional)

After confirming remote mode works, the local Python backend on the client is unnecessary. Remove it:

```bash
# Python backend + deps
rm -rf ~/.hermes/hermes-agent/venv
rm -rf ~/.hermes/hermes-agent/node_modules
rm -rf ~/.hermes/hermes-agent/.git
rm -rf ~/.hermes/hermes-agent/apps/desktop/node_modules
rm -rf ~/.hermes/hermes-agent/apps/desktop/src
rm -rf ~/.hermes/hermes-agent/apps/desktop/electron

# Python source code (keep only apps/desktop/release/)
rm -rf ~/.hermes/hermes-agent/gateway
rm -rf ~/.hermes/hermes-agent/hermes_cli
rm -rf ~/.hermes/hermes-agent/agent
rm -rf ~/.hermes/hermes-agent/tools
rm -rf ~/.hermes/hermes-agent/cron
rm -rf ~/.hermes/hermes-agent/plugins
rm -rf ~/.hermes/hermes-agent/providers
```

Keep: `~/.hermes/hermes-agent/apps/desktop/release/linux-unpacked/` (the compiled Electron app + app.asar).

Create a standalone launcher:

```bash
cat > ~/.local/bin/hermes-desktop << 'EOF'
#!/usr/bin/env bash
exec ~/.hermes/hermes-agent/apps/desktop/release/linux-unpacked/Hermes "$@"
EOF
chmod +x ~/.local/bin/hermes-desktop
```

---

## Reference Files

| File | Content |
|------|---------|
| `references/gateway-vs-dashboard-ports.md` | Port architecture, server types, CORS, session token mechanism |
| `references/cachyos-to-debian-remote-desktop.md` | Full working deployment: CachyOS → Debian, with UFW, systemd, fish shell workarounds |
| `references/basic-auth-token-extraction.md` | Full workflow: extracting session token from basic-auth-protected dashboard via password-login API + cookie jar |
