What the GitHub App can do
Connect GitHub once per organization in the Console. Every clone then mints a fresh installation token. Ask GitHub what that token may do rather than assuming:issues permission. GET /repos/{o}/{r}/issues answers
403 with an installation token. The agent cannot read the issue it was
asked to fix. Either your dispatcher reads it and puts it in input, or you
attach a GitHub MCP server with a separate PAT — see
Give an agent your tracker for that
shape. The first needs no extra credential; prefer it unless the agent
genuinely needs comments and linked PRs.
The token lives one hour, and is minted once, immediately before the
clone. It is not refreshed mid-session. A session that works for ninety
minutes and then pushes finds a dead token — and it finds it at the last
step, after all the work is done. That is the worst possible moment to
discover a credential problem.
Two architectures
A — the agent produces a patch, your worker pushes
No write credential inside the sandbox. No expiry window — your worker pushes with its own long-lived credential whenever it likes. One pusher, which you can serialise per repository, which is the entire answer to write races between parallel sessions.B — the agent pushes and opens the PR itself
The clone URL ishttps://x-access-token:<token>@github.com/owner/repo.git,
and git writes it into .git/config. So git push works from inside the
sandbox, and the same token has pull_requests: write, so the agent can open
the PR with one API call. No extra credential at all.
Fewer moving parts, and the run behind this guide used it successfully. It
also puts a write-capable credential inside the workspace — see the leak
below.
Choose A for anything long-running or unattended. Choose B when you want
the smallest possible pipeline and your sessions finish well inside the
hour.
Create the session
environment.repo takes owner/name — not a URL. Do not send
environment.branch: it is refused. The branch is reported by the clone,
not chosen by you; the agent creates its working branch from the default
one, per your instructions.
Put the branch name in metadata as well as in the message. That is how your
turn.completed handler knows which branch to look at without parsing
prose.
Instructions that produced a correct PR
Expect a review round, and use it
In the run behind this guide the first push did four of five things correctly and missed one: two tracked JSON files still contained the value the issue asked to remove. The agent’s own summary said the work was done. The issue had stated its verification as a command. Running that command is what found the miss — not reading the diff, and certainly not reading the summary. Sending the miss back took one message and 45 seconds:Verify the PR, not the report
app/<your-app-slug> when the agent opened it. That is
a useful property: PRs opened by the agent are distinguishable from PRs
opened by humans, in the GitHub UI and in filters, without any convention you
have to maintain.
The leak you need to know about
The installation token reaches the session transcript. In the verified run it appeared twice, in full, inGET /v1/sessions/{id}/items.
Not because the agent printed it. It was told not to, and it did not. The
token arrived in the output of an ordinary git remote -v, because the
remote URL contains it — and tool results are stored and served.
What that means concretely:
- A token holding only
sessions:readcan read a GitHub credential holdingcontents: writeandpull_requests: write. repository_selection: all— so that credential reaches every repository the App is installed on, not the one being worked.- The one-hour expiry is the only thing bounding it.
- It did not appear in
/v1/sessions/{id}/events.
- Keep
sessions:readtokens for repository-backed sessions as scarce assessions:writeones. On this path they are equivalent. - Delete the session when the PR is open. That hard-deletes its items; the
endpoint answers
404afterwards. - Scope the App installation to the repositories that actually need it.
repository_selection: allturns one transcript into access to all of them.
Write races between parallel sessions
Sessions are isolated machines; nothing races inside the runtime. The race is at your repository, and git already handles it:- One session per branch. Two sessions never write the same ref.
- One pusher. In architecture A that is your worker; serialise it per repository and the race is gone. In architecture B it is each agent, so the branch-per-session rule is doing all the work — keep it strictly.
- Conflicts surface at merge, in front of a reviewer, where they are visible and reversible.
- Retry = a new session on the same branch. No half-written workspace to reason about.
- Two issues touching the same files is a tracker problem. List
in-flight sessions, read their branches from
metadata, and hold one whose paths overlap.
Next
- sessions —
environment.repoand every other fieldPOST /v1/sessionsaccepts - Give an agent your tracker — the read side of this same pattern, against Linear instead of git
- Run a fleet of ticket-driven agents in parallel — this shape, run for every ticket at once