Skip to content

CLI reference

checked against the binary in CI

Every verb below appears in printHelp (packages/cli/src/cli/main.ts). A script in site/scripts/check-cli-reference.mjs runs in CI and fails if this page lists a verb the binary does not have, or omits one it does.

Two conventions apply throughout:

  • <target> is project when the project holds exactly one resource, and project/resource otherwise.
  • --json is accepted by every command that returns data, and prints the same object the human output was rendered from, so the two cannot drift.

Prepares the host. Creates ~/.hobby, checks that Docker is reachable, and inspects the filesystem for reflink support and the host networking Caddy would need. Warns rather than failing on either.

Accepts --json.

Runs the daemon in the foreground. It owns all state and all lifecycle, listens on a unix socket for the CLI and MCP, and on loopback TCP for Studio. Only one may run at a time, and that is enforced rather than assumed.

Takes no flags.

Creates a project, puts a postgres resource named primary in it, waits for that database to accept queries, and prints a connection string.

--empty creates the project with nothing in it.

Every project, with every resource and its sleep state. A released project is still listed, marked (released, not managed). An undeployed app or worker is marked (no code yet).

A resource whose last automatic wake failed is marked with when the daemon will try it again and a short form of the error. Until then a connection or request is answered with an error instead of starting it again. The wait starts at 30 seconds and doubles with each failure in a row, up to 15 minutes. A successful wake resets it. hobby wake retries at once, and hobby logs shows why the start failed.

blog
primary postgres running port 15000
site app sleeping site.blog.example.com
api app failed api.blog.example.com (wake refused, retry in 3m 12s: docker start failed)

With --json, the same thing is the resource’s wakeRefusal field: { failures, retryAt, lastError }, or null.

Accepts --json.

hobby create <postgres|app|worker> <name> --project <p>

Section titled “hobby create <postgres|app|worker> <name> --project <p>”

Creates a resource row with no code deployed to it, resting in the state undeployed. Deliberately takes no --port: a port describes code that does not exist yet.

Accepts --json.

The explicit form, for creating a second database. An alias for hobby create postgres, not a second implementation.

Accepts --json.

hobby branch <project> <name> [--allow-pause]

Section titled “hobby branch <project> <name> [--allow-pause]”

Makes a new project, <name>, from a copy of <project>’s Postgres data, so you can experiment against real data without risking it. On a filesystem with reflinks (XFS, ZFS, APFS) the copy is instant; on ext4 it is a real copy, and the command says so.

The branch gets its own container, port, network and data directory, and keeps the source’s database user and password, which live inside the copied data. It starts asleep, like hobby new, and is never pinned, whatever the source was.

An asleep source is copied as it is. An awake one is paused for the length of the copy and then resumed, and the command prints how long the pause was. A pinned source that is awake is refused unless you pass --allow-pause, because pinning it said it must stay up. A project holding apps, workers or queues cannot be branched yet. Remove a branch with hobby rm.

Accepts --json.

Destroys a resource, or a project, with confirmation. --yes skips the prompt.

Builds what is in the directory and serves it. The kind is detected from the contents: a Dockerfile means an app, a wrangler.toml, wrangler.jsonc or wrangler.json means a worker. Both present is an error rather than a guess.

Flag What
--project <p> Which project it belongs to
--name <n> The resource name
--port <n> The port your process listens on inside the container. Apps only
--env-file <f> Variables from a dotenv file, replacing the app’s current set. Apps only
--kind app|worker Only needed when the directory is ambiguous
--json Machine-readable output

Opens psql against a Postgres resource. The connection string never becomes an argv element: credentials are passed through the child’s environment, so nothing reading a process list can see the password.

--json prints connectionString and, when the daemon has a tailnet address, tailnetConnectionString.

If psql is not on PATH, the connection string is printed instead.

Stops the resource now, rather than waiting for it to go idle. Accepts --json.

Starts it now. Rarely necessary: a resource wakes by itself when something connects, which is the whole point. wake is for when you want it warm before something else needs it, or when its last wake failed, you have fixed the cause, and you do not want to wait for the daemon’s own retry (see hobby ls). Accepts --json.

Stops this project ever being put to sleep automatically. Useful for the one thing on the box that has to answer instantly, or anything whose first request genuinely cannot afford to wait.

Pinning is per project, not per box, so pinning your status page does not keep nine other projects awake with it. Accepts --json.

hobby unpin <project> [--sleep-after <seconds>]

Section titled “hobby unpin <project> [--sleep-after <seconds>]”

Lets it sleep again. Without the flag it goes back to the box-wide default.

If the box-wide default is itself “never sleep”, unpin refuses rather than writing a value that would make it a no-op reporting success, and asks you for an explicit number.

Accepts --json.

Tails the resource’s logs.

Every queue, with depth and its bound consumer, or a note that it has none.

A queue with no consumer bound yet. Bind one through a worker’s own wrangler.toml.

The oldest messages, without leasing them, so looking does not change what the consumer will receive.

Enqueues one message. Works on Linux and macOS, unlike producing from inside a container.

Deletes every message, with confirmation.

Destroys the queue, with confirmation.

hobby queue set <target> --retention <seconds>

Section titled “hobby queue set <target> --retention <seconds>”

Changes how long messages are kept. Note that retention never sweeps a queue with no drainable consumer.

These are the commands that make the CLI work from your laptop against a box. See ADR 0018 for the reasoning, and the remote guide for a walkthrough.

Exchanges the operator password for an API token and stores it. Every command afterwards runs against that box instead of the local socket.

Terminal window
hobby login https://your-box.tailnet.ts.net

The password is read without echoing and never becomes a command-line argument. --name labels the token, so you can tell one machine’s from another’s when revoking; it defaults to your hostname.

It refuses to send a token to a plain http:// origin unless it is loopback. A token can create and destroy every database on the box, and that is not a thing to send in cleartext.

Forgets a remote on this machine. The token stays valid on the box until you revoke it there with hobby token rm, and the output says so.

Which boxes this machine is logged in to, with the current one marked.

Set HOBBY_REMOTE=<url> to aim a single command at a different one without changing the default.

Writes the project name into the hobby.json in the current directory, so commands run from there default to it and stop needing --project.

Terminal window
cd ~/code/blog
hobby link blog
hobby create worker api # no --project needed

An explicit --project always wins over the linked one. The project must already exist: link checks, rather than writing a file that points at nothing.

Issues an API token. Local only, over the unix socket, so tokens are minted by someone with shell access to the box rather than by anyone already holding one. Shown once and never recoverable: only an argon2id hash is stored.

Names, creation dates, and the last six characters of each token, which is enough to match one against a value a machine is holding without the file containing anything usable.

Revokes it. Any machine holding it is locked out on its next request.

Moves the checkout to the newest release tag and rebuilds it by re-running install.sh, which is the same path a fresh install takes.

Terminal window
hobby update # update to the newest tag
hobby update --check # print the newest tag and change nothing

It refuses if the checkout has tracked modifications rather than discarding them. Untracked files do not block it, which matters because bun install leaves a bun.lock in every built checkout. If the rebuild fails it says the checkout has already moved, because believing you are still on the old version would be worse than the failure. Restart the daemon afterwards.

Prints the Studio URL and opens it. Prints the public host instead of the loopback URL when one is configured through Caddy.

Sets the operator password. The prompt never echoes.

A snapshot is a copy of a whole project’s data, taken on this box. It is local: it does not survive losing the disk, and the command reminds you of that. There is no point-in-time recovery (see ADR 0016).

Takes a snapshot. The project is held asleep for the length of the copy, so nothing can wake it halfway through, and whatever was running is started again afterwards. A pinned project that has something running is refused unless you pass --allow-pause, because pinning it said it must stay up; if it holds only Postgres, --online (below) snapshots it without stopping anything. Prints the snapshot’s id.

Accepts --json.

Takes a snapshot with no pause at all, for a project whose resources are all Postgres; a project with an app, worker or queue is refused, because their state has no online copy mechanism. A running database is copied with Postgres’s own online backup, pg_basebackup, run inside its container, and a sleeping one is copied as it rests. Nothing is stopped, so a pinned project needs no --allow-pause, and the two flags cannot be combined.

A database restored from an online snapshot runs recovery on its first start, replaying the write-ahead log that came with the backup, before it accepts connections. Each database is consistent on its own; two databases in one project are copied one after the other rather than at one instant. There is still no point-in-time recovery.

Accepts --json.

The project’s snapshots, newest first. Snapshots outlive their project, so this still works after the project is deleted. Accepts --json.

hobby snapshot restore <project> <id> [--as <name> | --in-place] [--allow-pause] [--yes]

Section titled “hobby snapshot restore <project> <id> [--as <name> | --in-place] [--allow-pause] [--yes]”

Without --in-place, restores into a new project, <project>-restored or the name given with --as, and leaves the original untouched.

--in-place replaces the project’s own data and keeps its connection strings. The snapshot is copied first while the project stays up, then the project is stopped, the data swapped, and whatever was running started again. The old data is kept until everything has come back, and its path is printed if anything did not. It asks you to type the project name unless --yes is given, and it refuses if the project’s resources have changed since the snapshot. It restores data, not configuration.

Accepts --json.

Deletes one snapshot, with confirmation. --yes skips the prompt.

Emits a docker-compose.yml plus the data. --release also stops Hobbyist managing the project.

Manages a released project again.

Code Means
0 Success
2 Usage error: an unknown flag, a missing argument
non-zero The operation failed, or the daemon could not be reached

A daemon that cannot be reached says so and suggests hobby init then hobby daemon, rather than failing with a bare status.