A reviewer that can run the code catches what reading misses. On Runtime, its machine costs $0.03125 per hour while the model thinks, for 2 vCPUs and 4 GiB. Runtime bills measured CPU, not reserved cores. That made it 42% to 88% cheaper than fourteen other sandbox providers on a job that mostly waits. The rates were checked on 25 October 2026 (the comparison).
Reading a diff versus running it
A model that only reads a diff guesses. It cannot tell whether a new test
passes, whether a changed function still type-checks, or whether the
"harmless refactor" breaks an import two files away. Given a machine, the
reviewer can find out. It can then report the command and its output as
evidence. That turns "this might fail" into "npm test fails on line 41 with
this error." A person can act on that comment.
The machine must be isolated. By definition, the code under review is not yet trusted. The model is also partly guided by text inside the pull request, where a prompt injection can hide.
Split the bot into two halves
| Half | Where it runs | Holds |
|---|---|---|
| Reviewer | A Runtime sandbox | The checkout, the tools, no write token |
| Poster | Your service | The GitHub token that can comment |
The sandbox never holds a token that can merge, push or comment. If the pull request persuades the model to do something hostile, the worst the model can reach is a throwaway machine.
The core loop
TypeScriptimport { Sandbox } from "withruntime";import { sandboxTools } from "withruntime/tools";const repo = "https://github.com/your-org/your-repo.git";const pr = 1234;await using sbx = await Sandbox.create({ diskMiB: 8192, timeoutSeconds: 3600, idlePauseSeconds: 900, labels: { bot: "review", pr: String(pr) }, network: { internet: true, allow: ["github.com", "*.github.com", "registry.npmjs.org"] },});await sbx.exec(["git", "clone", repo, "repo"], { check: true, timeoutMs: 300_000 });await sbx.exec(["git", "fetch", "origin", `pull/${pr}/head:pr`], { cwd: "/workspace/repo", check: true,});await sbx.exec("git checkout pr && npm ci", { cwd: "/workspace/repo", check: true, timeoutMs: 600_000,});// Facts to seed the model's context before it starts exploring.const changed = await sbx.exec("git diff --name-only origin/HEAD...pr", { cwd: "/workspace/repo" });const tests = await sbx.exec("npm test", { cwd: "/workspace/repo", timeoutMs: 900_000 });const lint = await sbx.exec("npx eslint $(git diff --name-only origin/HEAD...pr -- '*.ts')", { cwd: "/workspace/repo", timeoutMs: 300_000,});const tools = sandboxTools(sbx); // the model can run and read anything else it needsconsole.log(changed.stdout, tests.exitCode, lint.exitCode, tools.length);// ... the model's review loop runs here, and returns { summary, comments } ...Pythonfrom withruntime import Sandboxfrom withruntime.tools import sandbox_toolsrepo = "https://github.com/your-org/your-repo.git"pr = 1234with Sandbox.create( disk_mib=8192, timeout_seconds=3600, idle_pause_seconds=900, labels={"bot": "review", "pr": str(pr)}, network={"internet": True, "allow": ["github.com", "*.github.com", "pypi.org", "*.pythonhosted.org"]},) as sbx: sbx.exec(["git", "clone", repo, "repo"], check=True, timeout_ms=300_000) sbx.exec(["git", "fetch", "origin", f"pull/{pr}/head:pr"], cwd="/workspace/repo", check=True) sbx.exec("git checkout pr && pip install -e '.[test]' ruff", cwd="/workspace/repo", check=True, timeout_ms=600_000) changed = sbx.exec("git diff --name-only origin/HEAD...pr", cwd="/workspace/repo") tests = sbx.exec("python3 -m pytest -q", cwd="/workspace/repo", timeout_ms=900_000) lint = sbx.exec("ruff check $(git diff --name-only origin/HEAD...pr -- '*.py')", cwd="/workspace/repo") run, read, write, ls = sandbox_tools(sbx) print(changed.stdout, tests.exit_code, lint.exit_code) # ... the model's review loop runs here, and returns summary and comments ...Seeding the context with the changed files, the test result and the lint result saves the model several tool calls. From there, the model uses the four sandbox tools to read callers, run a single test, or try an edge case the author did not.
Post the review from your service
GitHub's POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews takes a
body, an event of APPROVE, REQUEST_CHANGES or COMMENT, and
comments with a path, a line and a body each. GitHub warns that
creating content too quickly there can trigger secondary rate limits, so post
one review per pull request, not one call per comment.
TypeScripttype Finding = { path: string; line: number; body: string };export async function postReview( owner: string, repo: string, pr: number, summary: string, findings: Finding[],) { await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${pr}/reviews`, { method: "POST", headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, Accept: "application/vnd.github+json", }, body: JSON.stringify({ body: summary, event: "COMMENT", comments: findings }), });}Use COMMENT, not APPROVE, until you trust the bot. A person still merges.
Private repositories
To let the sandbox clone or read a private repository without holding a token, store the token as a Runtime secret limited to GitHub's hosts. Every sandbox gets a placeholder in its environment. The proxy puts the real value into HTTPS requests to those hosts only (secrets sandboxes never see). Give the token read access only. This is the reviewer half.
What a review bot needs
| Need | How Runtime covers it |
|---|---|
| Running untrusted pull request code | A Firecracker microVM with its own kernel per review |
| A token the model cannot leak | Secrets added by the proxy; the guest holds a placeholder |
| Tools for any model framework | sandboxTools(sbx) or sandbox_tools(sbx), and adapters for ten frameworks |
| Tests with a database or queue | sudo enable-docker, then Compose, inside the same sandbox |
| Waiting on the model | $0.03125 an hour at 2 vCPU, 4 GiB; idlePauseSeconds pauses when idle |
| A second look after new commits | Pause keeps the checkout and installs; the next call wakes it |
| Many open pull requests | 100 sandboxes at the same time on a paid account |
What it costs
Assume 2,000 reviews a month. Each review keeps a 2 vCPU, 4 GiB sandbox running for 15 minutes, mostly waiting on the model. Install, tests and lint use 120 CPU-seconds:
- CPU
- 2,000 × 120 s / 3,600 × $0.025
- $1.67
- Memory
- 2,000 × 900 s / 3,600 × 4 GiB × $0.0075
- $15.00
- Total
- $16.67
Under a cent a review, at $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour (pricing). Memory dominates because most of a review is the model reading. That is why a sandbox that pauses when idle pays off. Your model provider bills the model tokens. Every account has 100 free hours of a 1 GB machine in its first 30 days, with no card.
Sources
- GitHub REST API: create a review for a pull request: the endpoint, its fields and the secondary rate limit note, read 26 October 2026.
- Checking out pull requests locally, read 26 October 2026.
Related: CI for agent pull requests, static analysis agent, coding agent sandbox, Claude Code in a sandbox.
Facts on this page were checked on 26 October 2026.