---
name: homeroot-agent-cli
description: Install and operate the HomeRoot agent. Create/edit/pause/delete tunnels and website forwards on the calling device only. Use when configuring HomeRoot on a server via the homeroot CLI.
---

# HomeRoot — Agent CLI

The `homeroot` agent runs on your server, connects to the hub at `https://homeroot.dev`, and exposes local services through tunnels and website forwards. Everything is **device-scoped**: the agent token can only manage tunnels and websites on that one device.

## Install

Get a device token from the dashboard (Devices → New device). Shown once — treat it as a secret.

**Linux:**

```bash
curl -fsSL 'https://homeroot.dev/install/linux.sh?token=DEVICE_TOKEN&api=https%3A%2F%2Fhomeroot.dev' | sudo sh
```

**Windows** (elevated PowerShell):

```powershell
irm 'https://homeroot.dev/install/windows.ps1?token=DEVICE_TOKEN&api=https%3A%2F%2Fhomeroot.dev' | iex
```

Installs the binary and registers the `homeroot-agent` service.

## Device signing credentials and migration

New agents and CLI requests use **Ed25519 request signatures**, not a bearer token.
The WireGuard key is separate. The server stores only the public signing key and
SHA-256 hashes of legacy/bootstrap tokens. Bootstrap credentials are returned once
on device creation or credential reset, never in list/config/admin responses.

At first use, `run`, `tunnel` or `website` enrolls with the bootstrap token and
persists `device-key.json` next to the agent state (Linux default:
`/var/lib/homeroot/device-key.json`; Windows: `%ProgramData%\homeroot\device-key.json`).
Use `--credential-file <path>` on these commands, or `HOMEROOT_CREDENTIAL_FILE`,
for another location. The API URL is stored in that file; subsequent commands do
not need `--token`. Run as the installed agent's privileged account (e.g. `sudo`
on Linux); Unix files are mode 0600 and Windows keys are restricted to SYSTEM
and Administrators. Never paste the credential file into chats or logs.

```bash
# Initial CLI-only enrollment (use a different file for each device)
homeroot tunnel list --api https://homeroot.dev --token <BOOTSTRAP_TOKEN> --credential-file /secure/device-key.json --json
# Afterwards: no token, no running daemon required
homeroot tunnel list --credential-file /secure/device-key.json --json
homeroot website domains --credential-file /secure/device-key.json --json
homeroot run --credential-file /secure/device-key.json --api https://homeroot.dev
```

The server accepts old clients during migration. **90 days after the first valid
signed request for a device**, its old token stops authenticating; subsequent
signed requests do not extend this deadline. Devices not yet updated keep working
with their legacy token. New clients never silently fall back to bearer auth.
Deploy the dual-protocol server **before** releasing new agents. After the grace
period a downgraded client must be updated or explicitly recovered; retaining an
old token does not bypass expiry. HTTPS is required except loopback development.

Recovery: stop the agent, reset credentials in Devices (revokes the old signing
key AND token), move the old credential file out of the way, then enroll with the
new bootstrap token and restart. Restore a matching key from a protected backup
instead when appropriate. An existing token cannot replace an enrolled key.
Never reset credentials merely because a CLI command needs permission: use the
same protected key as the installed agent. A stale `.lock` directory after a
crash can be removed only when no enrollment process is running. A pending
credential with an empty device ID requires the original bootstrap token to retry.

Installer scripts pin SHA-256 hashes of the embedded binaries, verify before
replacement and abort on mismatch. This is integrity checking over HTTPS, **not**
independent release signing; a compromised script-serving origin remains trusted.
Do not disable checksum checks to work around a mismatch.

## Environment

| Variable | Meaning |
| --- | --- |
| `HOMEROOT_TOKEN` | legacy/bootstrap token, only needed for enrollment |
| `HOMEROOT_CREDENTIAL_FILE` | protected Ed25519 credential file |
| `HOMEROOT_API` | hub base URL (default from installer: `https://homeroot.dev`) |
| `HOMEROOT_SERVICE` | auto-update mode: `systemd` \| `launchd` \| `windows-service` |
| `HOMEROOT_STATE` | path to agent state file |

Flags override env vars.

## Commands

### `homeroot run`

Daemon mode (normally started by the service).

```bash
homeroot run --token <DEVICE_TOKEN> --api https://homeroot.dev [--service systemd|launchd|windows-service]
```

| Flag | Meaning | Default |
| --- | --- | --- |
| `-token` | bootstrap token (only for first enrollment) | — |
| `-credential-file` | signing credential file (also tunnel/website commands) | next to state |
| `-api` | hub API base URL | `http://localhost:8080` |
| `-service` | service mode for auto-updates | unset = manual |

Registers the device, keeps the WireGuard link up, relays tunnels, heartbeats, and self-updates when a service mode is set.

### `homeroot tunnel`

Manage TCP/UDP port forwards for **this device only**.

```bash
homeroot tunnel list   [--token <T>] [--api https://homeroot.dev] [--json]
homeroot tunnel show   <id|port> [--token <T>] [--api ...] [--json]
homeroot tunnel create --local-port <PORT> \
    [--local-host 127.0.0.1] [--protocol tcp|udp] \
    [--proxy-protocol v1|v2|none] [--inactive] [--token <T>] [--api ...] [--json]
homeroot tunnel edit   <id|port> \
    [--local-host ...] [--local-port ...] [--protocol ...] \
    [--proxy-protocol ...] [--active|--inactive] [--token <T>] [--api ...] [--json]
homeroot tunnel delete <id|port> --yes [--token <T>] [--api ...] [--json]
```

- `create`: `--local-port` required (1–65535); `--local-host` defaults to `127.0.0.1`; `--protocol` defaults to `tcp`.
- `edit`: `--active` resumes, `--inactive` pauses (mutually exclusive).
- `delete`: requires `--yes`.
- `show` / `edit` / `delete` accept tunnel id or public port.

### `homeroot website`

Manage HTTP(S) reverse-proxy forwards (public subdomain → local port) for **this device only**.

```bash
homeroot website domains [--token <T>] [--api ...] [--json]
homeroot website check   --subdomain <name> --domain-id <id> [--token <T>] [--api ...] [--json]
homeroot website list    [--token <T>] [--api ...] [--json]
homeroot website show    <id|subdomain|fqdn> [--token <T>] [--api ...] [--json]
homeroot website create  --subdomain <name> --domain-id <id> --local-port <PORT> \
    [--local-host 127.0.0.1] [--scheme http|https] [--token <T>] [--api ...] [--json]
homeroot website edit    <id|subdomain|fqdn> \
    [--local-host ...] [--local-port ...] [--scheme http|https] \
    [--active|--inactive] [--token <T>] [--api ...] [--json]
homeroot website delete  <id|subdomain|fqdn> --yes [--token <T>] [--api ...] [--json]
```

- `domains`: list available public zones (pick a `domain_id` for create/check).
- `check`: whether a subdomain is free under that zone.
- `create`: `--subdomain`, `--domain-id`, and `--local-port` required; `--scheme` defaults to `http`.
- `edit`: `--active` enables, `--inactive` pauses.
- `delete`: requires `--yes`.
- `show` / `edit` / `delete` accept website id, subdomain label, or full FQDN.

## Agent API (device signatures; legacy tokens during migration)

Prefer the CLI, which signs requests automatically. Headers: `X-HR-Device`,
`X-HR-Time` (Unix seconds), `X-HR-Nonce` (24 random bytes, base64url without
padding), `X-HR-Signature` (Ed25519 signature, base64url without padding).
The signed UTF-8 message consists of newline-separated fields:
`homeroot-device-v1`, configured API origin, device ID, timestamp, nonce,
HTTP method, escaped path, exact raw query, lowercase hex SHA-256 of raw body.
Timestamps have a ±5 minute window; nonce reuse is rejected across server restarts.
Keep system clocks synchronized. A changed body/path/query/audience invalidates
the signature. Redirects are refused; signatures are only sent to the configured origin.

`POST /api/v1/agent/signing-key` accepts `{token, public_key}` for initial enrollment,
returns `{device_id, public_key}` and refuses replacement by a different key.
A signed request proves key possession and starts the 90-day migration deadline.
Legacy `Authorization: Bearer <DEVICE_TOKEN>`, token query/body parameters remain
supported only while the device's migration grace allows them.

### Tunnels

| Method | Path |
| --- | --- |
| `GET` | `/api/v1/agent/tunnels` |
| `POST` | `/api/v1/agent/tunnels` |
| `GET` | `/api/v1/agent/tunnels/{idOrPort}` |
| `PATCH` | `/api/v1/agent/tunnels/{idOrPort}` |
| `DELETE` | `/api/v1/agent/tunnels/{idOrPort}` |

### Websites

| Method | Path |
| --- | --- |
| `GET` | `/api/v1/agent/websites` |
| `POST` | `/api/v1/agent/websites` |
| `GET` | `/api/v1/agent/websites/domains` |
| `GET` | `/api/v1/agent/websites/check?subdomain=&domain_id=` |
| `GET` | `/api/v1/agent/websites/{idOrName}` |
| `PATCH` | `/api/v1/agent/websites/{idOrName}` |
| `DELETE` | `/api/v1/agent/websites/{idOrName}` |

### Other agent endpoints

| Method | Path | Notes |
| --- | --- | --- |
| `POST` | `/api/v1/agent/register` | used by `run` |
| `POST` | `/api/v1/agent/heartbeat` | used by `run` |
| `GET` | `/api/v1/agent/config` | used by `run` |
| `GET` | `/api/v1/agent/update` | self-update manifest |

## Workflows

**Expose a local TCP service**

```bash
homeroot tunnel create --local-port 8080 --local-host 127.0.0.1 --protocol tcp
homeroot tunnel list --json
homeroot tunnel edit <id> --inactive   # pause
homeroot tunnel edit <id> --active     # resume
homeroot tunnel delete <id> --yes
```

**Expose a local HTTP app as a website**

```bash
homeroot website domains --json                    # pick domain_id
homeroot website check --subdomain myapp --domain-id <DOMAIN_ID>
homeroot website create --subdomain myapp --domain-id <DOMAIN_ID> --local-port 3000
# → https://myapp.<zone> → http://127.0.0.1:3000 on this device
homeroot website edit myapp --inactive
homeroot website delete myapp --yes
```

## Scope limits

Device credentials (signatures or migration tokens) may **only**:

- manage tunnels on this device
- manage websites on this device
- register / heartbeat / self-update

It must **not** be used for user accounts, other devices, admin APIs, package changes, or DynDNS zone management. Those return 401 with a device token and are out of scope for this skill.

## Troubleshooting

| Symptom | Fix |
| --- | --- |
| Installer `400 missing token` | pass `token=DEVICE_TOKEN` in the install URL |
| CLI / API `401` | check clock, credential file/API match, revocation or expired legacy grace; use an updated CLI |
| CLI / API `429` | rate limited — back off and retry |
| `manual agent mode; … to update` | set `--service` / `HOMEROOT_SERVICE`, or re-run the installer |
| Tunnel up but unreachable | local service must listen on the configured host:port (usually `127.0.0.1`) |
| Website `409 subdomain is already taken` | pick another label; use `website check` first |
| Website `409 control panel hostname` | reserved name — choose a different subdomain |

## Safety

- Never print tokens or private signing keys. The agent stores its private key only in a protected credential file.
- `tunnel delete` / `website delete` need `--yes` — confirm with the user first.
- Prefer `--json` + `list`/`show` before any change; verify afterwards.
- Use `https://homeroot.dev` as the API base unless the user specifies another hub.
