stackbone link

link registers a deployment against the control plane, so its --agent is an agent slug, not an installation id: it does not use the shared target resolution. Its --json output 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.0

link 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.

BUILT WITH ❤️ FROM CANADA AND SPAIN