stackbone link
linkregisters a deployment against the control plane, so its--agentis an agent slug, not an installation id: it does not use the shared target resolution. Its--jsonoutput follows the standard envelope, and the signing secret you pass is never printed back.
Link the current directory to an existing organization + agent, and register
the deployment you run that agent on. Writes .stackbone/project.json,
patches .gitignore and offers the same
coding agent setup that
stackbone init does. Does
not scaffold starter files; use init for that.
Connect your box writes the same record
from the browser, with no CLI and no image tag. Use link when you also want
the current directory linked, or when a deploy script does the registering.
You host the agent yourself: you build and deploy the container image into
your own cloud, then tell Stackbone where it lives. That is what the three
required flags do. --url is the public URL the container answers on,
--secret is the signing secret your container verifies with (it must match
exactly), and --tag is the image tag you deployed. The tag is the agent's
version: there is no separate build step to promote.
stackbone link \
--agent support \
--url https://support.acme.example.com \
--secret "$STACKBONE_HMAC_SECRET" \
--tag 1.4.0link validates all three flags before any network call, so a missing flag
fails the command before it half-links the project. Re-running link for the same
agent updates the existing deployment (the new URL, secret and tag win) rather
than creating a second one, so it is safe to run on every deploy.
| Flag | Type | Description |
|---|---|---|
--agent |
string | Agent slug to link to. Required in non-interactive mode (CI, --json, -y). |
--url |
string | Required. Public URL where the deployed agent is reachable. |
--secret |
string | Required. The signing secret the deployed container verifies with. Must match it exactly. |
--tag |
string | Required. Tag of the image you deployed (a semver or a digest). This is the agent's version. |
--force |
boolean | Overwrite an existing .stackbone/project.json. |
--agents |
string | Comma-separated coding agents to set up, e.g. claude-code,cursor. Skips the prompt. --no-agents sets nothing up. See coding agents. |
The deploy then link flow: start the container, read its signing secret,
then run
stackbone link --agent <slug> --url <url> --secret <value> --tag <tag>. The control plane signs
every request it later proxies to your box with this secret, and your box
verifies the signature, so the two values have to be identical.
Where the secret comes from: you do not have to invent one. On its first
boot the container mints a signing secret, stores it in its own database so
every restart reuses it, and prints it in the container log at warning level.
Copy that value into --secret. The container prints it only on the boot that
mints it, so grab it from that first boot. It lands in the log on purpose, so
mind who can read your container logs.
You can still pick the value yourself: set HMAC_SECRET on the container and it
wins over anything the box would mint. Do that when your platform injects
secrets from a vault and you want the value decided before the container starts.
link proves the secret before it saves anything. Before it registers the
deployment, link signs a challenge request to your container and your container
verifies it against the secret it holds, whichever way it got it. On a match,
link saves the secret (encrypted at rest). On a mismatch, it stops with an error
naming the difference and saves nothing, so a typo fails at link time instead
of breaking the proxy later. If your box is unreachable, link says so
and saves nothing.
link then teaches the box which installation it answers for. A box admits
Studio and CLI traffic only for the organization and installation it was
registered under. Right after the secret check, link sends both values to the
box, in the same prove, teach, then write order the browser flow uses. This
step fails closed: link does not register a box it cannot teach, so no
deployment record points at a box that refuses every call.
If you registered a deployment before these checks existed, re-run
stackbone link once to validate the secret and teach the box its identity.
link also refuses a box that is too old. Its first call is the box's public
handshake, read before the secret challenge. link stops there when the box
speaks an agent protocol older than version 15,
with the same wording the browser flow uses. A deployment you register from a
terminal is the same record the same screens read later. Letting the terminal
past would move the failure to the first screen that needs the box.
Deploy a current image and run link again.
The secret challenge also reports whether the box verifies browser identity. A box
that does not still links, because the deployment record and the secret are both good.
Studio and the CLI reach your box directly, and the only credential they carry
is that identity token. A box that cannot verify one refuses every Studio screen
and every box command with a 401. link warns and names
STACKBONE_CONTROL_PLANE_URL as the value to set on the container. A box old
enough not to answer reads as unknown, which is "cannot tell", not
"misconfigured". The reported state is the one link leaves the box in: a box
that only needed teaching reads as armed, because link taught it during
this run.
A box that is never told a control plane verifies against
https://api.stackbone.ai, the Stackbone control plane, and its first boot log
says which one it armed against. Only a box belonging to a self-hosted control
plane needs STACKBONE_CONTROL_PLANE_URL set.
JSON payload
{
"schema_version": 1,
"agent": { "id": "...", "slug": "...", "name": "..." },
"organization_id": "...",
"local_dev_installation": { "id": "...", "organization_slug": "..." },
"deployment": {
"id": "...",
"url": "https://support.acme.example.com",
"image_tag": "1.4.0",
"status": "...",
"last_active_at": "2026-07-21T09:00:00Z" /* or null before the first call */,
},
"target_dir": "/abs/path",
"files_written": [".stackbone/project.json", ".gitignore"],
"agent_setup": {
"ok": true,
"selected": ["cursor"] /* the coding agents you ticked, or [] */,
"steps": {
"skills": "ok" /* "ok" | "failed" | "skipped" */,
/* One entry for EVERY supported agent, always. The ones you did not tick read "skipped". */
"mcp": { "claude-code": "skipped", "cursor": "written" /* … */ },
},
},
"identity_verification": {
"state": "armed" /* or "unarmed", or "unknown" from a box that does not report it */,
"warning": null /* the sentence printed in human mode, when there is one */,
},
}Exit codes: 0 ok · 2 auth · 4 not found (no agent with that slug in
the active organization) · 3 no project (you have no agents yet) · 1 generic
(a missing flag, a directory already linked without --force, an unreachable
box, a secret that does not match, a box too old to register). Full table:
Conventions → exit codes.