How to let an AI agent open pull requests with the GitHub CLI from a sandbox
Install gh in the sandbox, store the token as a Runtime secret named GH_TOKEN, and the agent can push a branch and run gh pr create.
On Runtime gh works with a token the sandbox never holds. gh reads
GH_TOKEN from the environment, and in a Runtime sandbox that variable holds a
placeholder. The host's proxy swaps in the real token only on HTTPS requests to
the GitHub hosts you name, so an agent that runs env, or a prompt injection
that tries to post the token somewhere, finds nothing worth taking. GitHub CLI
2.101.0 was the current release on 25 September 2026.
Store the token and build an image with gh
Store the token once for the account. Then build gh into a
custom image with the steps from GitHub's own Linux install
guide, so no sandbox spends time installing it:
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.secrets.set("GH_TOKEN", { value: process.env.GITHUB_TOKEN ?? "", hosts: ["api.github.com", "uploads.github.com"],});await runtime.images.build({ name: "gh", recipe: { commands: [ "mkdir -p -m 755 /etc/apt/keyrings" + " && wget -nv -O /etc/apt/keyrings/githubcli-archive-keyring.gpg https://cli.github.com/packages/githubcli-archive-keyring.gpg" + " && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg" + ' && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list' + " && apt-get update && apt-get install -y gh", ], },});Pythonimport osfrom withruntime import Runtimeruntime = Runtime()runtime.secrets.set("GH_TOKEN", value=os.environ["GITHUB_TOKEN"], hosts=["api.github.com", "uploads.github.com"])keyring = "/etc/apt/keyrings/githubcli-archive-keyring.gpg"runtime.images.build(name="gh", recipe={"commands": [ f"mkdir -p -m 755 /etc/apt/keyrings && wget -nv -O {keyring} https://cli.github.com/packages/githubcli-archive-keyring.gpg" f" && chmod go+r {keyring}" f' && echo "deb [arch=$(dpkg --print-architecture) signed-by={keyring}] https://cli.github.com/packages stable main"' " > /etc/apt/sources.list.d/github-cli.list && apt-get update && apt-get install -y gh",]})Recipe commands run as root, so GitHub's sudo steps need no sudo here.
| Install route | Version on 25 September 2026 | Command |
|---|---|---|
Ubuntu 24.04's own gh package |
2.45.0 | sudo apt-get install -y gh |
| GitHub's apt repository | 2.101.0 | The recipe above |
Ubuntu's package is quicker to type and 56 minor releases behind GitHub's.
The agent's pull request, end to end
gh handles the API; the branch itself goes up with git push, which needs
the github.com header secret from Git in a sandbox:
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await using sbx = await runtime.sandboxes.create({ image: "gh", timeoutSeconds: 1800 });const sh = (command: string) => sbx.exec(command, { cwd: "/workspace/app", check: true, timeoutMs: 300_000 });await sbx.exec("git clone https://github.com/acme/app.git app", { check: true, timeoutMs: 300_000,});await sh("git switch -c agent/update-readme");// ... the agent edits files here ...await sh( "git -c user.name='Build Agent' -c user.email=agent@example.com commit -am 'Update the README'",);await sh("git push -u origin agent/update-readme");const pr = await sh( "gh pr create --base main --head agent/update-readme --title 'Update the README' --body 'Opened by the build agent.'",);console.log(pr.stdout.trim()); // the pull request's URLPythonfrom withruntime import Runtimeruntime = Runtime()with runtime.sandboxes.create(image="gh", timeout_seconds=1800) as sbx: def sh(command: str): return sbx.exec(command, cwd="/workspace/app", check=True, timeout_ms=300_000) sbx.exec("git clone https://github.com/acme/app.git app", check=True, timeout_ms=300_000) sh("git switch -c agent/update-readme") # ... the agent edits files here ... sh("git -c user.name='Build Agent' -c user.email=agent@example.com commit -am 'Update the README'") sh("git push -u origin agent/update-readme") pr = sh("gh pr create --base main --head agent/update-readme" " --title 'Update the README' --body 'Opened by the build agent.'") print(pr.stdout.strip())gh auth login is not needed: when GH_TOKEN is set, gh uses it for
github.com and skips its login prompt. Do not run gh auth setup-git. It makes
gh git's credential helper, which would hand git the placeholder as a
password inside an encoded header that the proxy leaves alone; the header
secret already covers git.
gh commands an agent uses most
| Task | Command |
|---|---|
| Read the issue it was given | gh issue view 42 --json title,body,comments |
| Open the pull request | gh pr create --base main --head <branch> --title ... --body ... |
| Wait for CI | gh pr checks <branch> --watch |
| Read why CI failed | gh run view <run-id> --log-failed |
| Answer a review | gh pr comment <branch> --body ... |
| Anything else in the REST API | gh api repos/{owner}/{repo}/... |
--json output parses without scraping text, which suits a model. --watch
blocks until checks finish, so give that command a timeoutMs long enough for
your CI.
Keep the agent's reach small
- The token: a fine-grained token can be limited to selected repositories,
with "Contents" write for the push and "Pull requests" write for
gh pr create, and nothing else. - The network: create the sandbox with
network: { internet: true, allow: ["github.com", "api.github.com", "uploads.github.com"] }when the task needs nothing else. Root in the sandbox cannot change the rule. - The hosts: the secret's value goes only to the hosts it names. A request anywhere else carries the placeholder, which is worthless.
Related
- Git over HTTPS or SSH in a sandbox
- A sandbox for a coding agent
- Run Codex in a sandbox
- Secrets sandboxes never see
- Egress control
Sources
Checked 25 September 2026.
- Installing gh on Linux: the keyring and apt repository steps
- GitHub CLI: version 2.101.0
- gh environment:
GH_TOKENis used for github.com and takes precedence over stored credentials - gh in Ubuntu noble: 2.45.0
- Fine-grained token permissions: "Contents" and "Pull requests"
Facts on this page were checked on 25 September 2026.