Configuration reference
Global flags, environment variables, persistent state on disk, the
--jsonenvelope and the semantic exit codes. Everything you need to drive the CLI from a script (or from a coding agent inside an IDE) without parsing prose.
Global flags
Every command accepts these. The CLI reads them from process.argv before
subcommand parsing, so they work wherever you put them on the line.
| Flag | Env equivalent | Default | Description |
|---|---|---|---|
--json |
STACKBONE_JSON=1 |
off | Emit machine-readable JSON wrapped in { "schema_version": 1, ... }. Errors get error + exit_code keys. |
-y, --yes |
(none) | off | Skip every confirmation prompt. Required in CI / non-TTY contexts. |
--verbose |
STACKBONE_VERBOSE=1 |
off | Stream the raw firehose of log lines and docker compose output instead of the per-stage spinner UI. |
--version (or -v) is a root shortcut rather than a per-command flag. Alone on the
line it prints two lines and exits 0: the CLI's own version, then the agent
protocol contract version this binary advertises.
$ stackbone --version
stackbone-cli 0.3.3
contract 15Environment variables
The CLI reads these from the parent shell:
| Variable | Effect |
|---|---|
STACKBONE_JSON |
=1 ⇒ same as --json. |
STACKBONE_LOG_LEVEL |
Log level on stderr (trace, debug, info, warn, error, fatal). Defaults to warn on a terminal, so the per-stage spinners carry the UI, and to info under --json or --verbose. An explicit value wins over both. |
STACKBONE_API_URL |
Control-plane the CLI talks to. Wins over the project's .stackbone/project.json and your global config. Point it at your own deployment when you run Stackbone in-house. |
STACKBONE_FRPC_BIN |
Absolute path to a frpc binary. Used by stackbone dev for the relay tunnel. |
STACKBONE_FRPC_VERSION |
Override the pinned frpc release the auto-fetcher downloads. |
STACKBONE_DOCKER_BIN |
Path to a Docker-compatible binary stackbone dev drives. Defaults to docker. Set it for Podman or a non-standard install. |
STACKBONE_DEV_PORT_OFFSET |
Non-negative integer that shifts every port stackbone dev binds by offset × 10, so two projects can run side by side. See below. |
STACKBONE_POSTGRES_URL |
Connection string the stackbone db migrate verbs run against. Wins over the connection a running stackbone dev records for them. Set it to migrate a database that is not your local dev one. |
STACKBONE_VERBOSE |
=1 ⇒ same as --verbose (raw firehose of log + docker compose output). |
STACKBONE_CORS_ALLOW_ORIGINS |
CSV of origins added to the stackbone dev Studio CORS allowlist. |
MODEL_PROVIDER_API_KEY |
Your own model-provider key (OpenRouter, or any OpenAI-compatible gateway). When set in your shell it wins over the provider configured in the Studio Model provider screen. |
MODEL_PROVIDER_BASE_URL |
Override the OpenAI-compatible base URL the model client uses. Defaults to OpenRouter cloud. Enough on its own for a keyless local gateway (Ollama, LM Studio). |
OPENROUTER_API_KEY |
Deprecated alias for MODEL_PROVIDER_API_KEY, read only when the neutral name is unset. |
OPENROUTER_BASE_URL |
Deprecated alias for MODEL_PROVIDER_BASE_URL, read only when the neutral name is unset. |
STACKBONE_NO_UPDATE_CHECK |
=1 ⇒ silence the "a new release of the Stackbone CLI is available" banner. The check is a background local-cache read that never blocks a command, and it is already suppressed in CI, on a non-TTY, and under --json. |
Either model-provider variable also opens the first-run gate. Export one and
stackbone dev never stops for setup: it goes straight to your workflows and
agents. See The first run.
Running two projects at once
One stackbone dev session owns a fixed set of ports, so a second one on the
same machine collides. STACKBONE_DEV_PORT_OFFSET shifts the whole set: the API
port, and the Postgres, Redis and MinIO containers behind it.
STACKBONE_DEV_PORT_OFFSET=1 stackbone dev # API on :4252, Postgres on :5443, …Each step of the offset is worth 10 ports. 0 (the default) keeps the ports you
already know. An explicit --port always wins for the API port, but the
containers still shift, so pass the offset rather than only overriding --port.
stackbone dev also injects the env vars your agents and workflows read
through the ambient stackbone client (STACKBONE_POSTGRES_URL,
WORKFLOW_REDIS_URL, STACKBONE_AGENT_ID, …). See
SDK integration → how the CLI connects to the SDK.
State on disk
Per-machine (global)
Lives under ~/.stackbone/:
| Path | What it stores | Permissions |
|---|---|---|
~/.stackbone/credentials.json |
Map keyed by control-plane URL → SessionInfo. Lets one user stay logged into local + staging + prod simultaneously; only one is active at a time. |
0600 (owner read/write). |
~/.stackbone/config.json |
Global preferences: defaultApiUrl, telemetry (opt-in PostHog, default false until V1). |
0644. |
~/.stackbone/update-check.json |
The newest published CLI version seen on the last registry check, and when that check ran. Only ever read to print the upgrade banner. | 0600 (owner read/write). |
Per-machine (cache)
| Path | What it stores |
|---|---|
~/.cache/stackbone/bin/ |
Auto-downloaded binaries (today: frpc-<version>). |
Per-project
Lives at the project root:
| Path | Tracked? | What it stores |
|---|---|---|
stackbone.config.ts |
Optional | An optional workflow + build override. By default stackbone dev derives the workspace registry by convention (scanning every deep-agents/<name>/index.ts and every workflows/<name>.workflow.ts file), so most projects need no config file at all. When this file is present (default-exporting defineWorkspace({ agents, workflows }) from @stackbone/sdk) its workflows win over the convention scan; deep agents keep coming from the deep-agents/ scan unless you also list them under deepAgents, which you only need to hide one from Studio with $internal: true. agents and workflows are required fields: keep agents: []. It also carries build.external (see Keeping a package out of the bundle). See Workflows. |
.stackbone/project.json |
No (gitignored by stackbone init) |
{ schemaVersion, organizationId, agentId, controlPlaneUrl }, plus localDevInstallationId once stackbone dev has registered one. Identifies which organization this workspace points at, and which installation a verb targets when you pass no --agent. |
.stackbone/dev/session.json |
No (gitignored) | Written by stackbone dev while it runs: where the emulator listens and the connection string of the dev database. It is how a verb with no --agent finds the local installation, and how stackbone db migrate finds the database without you exporting anything. |
.stackbone/*.d.ts |
No (gitignored) | Generated editor types (config.d.ts, agents.d.ts, workflows.d.ts, connect.d.ts) that give the ambient stackbone client typed autocompletion. Regenerated by stackbone dev; config.d.ts also by stackbone config types (no control plane needed). |
stackbone.config.json |
Yes | { schemaVersion: 1, studio: { corsOrigins } }, the team-shared knobs. Reviewable in PRs. Different file from stackbone.config.ts above: the .json holds CLI/Studio knobs, the .ts holds the workspace manifest. |
agent.yaml |
Yes | Manifest. See agent.yaml reference. |
.claude/, .windsurf/, .agents/, … |
No (gitignored by stackbone init / link) |
Per-coding-agent skill directories the upstream skills registry materialises. See Agent skills overview. |
.mcp.json, .cursor/mcp.json, .vscode/mcp.json, .gemini/settings.json |
Yes | The stackbone-docs MCP server entry init / link write for the coding agents you tick. Committable on purpose: a teammate who clones the repository inherits the documentation server. See coding agents. |
Keeping a package out of the bundle
stackbone dev and stackbone build compile your deep agents the same way: each
deep-agents/<name>/index.ts is bundled into a single index.mjs next to it, with
your own dependencies inlined. The local run and the deployed container behave
alike because production resolves no dependencies a second time.
A few packages break when you bundle them: ones with a native binary, a WASM asset,
or that read their own files from disk at runtime. Name those in build.external and
they stay bare imports, resolved from the box's node_modules at boot:
import { defineWorkspace } from '@stackbone/sdk';
export default defineWorkspace({
agents: [],
workflows: [],
build: { external: ['sharp'] },
});The list applies to both paths. stackbone build records the full effective set
in the bundle manifest and warns about it at the end of the build, so whoever
packages the image knows which packages to install alongside it. Nothing installs
them for you. The image carries a node_modules decided at image build time, so
add these packages yourself, at the same versions this workspace uses. Miss one and
the agent fails to start with ERR_MODULE_NOT_FOUND.
@stackbone/sdk, deepagents and @langchain/* are always external, because
exactly one copy of each has to exist per process. You never list them.
The compiled index.mjs is a build artifact: stackbone init already gitignores
deep-agents/*/index.mjs.
Output contract
Human mode (default)
Free-form text on stdout. May span multiple lines, may include @clack/prompts
chrome and ANSI colour codes. Never parse this. Logging (debug,
trace, deprecation warnings) goes to stderr through Pino regardless of
mode.
JSON mode
Trigger it with --json or STACKBONE_JSON=1. Every command emits a single
JSON line on stdout wrapped in:
// success
{ "schema_version": 1, ...payload }Errors go on stderr and include the original exit code:
{
"schema_version": 1,
"error": {
"code": "auth",
"message": "No active session for https://api.stackbone.ai",
"suggestion": "Run `stackbone login`",
},
"exit_code": 2,
}schema_version is an integer that only bumps on
backward-incompatible shape changes. If a coding agent sees a value
greater than the one it was written against, it should refuse to parse
and surface the version mismatch.
Paginated lists
A cursor-paginated list command (runs list, hitl list, the storage,
rag and db table listings) takes --limit <n> and --cursor <opaque>,
and its JSON payload carries an items array plus nextCursor and
prevCursor (either may be null). To walk forward, pass the previous
page's nextCursor value back as --cursor. Some lists add domain-specific
fields alongside items (for example common_prefixes on a storage
listing); on a paginated list the three keys are always present and always
shaped the same way. A short list (stackbone list,
stackbone workflows list) returns everything at once, with no cursor keys.
{
"schema_version": 1,
"items": [
/* … */
],
"nextCursor": "eyJ…",
"prevCursor": null,
}Exit codes
| Code | Name | When |
|---|---|---|
0 |
ok | Command succeeded. |
1 |
generic | Catch-all error. Anything that isn't auth / project / not-found / permission. |
2 |
auth | Not logged in or session expired. Protected commands never auto-trigger login: run stackbone login first. |
3 |
no project | The command needs a target it can't resolve: the cwd has no .stackbone/project.json, or it targets the local-dev agent but stackbone dev is not running (dev_not_running). |
4 |
not found | The target does not exist: an unknown workspace, agent, run or object, or an agent with no registered deployment (runtime_not_configured). |
5 |
permission denied | Caller is authenticated but unauthorised for the resource. |
error.code in the JSON envelope carries the same decision at a finer grain.
Five names match the rows above one for one (generic, auth, no_project,
not_found, permission). The rest fold into one of those exit codes and keep
a name of their own, so a caller can tell two failures with the same integer
apart:
error.code |
Exit | Raised when |
|---|---|---|
auth_required |
2 |
The dev tunnel needs a session and there is none. |
dev_not_running |
3 |
The verb targets your local installation but stackbone dev is not up. |
runtime_not_configured |
4 |
The agent has no registered deployment, so no runtime can answer. |
workspace_tunnel_cap_reached |
5 |
Your workspace already holds the maximum number of live tunnels. |
unsupported_platform |
1 |
The CLI started in a native Windows shell (PowerShell or CMD). |
tunnel_relay_unhealthy |
1 |
The tunnel relay is down and cannot grant a tunnel. |
cli_upgrade_required |
1 |
The control plane refuses this CLI version. Upgrade it. |
eval_below_threshold |
1 |
stackbone eval ran a suite to the end and it missed the pass mark. |
Branch on the integer for the coarse answer, on error.code when you need the
distinction. Neither needs the message parsed:
# In a shell script
if ! out=$(stackbone whoami --json 2>err); then
case "$(jq -r '.error.code' < err)" in
auth) stackbone login ;;
not_found) echo "stale session" ;;
*) echo "unexpected: $out" ; exit 1 ;;
esac
fiThe six exit codes are stable across CLI releases, so scripts and coding agents
can pin to them. A new error.code name can appear inside an exit code that
already exists, so treat a name you do not recognise as its integer.
Putting it together
A coding-agent-friendly invocation looks like:
STACKBONE_JSON=1 stackbone metadataIt produces one line describing the organization state.
Not on this page: the stackbone config document
This page is about CLI and on-disk configuration: flags, env vars, and
the files that live in your project. It is not about the versioned
AGENT_CONFIG document your running agents and workflows read at runtime
through the ambient client (stackbone.config.get(...)). That runtime
config is its own surface, edited with the stackbone config command family
(get, set, versions, rollback, types) and shaped by a
config.schema.ts in your project. See the
config module reference.