# 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](/docs/images) with the steps from GitHub's own Linux install guide, so no sandbox spends time installing it: ```ts check import { 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", ], }, }); ``` ```python check import os from withruntime import Runtime runtime = 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](/integrations/git): ```ts check import { 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 URL ``` ```python check from withruntime import Runtime runtime = 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 --title ... --body ...` | | Wait for CI | `gh pr checks --watch` | | Read why CI failed | `gh run view --log-failed` | | Answer a review | `gh pr comment --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](/integrations/git) - [A sandbox for a coding agent](/use-cases/coding-agent-sandbox) - [Run Codex in a sandbox](/integrations/codex) - [Secrets sandboxes never see](/docs/security#secrets-sandboxes-never-see) - [Egress control](/glossary/egress-control) ## Sources Checked 25 September 2026. - [Installing gh on Linux](https://github.com/cli/cli/blob/trunk/docs/install_linux.md): the keyring and apt repository steps - [GitHub CLI](https://cli.github.com/): version 2.101.0 - [gh environment](https://cli.github.com/manual/gh_help_environment): `GH_TOKEN` is used for github.com and takes precedence over stored credentials - [gh in Ubuntu noble](https://packages.ubuntu.com/noble/gh): 2.45.0 - [Fine-grained token permissions](https://docs.github.com/en/rest/authentication/permissions-required-for-fine-grained-personal-access-tokens): "Contents" and "Pull requests" Facts on this page were checked on 25 September 2026.