About 5 minutes. This one needs a Pome account: it is
the page about the thing that only exists in the cloud. Everything else in
Get started runs on your own machine with no account, and
you can do that first.
What you are about to see, both halves
One sandbox is one thing you hold. It has a single id, a single bearer token that opens every twin in it, a single 30-minute clock, and it is one line on the bill no matter how many twins are inside. The twins inside it share nothing else. Each keeps its own state, and each writes its own tape — the record of the calls it served. The GitHub write does not appear on Slack’s tape, and Slack’s does not appear on GitHub’s. Which leaves the interesting question, and it is the one this page exists to answer: if the platform moves nothing between the two twins, what carried the issue number from GitHub into the Slack message?Your agent did. Pome gives the two twins one address book, one credential and
one lifetime. It does not copy a fact out of one and into the other, and there is
no setting that makes it. That is not a gap — it is what makes the run a test.
An agent that forgets to tell Slack produces a sandbox where GitHub is correct
and Slack is silent, and a test that carried the fact for it could never catch
that.
Before you start
- A Pome account. Sign up, then
npx @pome-sh/cli@latest login. A sandbox runs on our infrastructure, so unlike Get started there is no accountless version of this page. - Node 24+ for
npx, pluscurlandjq. Your agent can read the raw JSON withoutjq; it is here to keep the blocks short. - Your own coding agent. No
ANTHROPIC_API_KEYand no model inference paid for by Pome: your agent is both the operator and the actor.
Start one sandbox with two twins
Pass--twin once per twin. The order does not matter.
session_id. One expires_at. A per_twin
entry per twin, each with its own address — and the same id sitting as a segment
inside both. One thing you hold; two places to call.
The
jq above is a projection, not the whole response. Unprojected it also
carries an mcp_url and an openapi_url per twin, a provider_credentials map
of vendor-shaped tokens the twins hand out to code running inside the world,
and an agent_token. Drop the pipe to see all of it. None of those is the bearer
you call with — that comes from the secrets file below.One credential, two addresses
--secrets-file writes the values as a shell file. Source it and every twin in
the sandbox is reachable from the same shell.
POME_AUTH_TOKEN is the important one: a single bearer, and it
opens both twins.
Two helpers keep the rest of the page short, and they differ only in which URL
they call — the bearer is the same variable in both.
Hand your agent the work
The task crosses both twins on purpose: triage the bug in GitHub, then tell the channel. Paste this as-is.The GitHub half
acme/api. Triage it:
The Slack half, and the carry
acme/api#1 came out of the GitHub
response above and goes into the Slack message body here, and the thing that
moved it those two lines is the agent:
Read the tapes — one per twin
Each twin recorded the calls it served, and only those.ses_L2RwKae6MWKMg1v2 is a segment in every path on both tapes —
one sandbox, and both twins know they are inside it. And the two row sets are
disjoint. The chat.postMessage that Slack served is nowhere on GitHub’s
tape; neither GitHub write appears on Slack’s. Not summarised there, not
cross-referenced there — absent.
That is mechanical rather than a matter of reading — each tape says so itself:
state_mutation: true means the call landed — a write the twin refuses reads
false. The tool field is the twin’s own action vocabulary, stamped whether
the call arrived over REST or over MCP; the assignees row has none, because only
a short list of GitHub actions is stamped by name today.
The other half of isolation: a second sandbox
Two twins in one sandbox do not share state. Neither do two sandboxes — and that is the claim that is easy to fake, because reading a fresh sandbox after stopping the first proves nothing: an empty world is what a reset looks like too. So start a second one and leave the first running.#general is still
sitting on its seeded morning greeting, and its issue #1 is still unassigned with
no comments — while the first sandbox, alive on the other side of the &, holds
both writes. Sequential reads cannot tell isolation from a reset; this can.
The tape says it a second way. The second sandbox’s GitHub tape carries its own
id and one row, the read you just did:
What you could assert here
Nothing above was graded, but a tape and a final state are exactly what a graded check reads. Each twin declares its own closed set —GET /v1/checks?twin=<twin>
prints it, and so does pome checks.
That per-twin split is the point, and it is worth seeing in the vocabulary
itself. There is no check that reads both twins. A cross-twin claim is made
by conjoining one check on each side, because grading reads what each twin
recorded, and each twin recorded only itself:
Pointers, not a task. Write a task is where the grammar
lives.
What this costs, and how to stop it
Two sandboxes were open at once above, with four twins running between them. Ask the control plane what that cost —POME_API_KEY is a team key from the
dashboard, not the sandbox bearer:
expires_at in the create
response — and stopping one takes the twins inside it with it. Stop both rather
than waiting them out. $POME_SESSION_ID is whichever file you sourced last, so
name each one in turn:
--discard throws the world away instead of keeping it for a report, which is
what you want for an ungraded walkthrough like this one. Then the meter is back
where it started:
Next
The graded capstone
The same two twins, now an exam: a sealed agent under test, a deliberate
failing score, and one sentence of prompt that turns it green.
Cross-twin consistency
A worked example of the failure this page’s shape exists to catch — GitHub
correct, Slack silent, and only an exam reading both states at once notices.