CLI

Updated Aug 26, 2026View as Markdown

subs runs agents on your machine and administers a deployment.

curl -fsSL https://subs.dev/cli.sh | bash   # or: npm install -g @substructure.ai/cli

The script picks the release build for your platform, checks it against the published checksum, and installs to SUBS_INSTALL_DIR (default ~/.local/bin). Set SUBS_VERSION to pin a release instead of taking the latest.

Every command reads subs.toml from the working directory. Pass -c <path> to use another file. See Config.

Where a command acts

The [remote] section decides. A file that names one describes a deployment you administer, so that is what a command reads and writes. A file that names none describes an engine you run here, so a command reads and writes this machine's environment and this machine's database.

subs agents list        # no [remote]: the agents this file declares
subs agents list        # with [remote]: the agents the deployment holds

--url targets a deployment for one command whatever the file says, and --db targets a database the same way.

CommandWith no [remote]
sessions, agents list/show, list, auth, revoke, doctorReads the file and this machine.
run, chatThe turn runs on an engine here.
serveAlways runs here. This is the engine.
keys, open, config log, projects, orgs, agents secret/rotate-secretFails, and says how to get a deployment.

login, logout, and whoami are about a credential rather than a project. apply and link are how a file gets a [remote] in the first place. All five work before a file has one.

On this machine, a key or a signing secret is an environment variable that the file names, never a value that it holds. The commands that set one on a deployment tell you which variable to export instead.

$ subs auth llm.claude
Error: an engine here reads the key for [llm.claude] from $ANTHROPIC_API_KEY, so there is none to set.
  export ANTHROPIC_API_KEY=...

Commands

CommandDoes
subs runRun one turn and exit.
subs chatChat with an agent, holding one session open.
subs serveRun the engine as an HTTP server.
subs login / logout / whoamiAuthenticate against a deployment.
subs applyPush the file to the deployment.
subs doctorShow what the project still needs.
subs orgs listList the organizations you belong to.
subs projectsList, show, and delete projects.
subs agentsList agents and read their signing secrets.
subs keysCreate, list, and revoke API keys.
subs sessionsList sessions and stream their events.
subs listShow every connection and LLM block, and what each still needs.
subs auth / revokeFill or empty what a path holds.
subs linkPin an org and project in the file.
subs config logShow what changed the configuration, and who changed it.
subs openOpen the project's dashboard.

Global flags

Every cloud command takes these.

FlagMeaning
--urlThe API to talk to. Wins over the file's [remote].url.
-c, --configThe project file.
--credentialsThe credentials file. Default ~/.config/subs/credentials.toml.
--org / --projectTarget one org or project, instead of the file's pin.
--jsonPrint JSON instead of a table.
-n, --no-interactionNever prompt. Fail if input is required.

subs run

Sends one input, streams the turn, and exits.

subs run assistant "hi"
FlagMeaning
<agent>Which [agent.<id>] to run. Required.
<message>The message to send, as a user turn.
--inputA ClientInput in JSON. Its type selects submit, resume, or settle.
--sessionContinue a session. Omit it to start one.
-o, --outputpretty at a terminal, ag-ui when piped. Set it to ag-ui, jsonl, or pretty to decide.
--urlRun the turn on the deployment at this URL.
--dbThe SQLite file.
-cThe project file.

The CLI checks the agent against the file before it creates a session. After a turn, it prints the command to continue the session.

With a [remote], the turn runs on the deployment. Nothing starts here: no engine, no database, and no key, because the deployment holds them. The reply still streams as it is written.

$ subs run assistant "hi"    # no [remote]: an engine here, writing the local database
$ subs run assistant "hi"    # with [remote]: the deployment runs it

It uses the credential that subs login stored, so there is no API key to create and no token to mint. A turn started this way belongs to an operator, not to an end user.

-o jsonl streams the engine's own events. -o pretty and -o ag-ui stream the deployment's translation of them.

--url sends one turn to the server that it names, whatever the file says. Point it at a subs serve to use the engine that is already running there, instead of starting a second engine on the same database.

subs serve --no-auth                                   # one terminal
subs run --url http://127.0.0.1:8080 assistant "hi"    # another

subs chat

Holds one session open and reads the next message from you, instead of exiting after a turn.

subs chat assistant
FlagMeaning
<agent>Which [agent.<id>] to chat with. Required.
--sessionContinue a session. Omit it to start one.
--urlChat with the deployment at this URL.
--dbThe SQLite file.
-cThe project file.

Where the turn runs follows the same rule run does: the file decides, and --url overrides it. Ctrl-D ends the chat and prints the command to pick the session back up. walks what you typed before.

A turn that stops to ask something renders the question as a picker and your answer resumes it, so you do not type an interrupt ID. A question left unanswered stays open in the session, and a chat that opens that session asks it first. See Chat.

subs serve

Runs the engine as an HTTP server.

subs serve --no-auth
FlagMeaning
--hostThe address to bind.
--portThe port.
--dbThe SQLite file.
--no-authServe without client or worker authentication.
-cThe project file.

At startup it logs each declared agent and whether the engine or a worker decides for it. See Self-hosting.

subs login

subs login
subs login -c subs.prod.toml     # against the URL that file names

Authenticates with the OAuth device flow and stores a token under ~/.config/subs.

Without --url, the command uses the file's [remote].url, then $SUBS_API_URL, then the hosted cloud. The CLI stores credentials per server, so you can be logged in to several deployments.

subs apply

Pushes the file to the deployment. This is the only way to put configuration into one.

$ subs apply -c subs.prod.toml
Created project support-bot (proj_01hy…) in org_01hx…
Pinned proj_01hy… in subs.prod.toml
Applied 4 changes:
  llm.updated             claude (anthropic)
  agent.updated           support -> https://bot.example.com/agent
  mcp.connection_declared sentry (pending authorization)
  mcp.grant_added         sentry
Signing secrets minted:
  support: `subs agents secret support`

Action required:

  Authorize the [mcp.sentry] connection
    subs auth mcp.sentry -c subs.prod.toml
FlagMeaning
--nameName the project, when the file does not. Written back with the pin.
--orgThe org to create the project in. Only used when nothing is pinned.

Apply creates the project when the file pins none. It is idempotent. An unchanged file prints No changes. and exits 0.

Apply replaces. An agent, an LLM block, or a Slack app that is not in the file is one you removed.

$ subs apply -c subs.prod.toml
Applied 1 changes:
  agent.removed           triage

Apply never sends a secret. It strips api_key_env and signing_secret_env. A deployment refuses a document that carries one. See Cloud.

subs doctor

Lists every setup step that nobody has finished.

$ subs doctor -c subs.prod.toml

Action required:

  `agent.oncall.slack` has no Slack app yet
    subs auth agent.oncall.slack -c subs.prod.toml

  Set the key for [llm.claude]
    subs auth llm.claude -c subs.prod.toml

With a [remote], doctor asks the deployment. Without one, it reads this machine: the variables that are empty, and the connections with no credential.

$ subs doctor

Action required:

  Set $ANTHROPIC_API_KEY, the key for [llm.claude] (https://console.anthropic.com/settings/keys)
    export ANTHROPIC_API_KEY=...

  Authorize the [mcp.linear] connection
    subs auth mcp.linear

--json prints the list as JSON. With nothing left, doctor prints Nothing to do.

subs orgs

subs orgs list

Lists the organizations you belong to, with your role in each.

subs projects

subs projects list
subs projects show <project-id>
subs projects delete <project-id>

list shows the projects in the org the file pins, or the org you pick. show reads one project. delete removes a project and its keys, agents, sessions, and grants, and only an owner can run it. The configuration history stays. Omit the ID and each command uses the project the file pins.

A project exists because someone applied a file to it. subs apply is what creates one. See Cloud.

subs agents

$ subs agents list
ID       LLM     MODEL                HOSTING                          SECRET
support  -       -                    https://bot.example.com/agent    set
triage   claude  claude-sonnet-4-5    engine                           -

$ subs agents show support          # never the signing secret
$ subs agents secret support        # the secret, on stdout
$ subs agents rotate-secret support # the old secret stops working at once

An agent exists because the file declares it. There is nothing to create.

Only secret and rotate-secret print a signing secret, so nothing else you run can leave one in a terminal or a log.

With no [remote], list and show read the file, and SECRET names the variable a worker-hosted agent signs with.

$ subs agents list
ID       LLM     MODEL                HOSTING                        SECRET
support  -       -                    https://bot.example.com/agent  $SUPPORT_SECRET
triage   claude  claude-sonnet-4-5    engine                         -

An engine here holds no secret to print or rotate, so those two commands say where the secret is instead.

subs list

Everything the file declares, named by where it is declared, and whether each one holds what it needs.

$ subs list
PATH                   WHAT                            CREDENTIAL
llm.claude             anthropic                       set
mcp.sentry             https://mcp.sentry.dev/mcp      authorized
plugin.pdf.mcp.render  https://pdf.example.com/mcp     no token set

With no [remote], a block reports the variable it takes its key from, because an engine here reads it from this machine rather than from the deployment.

$ subs list
PATH        WHAT       CREDENTIAL
llm.byo     worker     n/a (your worker runs it)
llm.claude  anthropic  $ANTHROPIC_API_KEY

subs keys

subs keys create backend    # prints the key once, to stdout
subs keys list
subs keys revoke <key-id>

create takes the label as a positional argument. Pipe it into your secret store. See Authentication.

subs sessions

subs sessions list
subs sessions events <session-id> --stream

subs sessions list takes --session-id and --agent-id to filter, and --limit and --cursor to page.

subs sessions events takes a session ID. Omit it and the command prompts you to pick from recent sessions.

FlagMeaning
--fromOnly events after this sequence number.
-f, --streamFollow the session live.
-o, --outputjsonl (default), pretty, or ag-ui.
--dbRead this SQLite database.

Sessions come from whichever store holds them, by the rule in Where a command acts. With no [remote], the commands read ~/.config/subs/subs.db directly, the same sessions subs run and subs serve write, unless the file names another db.

$ subs run oncall "hi"
...
$ subs sessions list
SESSION_ID                            AGENT    FIRST_EVENT           LAST_EVENT
0198f2c1-...                          oncall   2026-08-10T14:02:11Z  2026-08-10T14:02:14Z

--db <path> reads that file whatever the config says, and --url asks that server. Locally, --stream polls the file, so it follows a subs serve that runs beside it.

-o pretty replays a session as text, the way subs run prints a live turn. Each turn is its own run, so a session of many turns replays as many runs.

$ subs sessions events <session-id> -o pretty
Hello!
Goodbye!

See Events.

subs auth and subs revoke

One command for whatever a path needs. subs auth reads what the file declares and does what that asks: a browser for consent, a prompt for a token, or a prompt for a key.

subs auth   mcp.sentry                 # opens a browser
subs auth   plugin.pdf.mcp.renderer    # takes a token
subs auth   agent.oncall.slack         # takes a Slack app's two values
subs revoke plugin.pdf.mcp.renderer
FlagMeaning
<path>Where the credential is declared. Optional when the file declares exactly one connection.
--no-browserPrint the authorization URL instead of opening a browser.

A credential never appears in the command line. Pipe it in, or let the command prompt for it. subs revoke empties every holder's, whichever way it was obtained. See Connectors.

A Slack app takes two values, so agent.<id>.slack asks for both. Piped, it reads them from two lines.

printf '%s\n%s\n' "$BOT_TOKEN" "$SIGNING_SECRET" | subs auth agent.oncall.slack

The command prints the app manifest first, unless the app already has its values. See Slack.

Pins an org, and usually a project, into the file, so every later command in this tree finds them.

subs link                          # pick an org and a project
subs link --org org_01hx… --project proj_01hy…
subs link --project=               # pin the org alone
FlagMeaning
--orgThe org to pin. Skips the org picker.
--projectThe project to pin. Skips the project picker.
--forceRepin a file that already names an org or a project.

Use it for a project that already exists: a fresh clone, or a teammate's machine. subs apply writes the same pin when it creates a project, so you need link only when the project is already there.

Link writes [remote].org and [remote].project and keeps everything else, so relinking preserves the engine settings, connections, and worker that the file declares.

subs config log

$ subs config log -c subs.prod.toml
2026-07-30 14:02  alex@…  apply  config.applied           hash 6f2a1c9d4b7e
2026-07-30 14:02  alex@…  apply  mcp.grant_added          sentry
2026-07-30 14:02  alex@…  apply  agent.updated            support -> https://bot.example.com/agent
2026-07-30 14:02  alex@…  apply  project.created          support-bot

subs open

Opens the project's dashboard in your browser.

subs open
subs open <project-id>
subs open --no-browser      # print the URL instead

Without an ID it opens the project the file pins. This is the only command that needs a browser, and --no-browser removes even that.

Next steps