> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pome.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub twin in 5 minutes

> Start a private GitHub, let your own coding agent triage the open bug in it, then read back the twin's recorded tape of every call it made. No GitHub account, no API key, nothing graded.

<Info>
  <Icon icon="clock" /> **About 5 minutes.** You start a private GitHub holding one
  open bug report, your own coding agent triages it, and you read the twin's own
  record of every call it made — including the `201` on the comment it wrote and
  the state that changed behind it.
</Info>

A **digital twin** is not a mock. It is a stateful service that answers the same
REST and MCP calls as `api.github.com`, boots from a declared starting state,
records every request, and never touches github.com. What you write at one call
is there at the next — and gone when the sandbox stops.

## Before you start

* **A Pome account.** `pome login` opens the browser sign-in and creates one if
  you do not have it. No credit card.
* **Your own coding agent** — Claude Code, Cursor, anything that can run a shell
  command and read JSON back.
* **Node 18+** for `npx`, plus `curl` and `jq` for the transcripts below. Your
  agent can read the raw JSON without `jq`; it is here to keep the blocks short.

Nothing else. No GitHub account, no `ANTHROPIC_API_KEY`, no model inference paid
for by Pome: **your agent is both the operator and the actor.** It drives Pome,
and it is the thing that acts on the twin.

<Note>
  **Nothing on this page is graded.** No task file, no criteria, no score — and no
  agent eval is charged, because an eval is only ever burned when a run is graded.
  A sandbox you start, drive and stop costs you nothing. Grading appears exactly
  once in this curriculum, at the
  [support-triage capstone](/quickstart/claude-code), where the agent under test
  is sealed off from the criteria that judge it.
</Note>

## Paste this

Hand this to your coding agent as-is. It names the twin's own action vocabulary
(`add_issue_comment`, `add_assignees`) and the boundary it must not cross.

```text theme={"dark"}
Set up a Pome GitHub sandbox, triage the bug already in it, then
show me the recorded tape.

Show me this plan first and wait for my go-ahead. Keep every
command plain text; never pipe a remote script into a shell.

1. Sign in and start the world:
     npx @pome-sh/cli@latest login
     npx @pome-sh/cli@latest sandbox create --twin github \
       --secrets-file .pome-sandbox.env --format json
   Then source .pome-sandbox.env. POME_AUTH_TOKEN is the bearer
   for EVERY call to the twin and the only one it accepts: any
   other token, or none, comes back as an opaque 404. Keep it in
   the shell, and add .pome-sandbox.env to .gitignore.

2. Read the world before you touch it:
     GET $POME_GITHUB_REST_URL/repos/acme/api/issues
   Tell me what is open and how it is labelled.

3. Triage that issue the way you would triage a real one, through
   the twin's own GitHub surface -- the same routes and payloads
   api.github.com takes:
     add_issue_comment  POST .../issues/<n>/comments, with a repro
                        summary drawn from the issue body, not
                        invented.
     add_assignees      POST .../issues/<n>/assignees, ["alice"].
   Do not open a new issue and do not close this one.

4. Read the result back THROUGH THE TWIN, not from your memory of
   what you sent: GET .../issues/<n> and .../issues/<n>/comments.

5. Show me the tape, one line per call, in the order they
   happened: GET $POME_GITHUB_REST_URL/_pome/events

6. Stop the sandbox:
     npx @pome-sh/cli@latest sandbox stop $POME_SESSION_ID --discard

This is a sandbox, not an exam. Nothing here is scored and Pome is
not evaluating you. Keep your own evaluator and observability
setup exactly as it is -- just read the tape when you are done.
```

<Warning>
  **One token opens a sandbox: `POME_AUTH_TOKEN`.** It is the bearer on every call
  to the twin, REST and MCP alike. The secrets file also carries a provider-shaped
  token — `POME_GITHUB_TOKEN=github_pat_…`, `POME_STRIPE_API_KEY=sk_test_…` — and
  that one is *not* the bearer; it is what the twin serves inside the sandbox.
  Send it, or send no bearer at all, and the proxy answers an opaque `404` reading

  `No twin pod for this session.` So a 404 on a sandbox you just created is almost
  always the wrong bearer rather than a dead sandbox.
</Warning>

## The world

`sandbox create` boots a GitHub twin from its declared starting state and hands
back the URLs that reach it. It also writes the connection secrets to
`.pome-sandbox.env` at mode `0600`, and says so on stderr.

```bash theme={"dark"}
npx @pome-sh/cli@latest sandbox create --twin github \
  --secrets-file .pome-sandbox.env --format json
```

```json theme={"dark"}
{
  "session_id": "ses_XmHBDeCkU7UEU1VM",
  "expires_at": "2026-08-24T17:56:10.378Z",
  "per_twin": {
    "github": {
      "api_url": "https://twins.pome.sh/github/s/ses_XmHBDeCkU7UEU1VM",
      "mcp_url": "https://twins.pome.sh/github/s/ses_XmHBDeCkU7UEU1VM/mcp"
    }
  },
  "agent_token": "***redacted***"
}
```

Every call below goes to that `api_url` with the one bearer the sandbox accepts:

```bash theme={"dark"}
source .pome-sandbox.env

gh() {
  curl -s -H "Authorization: Bearer $POME_AUTH_TOKEN" \
       "$POME_GITHUB_REST_URL$1" "${@:2}"
}
```

The world is one repository, `acme/api`, holding one open bug report. Read it
the way any GitHub client would:

```bash theme={"dark"}
gh /repos/acme/api/issues | jq '[.[] | {
  number, title, state,
  labels:    [.labels[].name],
  assignees: [.assignees[].login],
  comments
}]'
```

**You should see one item:**

```json theme={"dark"}
[
  {
    "number": 1,
    "title": "500 error on POST /orders after deploy",
    "state": "open",
    "labels": ["bug"],
    "assignees": [],
    "comments": 0
  }
]
```

Unassigned, uncommented, labelled `bug`. That is the starting state, and it is
the same every time you create this sandbox — which is what makes anything you
observe next reproducible.

Now the triage: two writes, then two reads back through the twin.

```bash theme={"dark"}
gh /repos/acme/api/issues/1/comments -X POST \
   -H 'content-type: application/json' \
   -d '{"body":"Repro: POST /orders 500s since the 14:00 deploy."}' \
   -w 'HTTP %{http_code}\n' -o /dev/null

gh /repos/acme/api/issues/1/assignees -X POST \
   -H 'content-type: application/json' \
   -d '{"assignees":["alice"]}' \
   -w 'HTTP %{http_code}\n' -o /dev/null
```

```text theme={"dark"}
HTTP 201
HTTP 201
```

Read the issue back through the twin's own surface — not out of the response
your agent already holds:

```bash theme={"dark"}
gh /repos/acme/api/issues/1 | jq '{
  number, state,
  assignees: [.assignees[].login],
  comments
}'

gh /repos/acme/api/issues/1/comments \
  | jq -r '.[] | "\(.user.login): \(.body)"'
```

```json theme={"dark"}
{
  "number": 1,
  "state": "open",
  "assignees": ["alice"],
  "comments": 1
}
```

```text theme={"dark"}
pome-agent: Repro: POST /orders 500s since the 14:00 deploy.
```

**This is the thing worth noticing.** What was written at call two is there at
call four, read through a different route — and only inside this sandbox. Stop
it and that world is gone; create another and issue #1 is unassigned and
uncommented again.

## Read the tape

Every call above was recorded by the twin as it happened. This is the part
neither a mock nor a staging account gives you: an account of the run written by
the service, not by the agent.

```bash theme={"dark"}
gh /_pome/events | jq -r '.[]
  | "\(.method|(.+"     ")[:5])\(.path|sub("^/s/[^/]+";"")) -> \(.status)"'
```

```text theme={"dark"}
GET  /repos/acme/api/issues -> 200
POST /repos/acme/api/issues/1/comments -> 201
POST /repos/acme/api/issues/1/assignees -> 201
GET  /repos/acme/api/issues/1 -> 200
GET  /repos/acme/api/issues/1/comments -> 200
```

Each row carries more than the request line. Ask for the whole row on the one
call the twin recognised as a named action:

```bash theme={"dark"}
gh /_pome/events | jq '.[] | select(.tool) | {
  method, status, tool, state_mutation, fidelity
}'
```

```json theme={"dark"}
{
  "method": "POST",
  "status": 201,
  "tool": "add_issue_comment",
  "state_mutation": true,
  "fidelity": "semantic"
}
```

**You should see:**

* **Five rows, in the order they happened.** Read top to bottom and the triage is
  legible without asking the agent what it did: it looked, it wrote twice, it
  checked.
* **`201` on `POST .../issues/1/comments`, stamped `tool: add_issue_comment`.**
  The twin stamps its own action vocabulary on the row whether the call arrived
  over REST or over MCP, so the same work reads the same either way.
* **`state_mutation: true` on exactly the two writes.** It means the call
  *landed* — a write the twin refuses reads `false`, not `true`.
* **No `tool` on `POST .../issues/1/assignees`.** Only a short list of GitHub
  actions is stamped by name today; every other call is identified by method and
  path. That is why the assertable check below binds to `add_issue_comment`.
* **`fidelity: semantic`.** This surface carries a full behavioural contract,
  not a response shape with placeholder values.

Each write also carries what it changed:

```bash theme={"dark"}
gh /_pome/events \
  | jq '[.[] | select(.state_delta) | .state_delta] | last'
```

```json theme={"dark"}
{
  "before": {
    "repo": "acme/api",
    "issue_number": 1,
    "assignees": []
  },
  "after": {
    "repo": "acme/api",
    "issue_number": 1,
    "assignees": ["alice"]
  }
}
```

Before and after, recorded by the service that changed. When you are done, stop
the sandbox to free the slot — it also expires on its own 30 minutes after it
was created:

```bash theme={"dark"}
npx @pome-sh/cli@latest sandbox stop $POME_SESSION_ID --discard
```

## What you could assert here

Nothing on this page was graded, but a tape and a final state are exactly what a
graded check reads. The GitHub twin already declares these — no authoring
required, and `list_checks` on the Pome MCP prints the full set with what each
one actually compares:

| Declared check                                 | What it would catch here                                                                                                                                                                     |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `github.tool-was-called` (`add_issue_comment`) | An agent that reports triaging the bug without ever writing the comment. It reads the tape, so an agent that does nothing fails it instead of collecting free points.                        |
| `github.no-new-issues`                         | An agent that "handles" the report by opening a second issue for it. It compares issue numbers between the starting and final state, so a duplicate carrying the same title is still caught. |
| `github.issue-state` (`open`)                  | An agent that closes #1 to make its queue look clean. Asserting `open` is a prohibition: it asks the agent *not* to close the issue.                                                         |

Pointers, not a task. Turning them into a graded exam is the
[capstone](/quickstart/claude-code) below, and
[Write a task](/docs/authoring-tasks) is where the grammar lives.

## How real is this twin?

Every surface you called above is compared against a captured response from the
real GitHub API, re-run daily and published.
[The GitHub row on status.pome.sh](https://status.pome.sh) carries the current
count of twin responses that match, names every divergence we have ruled on by
id, and states how old the captured baseline is. Nobody has to take our word for
the fidelity, including us — a twin that drifts turns that row red.

Which surfaces are covered, and which are shape-only, is on the
[GitHub twin reference](/docs/twins/github).

## Next: the one graded lesson

<CardGroup cols={2}>
  <Card title="The support-triage capstone" icon="graduation-cap" href="/quickstart/claude-code">
    The same twins, now an exam: a sealed agent under test, a deliberate failing
    score, and one line of prompt that turns it green.
  </Card>

  <Card title="GitHub twin reference" icon="github" href="/docs/twins/github">
    Every route and MCP tool the twin serves, by use case, with its fidelity tier.
  </Card>

  <Card title="Write a task" icon="pen-line" href="/docs/authoring-tasks">
    Turn the checks above into a graded exam for your own agent.
  </Card>

  <Card title="pome sandbox" icon="terminal" href="/docs/cli/sandbox">
    Create, list and stop sandboxes — including multi-twin ones.
  </Card>
</CardGroup>
