Plane docs
SDK and CLI

@plane/sdk/dev

The outbound channel client, behind npx plane dev.

Your app opens a socket to a hosted Plane and serves its tools, its event hooks and its tool-call gate down it. No public URL, no tunnel.

Most people should use npx plane dev, which is this plus a file watcher, a session stream and a terminal. Call it directly when you want to own the process:

import { plane } from "./lib/plane"

const dev = await plane.dev({
  planeUrl: process.env.PLANE_URL,
  name: "jordan"              // namespaces the agent slug and the connection
})

await dev.ready
// …
await dev.close()

The socket code is loaded on demand, so importing @plane/sdk in a deployed app does not pull it in.

What it registers

A connection with transport: "channel" and no url and no hooksUrl. There is nothing to expose and nothing to rotate. MCP requests, hook deliveries and the synchronous tool.call.before gate all arrive as frames on that one socket and are answered by the same handlers your deployed app serves over HTTP.

Reconnect is the protocol's own rule: exponential backoff with full jitter, 250 ms to 30 s, reset once a channel has survived a minute, and terminal on 4401/4403 — a token that will never verify must not become a hot loop against the control plane.

Verification is replaced, not skipped

Over HTTP a per-request JWS proves the caller is Plane. Over a channel the socket proved it: your process presented a Plane-signed channel token at the upgrade and everything since arrived on that one TLS stream. ctx.claims, ctx.connection and ctx.session are the same shape either way — a tool handler cannot tell which transport reached it, which is what makes this a development mode rather than a second product.

Tokens

await client.channels.issueToken({ kind: "dev", name: "jordan" })

kind is dev or worker, and deliberately not sandbox: Plane hands a sandbox its own token, and a caller who could ask for one could name somebody else's sandbox id. The channel id is derived from (kind, name) rather than generated, because plane dev restarts on every save and a moving id would orphan a connection row each time.

On this page