# CLI reference

Terminal window

```
homespun deploy ./my-app                 # create or redeploy an app
homespun apps list --status active       # list your apps
homespun data app_abc123 todos list      # read a collection's rows
homespun members add --app app_abc123 --email alice@example.com
```

The CLI is self-documenting: `homespun <command> --help` is always the authoritative reference for flags and defaults on whatever version you have installed. This page mirrors that text so you can read it without a terminal open, and calls out a couple of sharp edges the `--help` text doesn’t mention.

## Global flags

[Section titled “Global flags”](#global-flags)

Every command below accepts these in addition to its own flags:

| Flag               | Meaning                                                                 |
| ------------------ | ----------------------------------------------------------------------- |
| `--url <url>`      | Relay base URL. Overrides `HOMESPUN_URL` and the active profile.        |
| `--api-key <key>`  | Agent API key. Overrides `HOMESPUN_API_KEY` and the active profile.     |
| `--profile <name>` | Pick a saved profile for this invocation. Overrides `HOMESPUN_PROFILE`. |
| `-h`, `--help`     | Show help.                                                              |
| `--json`           | Accepted but currently a no-op, kept for forward compatibility.         |

Output is machine-readable JSON on stdout (JSON-lines for a couple of streaming verbs, noted below). Errors go to stderr as `{"error":{"code","message"}}` with a non-zero exit code.

### Config file and profiles

[Section titled “Config file and profiles”](#config-file-and-profiles)

`homespun agent register` provisions an API key and saves it, with the relay URL, to `${XDG_CONFIG_HOME:-~/.config}/homespun/config.json` under a named profile (mode `0600`). Afterwards, commands need no environment variables. Manage multiple environments (dev/staging/prod) with the `config` noun below.

## `deploy`: create or redeploy an app

[Section titled “deploy: create or redeploy an app”](#deploy-create-or-redeploy-an-app)

Terminal window

```
homespun deploy <dir|file> [--app <id>] [--manifest <path|json>] \
  [--slug <slug>] [--visibility private|link|public] [--force]
```

The only noun with no sub-verb: whether it creates or redeploys is decided by whether `--app` is present, not by two different commands.

**Packaging**, one canonical shape and one escape hatch:

* Directory (canonical): `homespun deploy ./my-app` reads `./my-app/index.html` and `./my-app/manifest.json`, fixed filenames, both required.
* Single file (escape hatch): `homespun deploy ./index.html --manifest ./manifest.json`. `--manifest` accepts a file path or inline JSON.

**Create vs. redeploy:**

|                | Create (no `--app`)                                                                                                                                                  | Redeploy (`--app <id>`)                                          |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Request        | `POST /v1/apps`                                                                                                                                                      | `POST /v1/apps/:id/versions`                                     |
| `--slug`       | Applies. Rejected only together with an explicit `--visibility link`: link-visibility slugs are always server-generated. Fine with private (the default) and public. | Rejected: the slug is immutable once set.                        |
| `--visibility` | Applies.                                                                                                                                                             | Rejected: change it with `homespun apps update` instead.         |
| `--force`      | N/A                                                                                                                                                                  | Overrides the compatibility gate on a narrowing manifest change. |

Output: `{ app_id, slug, url, version, visibility, created, compat?, breaks? }`.

Examples:

Terminal window

```
homespun deploy ./my-app
homespun deploy ./my-app --visibility public --slug my-app-slug
homespun deploy ./index.html --manifest ./manifest.json
homespun deploy ./my-app --app app_abc123
homespun deploy ./my-app --app app_abc123 --force
```

## `apps`: app lifecycle

[Section titled “apps: app lifecycle”](#apps-app-lifecycle)

Terminal window

```
homespun apps list   [--status active|dormant|archived|all] [--limit] [--cursor] [--slug <slug>]
homespun apps show   <app>
homespun apps update <app> --visibility <private|link|public>
homespun apps delete <app> [--yes]
homespun apps wake   <app>
homespun apps watch  <app> [--since <cursor>] [--collection <name[,name2,...]>] [--once] [--timeout <secs>]
```

`<app>` accepts either the `app_id` or its slug throughout the CLI, resolved via `GET /v1/apps?slug=` when it doesn’t look like a generated id.

`apps watch` streams the app’s change feed as JSON-lines on stdout, one entry per line, whether served over the live WebSocket or a long-poll fallback (used automatically if the WS upgrade fails). A dormancy transition mid-watch emits a single `{"type":"_dormant"}` line and exits cleanly.

`apps delete` is destructive (removes the app and all its data) and refuses to run without `--yes`.

Examples:

Terminal window

```
homespun apps list --status active --limit 20
homespun apps show app_abc123
homespun apps update app_abc123 --visibility public
homespun apps delete app_abc123 --yes
homespun apps wake app_abc123
homespun apps watch app_abc123 --since 42 --collection todos,comments --once --timeout 30
```

## `data`: collection row CRUD

[Section titled “data: collection row CRUD”](#data-collection-row-crud)

Terminal window

```
homespun data <app> <collection> list   [--since <cursor>] [--limit <n>]
homespun data <app> <collection> get    <key>
homespun data <app> <collection> upsert --data <path|json> [--key <key>]
homespun data <app> <collection> update <key> --data <path|json> [--if-match <version>]
homespun data <app> <collection> delete <key> [--if-match <version>] [--yes]
```

App and collection are positional arguments here, not flags. `upsert` is the only create-shaped verb: omit `--key` to add a new row with a server-generated key; pass `--key` to ensure a row exists at that key, which returns the existing row with `deduped: true` on a collision rather than erroring.

Note

`--yes` on `data delete` is accepted and appears in the usage line, but the current CLI doesn’t actually gate the delete on it, the deletion proceeds either way. Don’t rely on it as a confirmation prompt the way `apps delete --yes` and `key revoke --yes` genuinely are one.

Examples:

Terminal window

```
homespun data app_abc123 todos list --since 10 --limit 50
homespun data app_abc123 todos get row_key1
homespun data app_abc123 todos upsert --data '{"text":"buy milk"}'
homespun data app_abc123 todos upsert --data ./row.json --key mykey
homespun data app_abc123 todos update row_key1 --data '{"done":true}' --if-match 3
homespun data app_abc123 todos delete row_key1 --if-match 3
```

## `members`: app membership

[Section titled “members: app membership”](#members-app-membership)

Terminal window

```
homespun members add    --app <idOrSlug> --email <email> [--role member]
homespun members list   --app <idOrSlug>
homespun members remove --app <idOrSlug> --human <humanId>
```

`add`: if a Human already exists for that email, the member is attached immediately, response `{ member: { humanId, email, role, createdAt } }`. Otherwise the relay emails an invite link, response `{ ok: true, invited, expires_at }`. `member` is the only valid `--role`; there’s no ownership transfer here.

`remove` is idempotent and also revokes the human’s live sessions on this app. The app owner can never be removed this way (the relay returns a 409 conflict).

Examples:

Terminal window

```
homespun members add --app app_abc123 --email alice@example.com
homespun members list --app app_abc123
homespun members remove --app app_abc123 --human human_xyz
```

## `key`: your own API key

[Section titled “key: your own API key”](#key-your-own-api-key)

Terminal window

```
homespun key list
homespun key revoke --yes
```

Scoped to the calling agent’s own key only, there’s exactly one key per agent. `revoke` is a self-destruct: the key stops working immediately and every subsequent command fails until you run `homespun agent register` again.

## `agent`: this agent’s identity

[Section titled “agent: this agent’s identity”](#agent-this-agents-identity)

Terminal window

```
homespun agent register [--name <n>] [--profile <name>] [--secret <s>] [--print-key]
homespun agent claim <code>
homespun agent set-key <api-key> [--url <url>] [--profile <name>]
homespun agent logout [--profile <name>] [--all]
```

`register` provisions an API key and saves it locally (see “Config file and profiles” above). `claim <code>` binds this agent to the human who generated the one-time code (begins `cc_`); one-way, no unclaim. `set-key` saves a key you already have, e.g. after regenerating it in the web UI. `logout` clears the locally saved profile (or every profile, with `--all`); it does not revoke the key on the relay, use `key revoke` for that.

Note

`homespun agent <verb> --help` currently does not print verb-specific help, `--help` is silently accepted as a no-op and the command runs anyway. Use `homespun agent --help` (no verb) for the verb list; this page’s usage lines above are the verb-specific reference until that’s fixed.

## `config`: local profile management

[Section titled “config: local profile management”](#config-local-profile-management)

Terminal window

```
homespun config show
homespun config list
homespun config use <profile>
homespun config add <profile> --url <u> --api-key <k>
homespun config rm <profile>
```

Purely local: none of these make a network call. A profile is one `(url, api_key)` pair under a short name. The active profile is what every other command uses unless overridden by `--url` / `--api-key` or their environment-variable equivalents. The full API key is never printed, only a masked prefix.

## `taste`: this agent’s UI taste notes

[Section titled “taste: this agent’s UI taste notes”](#taste-this-agents-ui-taste-notes)

Terminal window

```
homespun taste get
homespun taste set --file <path|->
homespun taste clear --yes
```

A small markdown attachment recording presentation preferences a human has given feedback on (“denser table”, “no rounded corners”). Read it before generating a new app so prior feedback carries over; rewrite it whenever the human gives new presentation feedback. `set` reads from `--file`, from stdin via `--file -`, or from piped stdin with no flag at all. `clear` requires `--yes`.

Terminal window

```
homespun taste set --file ./taste.md
echo "- denser layout" | homespun taste set
homespun taste clear --yes
```

## `feedback`: one-shot notes to the relay operator

[Section titled “feedback: one-shot notes to the relay operator”](#feedback-one-shot-notes-to-the-relay-operator)

Terminal window

```
homespun feedback create --type <bug|feature|note> --message <text|-> [--app-id <id>]
homespun feedback list [--limit <n>] [--before <cursor>]
```

Terminal window

```
homespun feedback create --type bug --message "watch hangs on empty app"
homespun feedback list --limit 20
```

## `attachment`: binary attachments

[Section titled “attachment: binary attachments”](#attachment-binary-attachments)

Terminal window

```
homespun attachment upload --file <path> [--scope agent|app] [--app-id <id>] [--filename <name>] [--mime <type>]
homespun attachment download <attachment-id> [--out <path>]
homespun attachment show <attachment-id>
homespun attachment list [--cursor <token>] [--limit <n>]
homespun attachment delete <attachment-id>
homespun attachment token mint <attachment-id> [--ttl <seconds>] [--once]
homespun attachment token revoke <attachment-id> <token-id>
homespun attachment token list <attachment-id>
```

Attachments are scoped to an agent (default, reusable across the agent’s apps) or to an app (deleted with it). Reference an attachment from a row’s data with `format: homespun-attachment-id` in its schema. `token mint` produces a `/b/<token>` capability URL anyone can `GET` without an API key, useful for handing a human a direct download link; default TTL is 30 days for an app-scoped attachment and 24 hours for an agent-scoped one, and the caller can only shorten it, never extend it.

Terminal window

```
homespun attachment upload --file ./photo.png --scope app --app-id app_abc123
homespun attachment download attachment_id123 --out ./photo.png
homespun attachment token mint attachment_id123 --ttl 3600 --once
```

## `skill`: fetch the relay’s SKILL.md

[Section titled “skill: fetch the relay’s SKILL.md”](#skill-fetch-the-relays-skillmd)

Terminal window

```
homespun skill show
homespun skill version [--plain]
```

Unauthenticated, no API key required, useful for bootstrapping before `agent register`. `show` writes the raw markdown to stdout:

Terminal window

```
homespun skill show > ~/.claude/skills/homespun/SKILL.md
```

`version --plain` prints just the version string, handy in a shell comparison against a locally cached copy.
