--- title: 'link' description: 'Link the current directory to an agent and register the deployment you run it on' position: 8 --- # 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](/docs/cli/reference/conventions#target-resolution). > Its `--json` output follows the [standard envelope](/docs/cli/reference/conventions#json-output), > and the signing secret you pass is [never printed back](/docs/cli/reference/conventions#secrets-are-never-printed). 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](/docs/home/get-started/coding-agents) that [`stackbone init`](/docs/cli/reference/init) does. Does **not** scaffold starter files; use [`init`](/docs/cli/reference/init) for that. [Connect your box](/docs/home/deployments/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. ```sh 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](/docs/home/get-started/coding-agents). | **The deploy then link flow:** start the container, read its signing secret, then run `stackbone link --agent --url --secret --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](/docs/cli/protocol/overview) 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** ```jsonc { "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](/docs/cli/reference/conventions#exit-codes).