Delivery11 min read
Running a platform from the command line
JSON on every command, explicit writes, the organisation named on each change, one public gateway and versioned writes make a CLI safe for agents to operate.
Mercury Labs
JSON on every command, explicit writes, the organisation named on each change, one public gateway and versioned writes make a CLI safe for agents to operate.
We run the Company Twin and the servers it runs on through two command-line tools, because a command line is the interface that people and AI agents can both use well. Every command answers in JSON, every change must be asked for by name, and every call that can change something must name the organisation it is for. That makes a coding agent a safe operator, not just a fast one.
An agent-native CLI is a command-line tool designed so a program can discover its commands, read its results and make changes safely: structured output on every command, writes that are explicit opt-ins, and a scope that is stated on each call rather than remembered.
This article covers the Twin CLI, which reaches an organisation's Twin through one public gateway, and Fleet, the tool we use to see and change the state of every customer server. It ends with how agents run our own work board through the same commands our customers use.
Coding agents such as Claude Code and Codex already live in a terminal. They read help text, run a command, parse what comes back and decide what to do next. A web interface makes them drive a browser; a bespoke agent API makes us maintain a second contract. A CLI with structured output gives both people and agents the same door, and every improvement to it helps both.
The Twin CLI is a public package. It signs in through the browser with OAuth, and one sign-in covers every organisation that identity may use. There is no SSH, no tunnel, no API key to paste and no tenant ID to look up. The first command an agent runs is the capability manifest:
twin help --json # every command, its options, limits and examples, for an agent to readtwin organisations --json # which organisations this sign-in may usetwin channels --assert-org "Acme Ltd" --jsonThe JSON help is written for a model to read: exact flags, filter operators, limits and examples, so the agent does not have to guess from prose. When something fails, the CLI writes a stable error object to standard error with the server's problem detail and validation paths, so an agent can explain the failure instead of reporting "Bad Request".
Three rules carry most of the weight.
--json. Long-running questions can stream their lifecycle as one JSON object per line (submitted, queued, running, completed), carrying only stable identifiers until the end, so an agent can report progress without spending tokens on repeated data. A local timeout does not cancel the server's work; the agent resumes waiting on the same query ID instead of starting a duplicate.--allow-write and a sign-in with the write scope. A read can never become a write because an agent misread a flag.--org or --assert-org chooses the organisation for one command; an environment variable does it for a process. Neither changes the saved default, so two agents working for different organisations on one machine cannot disturb each other. A command that can change something and names no organisation is refused. --assert-org goes one step further: the call fails before it starts unless the organisation it would use is the one named.That last rule came from experience. An agent that remembers which organisation it is in will eventually be wrong about it, usually after a long session or when another process changed the default. Making the scope part of each command removes the memory from the problem.
The CLI, the SDK, MCP clients and our desktop app reach an organisation only through the public gateway. None of them talks to the control plane directly. That gives us one place to authenticate, rate-limit and audit, and one list of what outside clients can do.
That list is a single operation table in our contracts package. Each entry is a full definition: the route and method, the OAuth scope it needs, its rate class, whether it reads or changes, the request and response schemas, the SDK method it maps to and, where it is offered to outside models, its MCP tool name. The OpenAPI description and the SDK are generated or checked against it. Today it holds more than 260 operations, of which about 120 need the write scope.
Two tests keep the surfaces honest. One reads the CLI's command sources and the operation table, rather than mocking either, and fails if the CLI can make a request the gateway's route matcher would not accept. It caught a command that was quietly addressing control-plane paths, which worked on a developer's machine and would have failed for every customer. The other checks that every public operation maps to a real SDK method in a namespace the gateway serves, and that no public operation maps into a control-plane-only namespace. Known gaps live in explicit sets that the tests require to shrink: when the gateway serves a route, its entry must leave the set.
Every write follows the same loop: read the current state, write only what changes with the version you read, and read the result back.
twin channels --channel clients --record rec_123 --json # returns values and "version": 3 cat > patch.json <<'JSON'{ "expectedVersion": 3, "values": { "nextAction": "Schedule review" } }JSON twin channels --channel clients --record rec_123 \ --update-record patch.json --assert-org "Acme Ltd" --allow-write --json twin channels --channel clients --record rec_123 --json # confirm: version 4, the new valueValues are merged, not replaced, so a patch cannot wipe a field it did not mention. A stale version returns 409 Conflict. Creating a record takes an idempotency key: reusing the key with the same content after a lost response returns the original record and its receipt, and reusing it with different content is a conflict. An agent that lost its connection mid-write can therefore try again without creating a duplicate, and an agent that confused two writes is stopped. Deleting is archiving, with the same version check, so the audit history survives.
The read-back step looks redundant until the first time a write is accepted by one layer and rewritten by another. We treat a consequential write as unfinished until an independent read shows it.
Yes, and for automation you usually should. Asking a question in prose runs the Twin's agent, which is the right tool for open questions. When a script already knows the shape of the answer, it can skip the model entirely:
twin graph catalogue --json # entities and measures this person may readtwin graph query '{"measures":["invoice_amount_total"],"by":["currency"]}' --jsontwin graph search "late delivery" --jsontwin query --intent-file aggregate.json --json # a typed query over a channel's own dataThe graph commands read the organisation's graph exactly as the Twin's own agent does, with each table pinned by a certificate (we described the engine in certified answers from a Rust graph engine). They only read, and they return what the person's data roles and channels allow. Typed queries over a channel's data are compiled by the server into a parameterised query after checking access and every field against the schema; raw SQL is not accepted. Our standing answer and invariant suites run through these commands, which is why they need no model and cost nothing to run after every release.
Each customer runs on its own server with a full copy of the stack. Fleet is the tool that sees and changes all of them, and it follows the same rules as the Twin CLI.
| Command | What it does | The rule |
|---|---|---|
| status | Reads, for every service on every server, the release each running process actually loaded, its health and its queues | Fails closed: an unreachable server or a query that could not run is reported as unknown, never as healthy |
| deploy --dry-run | Plans a rollout locally, with no credentials | A plan is free; a change is not |
| request | Submits a deployment for the governed runner to carry out | Short-lived, request-bound assertion; agents never hold deploy credentials |
| status --deployment-id | Follows one rollout to its terminal result | A filed request is not a delivery |
Exit codes carry the verdict so a script needs no parsing: a fully converged, healthy fleet exits 0, drift, an unfinished rollout or unhealthy health exits 1, and invalid input exits 64. Status reads the release from each running process rather than from symlinks, because a service that was never restarted looks current on disk and is not.
Production changes are made only by a governed runner. An agent submits intent: the commit, the targets, the services. Submission is idempotent. A new request returns created: true; the same requester repeating the same key and request gets replayed: true and the existing rollout, including its result, with no new work queued. When a submission's response is lost, the agent reuses the key to find out what happened. Changing the request while reusing the key is a conflict. The release path behind it is the subject of one customer per server.
Our own engineering board is a channel data set in our own Twin: one record per piece of work, with fields for status, owner, what it is waiting on and the question a person must answer, and comments for the history. The agents that write most of our code keep it current through the same twin channels commands a customer would use.
An agent claims a record by setting its owner and its own session, comments findings as it works, and when a person must choose, sets the waiting-on field to that person and writes the question, recommendation first. It mentions them in a comment, which notifies them in the app and in Slack. Each update is written with the record's version and read back. When something significant lands, the agent posts a short summary to the channel's feed and mentions the people it affects.
Running it this way surfaced problems a demo never would. Two agents creating records at the same moment once took the same reference number; the fix was to treat creation as a loop that takes the next free number and stops only when the server says the record was created. Version checks exist for the related case of an agent writing from a stale read: it gets a visible conflict instead of silently overwriting a colleague's change. Because our agents and our customers' agents use the same commands, what we fix for one is fixed for both.
If you are putting coding agents to work on operations, the interface they use matters more than the model. We would start with the commands your team already runs and make them safe for an agent before making them smarter.
06 stages
Give every command structured output and a stable error shape
Watchpoint
An agent that parses prose will misread a warning as success
Make writes an explicit flag and a separate scope
Watchpoint
A read that can become a write by accident eventually will
Require the target (organisation, environment, server) on every call that changes something
Watchpoint
Remembered context drifts across long sessions and parallel agents
Write with versions or idempotency keys, and read back
Watchpoint
Retries after a lost response create duplicates without them
Put one gateway in front of every client and test the CLI against it
Watchpoint
A client that works on a developer's machine can fail for every customer
Let agents request production changes, never perform them
Watchpoint
An agent holding deploy credentials is one confused step from an outage
Most of this is plumbing, and that is the point. Our piece on the last mile problem for AI coding agents covers keeping their work inside scope; this is the operational half. For how every interface reaches the Twin without widening access, see one Twin, many interfaces.
The Twin CLI is public: install it, sign in through your browser and point your own agent at your Company Twin. Read the Twin CLI guide →