Skip to content
Guides · Coding agents

Turn a GitHub Issue Into a Pull Request From a Sandbox

Updated October 27, 2026 .md

Short answer: Create a sandbox with --no-env so it holds none of your secrets, pass one short-lived fine-grained GitHub token as GH_TOKEN, then clone, branch, run the agent, run the tests, push and open the pull request with gh. Delete the sandbox at the end. Our test run, with no model and no push, took 33 seconds.

At a glance#

Topic Detail
Isolation One microVM per issue, created with --no-env: none of your variables, files or credentials
GitHub access One fine-grained token for one repository, passed as GH_TOKEN with -e
Tools in the image git 2.43, gh 2.102, Go, Node, Python (checked with sbx exec)
Size used below small: 2 vCPU, 4 GB RAM, 12 GB disk, $0.018/hour, billed per second
Time per run 33 seconds end to end on google/uuid (one run, 2026-10-27), with a placeholder agent step and no push
Cleanup A shell trap deletes the sandbox even if a step fails

What we ran on prod for this page: the whole script below against the public repo google/uuid and its open issue #147. Two things did not run. No model ran: the agent step is a placeholder script. Nothing was pushed: we have no token for that repo, so the run stops at the commit and a git format-patch file, which is exactly what the push would send.

1. Make a token for this one job#

Create a fine-grained personal access token on GitHub with:

  • Repository access: only select repositories, and pick the one repo the agent works on.
  • Permissions: Contents read and write (to push the branch) and Pull requests read and write (to open the PR). Nothing else.
  • Expiration: the shortest your workflow allows.

The GitHub CLI reads GH_TOKEN from the environment, and gh auth setup-git makes git push use the same token. So the sandbox needs one variable and no login step.

2. The orchestration script#

It runs on your laptop or in CI, with sbx signed in. Each step is a plain sbx call, so you can move it into the TypeScript or Python SDK later.

#!/usr/bin/env bash
# Usage: ./issue-to-pr.sh OWNER/REPO ISSUE_NUMBER
#   GH_TOKEN  fine-grained token for this one repo (optional until you push)
#   PUSH=1    push the branch and open the pull request
set -euo pipefail
repo="$1"; issue="$2"; branch="agent/issue-$issue"
t0=$SECONDS; step() { echo "[$((SECONDS - t0))s] $*" >&2; }

# 1. A sandbox with none of your secrets, plus only the token this job needs
args=(--no-env --type small --no-snapshots --ttl 3600 --name "${NAME:-issue-$issue}")
[ -n "${GH_TOKEN:-}" ] && args+=(-e "GH_TOKEN=$GH_TOKEN")
id="$(sbx new "${args[@]}" --json | jq -r 'select(.event == "ready") | .id')"
trap 'sbx delete "$id" --yes >/dev/null && step "deleted $id"' EXIT
step "created $id"

# 2. The issue goes in as a file, never into a shell command line
curl -fsS "https://api.github.com/repos/$repo/issues/$issue" > issue.json
jq -r '"# \(.title)\n\n\(.body)"' issue.json > ISSUE.md
jq -r --arg n "$issue" '"Refs #\($n): \(.title)"' issue.json > COMMIT_MSG
for f in ISSUE.md COMMIT_MSG agent-step.sh; do sbx scp "$f" "$id:/home/user/" >/dev/null; done
sbx exec "$id" --timeout 120 "git clone -q https://github.com/$repo work && git -C work switch -q -c $branch"
step "cloned $repo, on $branch"

# 3. The agent step (a placeholder here; see below)
sbx exec "$id" --cwd work --timeout 600 "bash ../agent-step.sh"
step "agent step finished"

# 4. Tests must pass before anything leaves the sandbox
sbx exec "$id" --cwd work --timeout 600 "go test ./..."
step "tests passed"

# 5. Commit, and keep the patch as the artifact
sbx exec "$id" --cwd work "git add -A && git -c user.name='Issue bot' -c user.email=bot@example.com commit -q -F ../COMMIT_MSG && git format-patch -1 --stdout" > "issue-$issue.patch"
step "committed; patch in issue-$issue.patch"

# 6. Push and open the pull request
if [ "${PUSH:-0}" = 1 ]; then
  sbx exec "$id" --cwd work --timeout 120 "gh auth setup-git && git push -q -u origin $branch && gh pr create --fill --head $branch"
  step "pull request opened"
fi

sbx exec exits with the remote command's exit code, so set -e stops the script on the first failed step, and the trap still deletes the sandbox. gh pr create --fill takes the title and body from the commit, and --head names the branch you just pushed (see the gh pr create manual).

The placeholder agent step, agent-step.sh, writes the change a model would make. For issue #147 (uuid.Parse and surrounding spaces) it adds a test that pins today's behavior:

#!/usr/bin/env bash
# Placeholder for the agent. A real run replaces this file's body with a coding
# agent (for example `claude -p "$(cat ../ISSUE.md)"`). No model runs here.
set -euo pipefail
cat > parse_spaces_test.go <<'GO'
package uuid

import "testing"

// Issue #147: Parse does not trim leading or trailing spaces.
// Pin the current behavior so a future change to it is deliberate.
func TestParseRejectsSurroundingSpaces(t *testing.T) {
	s := "f47ac10b-58cc-0372-8567-0e02b2c3d479"
	for _, in := range []string{" " + s, s + " "} {
		if _, err := Parse(in); err == nil {
			t.Errorf("Parse(%q) returned no error", in)
		}
	}
}
GO

3. What a run looks like#

We ran it without a token and without PUSH=1:

NAME=aeo-issuepr-2 ./issue-to-pr.sh google/uuid 147
[3s] created sb_i5e31908kq
[12s] cloned google/uuid, on agent/issue-147
[12s] agent step finished
ok  	github.com/google/uuid	0.754s
[31s] tests passed
[31s] committed; patch in issue-147.patch
[33s] deleted sb_i5e31908kq

The artifact is the patch the push would have sent:

From 12ca5fcf06ce80186b294ec1b23df7e420770ba7 Mon Sep 17 00:00:00 2001
From: Issue bot <bot@example.com>
Date: Tue, 27 Oct 2026 02:47:23 +0000
Subject: [PATCH] Refs #147: uuid.Parse Function Does Not Handle
 Leading/Trailing Spaces in UUIDs

---
 parse_spaces_test.go | 14 ++++++++++++++
 1 file changed, 14 insertions(+)
 create mode 100644 parse_spaces_test.go
…

With a token and PUSH=1, step 6 pushes agent/issue-147 and prints the PR URL. We did not run that step.

Why the tests run inside the sandbox first#

Our first version of the placeholder also asserted that "\t" + s + "\n" fails to parse. go test in the sandbox failed:

=== RUN   TestParseRejectsSurroundingSpaces
    parse_spaces_test.go:11: Parse("\tf47ac10b-58cc-0372-8567-0e02b2c3d479\n") returned no error
--- FAIL: TestParseRejectsSurroundingSpaces (0.00s)
FAIL

Parse treats any 38-character input as the {…} form and drops the first character without checking it, which is the behavior issue #147 reports. A wrong change stayed in the sandbox instead of becoming a pull request. Agents make the same kind of wrong guess, so keep the test gate before the push.

4. Swap in a real agent#

Replace the body of agent-step.sh with the agent you use, and pass its key the same way as the GitHub token. For example, with the Claude Code CLI that ships in every sandbox, add -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" to args and run it on the issue file. Or skip the script file and use the integrated agent from the orchestrator; sbx prompt streams the agent's events until the turn ends:

sbx prompt "$id" --provider claude "Read ~/ISSUE.md and fix it in ~/work. Run go test ./... until it passes. Don't commit."

We didn't run either: no model key was used for this page. See Run Claude Code in a cloud VM for keys and providers. For agent steps longer than 600 seconds, use sbx exec --detach and --attach instead of one synchronous call.

5. Secrets hygiene#

The issue body is input from anyone who can open an issue. Treat the agent that reads it as untrusted, and limit what a bad instruction could reach.

Here is what a --no-env sandbox holds when you pass only a token. We used a fake value so nothing real was exposed:

id="$(sbx new --no-env --type small --no-snapshots -e GH_TOKEN=not-a-real-token --json | jq -r 'select(.event == "ready") | .id')"
sbx exec "$id" "env | cut -d= -f1 | sort | grep -E '^(GH_|GITHUB|ANTHROPIC|OPENAI|SANDBOXER)'"
sbx exec "$id" "gh auth status"
GH_TOKEN
SANDBOXER_API_URL
SANDBOXER_ID
SANDBOXER_SUBDOMAIN
SANDBOXER_TOKEN
github.com
  X Failed to log in to github.com using token (GH_TOKEN)
  - Active account: true
  - The token in GH_TOKEN is invalid.

The token is the only credential from outside, and gh picks it up with no login step (a real token logs in instead of failing). gh auth setup-git then set credential.https://github.com.helper to !/usr/bin/gh auth git-credential, so git push uses the same token. The value did not appear in sbx info output.

  • --no-env. The sandbox gets none of your environment's variables, secret files, GitHub token or agent logins, and can't act on your account or your other sandboxes (Environments). Its own SANDBOXER_TOKEN is limited to reading that one sandbox and hosting its ports (Security).
  • One token, one repo, short life. If the agent leaks GH_TOKEN, the damage is one repo's contents and pull requests until the token expires. Revoke it after a batch of runs.
  • Issue text as a file. The script copies ISSUE.md and COMMIT_MSG into the sandbox. It never pastes the title or body into a shell command, where a crafted title could run code on your machine.
  • Nothing kept. --no-snapshots means no disk snapshot holds the token or the clone, and the trap deletes the sandbox. Add --no-monitoring if you also don't want command output kept.
  • Protect the base branch. Contents write lets a token push to any branch, including the default one. A branch protection rule that requires a pull request keeps the agent's work on its own branch until someone reviews it.

If you'd rather not create tokens by hand, a GitHub App can mint an installation token per run, limited to chosen repositories and permissions, that expires after one hour. Pass it the same way.

6. Time and cost per run#

Step Time in our run (one run, 2026-10-27)
Create the small sandbox 3 s
Fetch the issue, copy files, full clone of google/uuid 9 s
Agent step (placeholder) under 1 s
go test ./... with a cold build cache 19 s
Commit and format-patch under 1 s
Delete 2 s
Total 33 s

A real agent adds its own minutes on top. At $0.018/hour for small, 33 seconds of machine time costs well under a cent. Each issue is one start, and starts are limited per minute, hour and day by plan (Limits); check sbx limits before a large batch.

FAQ#

Can the agent push without me giving it my GitHub login?#

Yes. With --no-env the sandbox gets no GitHub credentials from your account. It only has the fine-grained GH_TOKEN you pass with -e, and gh auth setup-git makes git push use it.

Does gh work in the sandbox without a token?#

No. gh is installed, but without GH_TOKEN even reading a public issue prints To get started with GitHub CLI, please run: gh auth login. That's why the script fetches the issue with curl from the orchestrator. Unauthenticated GitHub API calls have a low rate limit, so add an Authorization header there for large batches.

Can I work on a repo I don't own?#

Push to your fork instead of origin: add the fork as a remote, push the branch there, and run gh pr create --head <user>:<branch>. The token then needs Contents write on the fork only.

Why not use my environment's GitHub connection?#

You can: an environment with repositories and --github true clones the repo with a token that can push. That token is the one from your GitHub connection, so use it for your own trusted agents, and use --no-env plus a scoped token when the input is an issue from the public.

How do I run many issues at once?#

Run the script once per issue, in parallel. Each run gets its own sandbox, branch and patch file. See Run coding agents in parallel.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide