stackbone link
linkregisters a deployment against the control plane. Its--agentis an agent slug and its--installationis the id of the cloud installation the box answers for: 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 that serves one of its cloud installations: the box you run.
It writes .stackbone/project.json, patches .gitignore and offers the same
coding agent setup that
stackbone init does. It 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, for the installation you opened
it from. Use link when you also want the current directory linked, or when a
deploy script does the registering.
link is for a box on your own infrastructure. On Stackbone Cloud, Stackbone
runs the box for you. On your own infrastructure you host the agent yourself:
you build and deploy the container image into your own cloud, then tell
Stackbone where it lives and which installation it serves. That is what the
four required flags do. --installation is the id of the cloud installation
this box answers for, --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 \
--installation 3f1c2a4e-9b7d-4c1e-8a2f-5d6e7f8a9b0c \
--url https://support.acme.example.com \
--secret "$STACKBONE_HMAC_SECRET" \
--tag 1.4.0A box answers for exactly one installation, so the deployment record belongs
to the installation, not to the agent. Find the id with
stackbone agents list: it is the id of a
cloud row of the agent you are linking. Two installations of the same agent
are two boxes, each registered with its own link. link never creates an
installation; install the agent first, then link its box.
link validates its flags before any network call, so a missing flag fails the
command before it half-links the project. Without --installation it stops and
names stackbone agents list. --url must be an https:// address: a
plain http:// one is refused here and by the control plane, whatever the host,
loopback included. The signing secret proves who sent a request, it does not
encrypt it, so an unencrypted box would carry every control-plane callback and
every call from the browser in clear text. Terminate TLS at your own proxy and
register the box by that address. It then checks the id against the control plane
before it touches the box: an id from another agent, an id outside your
organization, or the id of a local-dev installation (served by its
stackbone dev tunnel, never by a registered box) is refused, and nothing is
taught or written.
Re-running link for the same installation 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. When that installation already answers somewhere,
link prints the address it is about to replace before proceeding:
Installation <id> already answers at <old-url>; this link replaces that registration with <url>.
| Flag | Type | Description |
|---|---|---|
--agent |
string | Agent slug to link to. Required in non-interactive mode (CI, --json, -y). |
--installation |
string | Id of the cloud installation this box answers for: a cloud row of the linked agent in stackbone agents list. Required, unless --force re-links a directory whose .stackbone/project.json remembers one. link never creates an installation. |
--url |
string | Required. Public URL where the deployed agent is reachable. https:// only — a plain http:// address is refused before any network call. |
--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. Without --installation, reuses the installation id that file remembers, as long as the file names the same agent. |
--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> --installation <id> --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 or registers anything. It 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.
The installation it teaches is the agent's cloud installation, and link
creates one when the agent has none yet. It is never the local-dev installation
stackbone dev runs against: that one is addressed
through the dev tunnel and would ignore the deployment you just registered.
If you registered a deployment before these checks existed, re-run
stackbone link once to validate the secret and teach the box its identity.
The same applies after the registry moved from one record per agent
to one per installation: every box registered before that change was reset to
pending and has to be registered again, once, with --installation. See
Connect your box.
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 you never point at 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": "...",
/* the installation the box was taught and the deployment record is keyed on */
"installation": { "id": "...", "organization_id": "...", "kind": "cloud" },
"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 [] */,
"skillsAvailable": true /* the agent skills are on disk */,
"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 */,
},
"deployment_reachable": {
"reachable": true,
"reason": null /* what stops it, when something does */,
"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 missing or wrong --installation, 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.