Skip to content

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 [email protected]

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.

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

FlagMeaning
--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, --helpShow help.
--jsonAccepted 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.

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.

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>)
RequestPOST /v1/appsPOST /v1/apps/:id/versions
--slugApplies. 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.
--visibilityApplies.Rejected: change it with homespun apps update instead.
--forceN/AOverrides 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
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
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.

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
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 [email protected]
homespun members list --app app_abc123
homespun members remove --app app_abc123 --human human_xyz
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.

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.

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.

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”
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
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
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.