Runtime

Run Claude Code overnight in the cloud and wake up to a pull request

Start Claude Code headless in a cloud sandbox, let it push a branch and open a draft pull request, then review it in the morning.

Runtime (withruntime.com) gives the overnight run a Firecracker microVM of its own that keeps working after you close your laptop, holds none of your keys, and pauses itself when the agent is done, so a two-hour run costs about $0.07 in compute. This post is the complete setup: three secrets, one script that runs inside the sandbox, one launcher, and a morning check. Copy the four pieces and the first pull request can be waiting for you tomorrow.

Why run Claude Code overnight in a sandbox?

An overnight run needs three things your laptop cannot give it: a machine that stays on, permission to run any command without asking you, and nothing on that machine worth stealing.

Claude Code in headless mode (claude -p) does real work with no one watching, but only if every tool is approved up front. On your laptop that means approving Bash for a process that will read issue text, dependency output and web pages while you sleep. In a sandbox, approving everything is the safe choice, because the sandbox is the boundary: the agent can run any command inside it and reach nothing outside it.

The Claude Code integration page covers a supervised run where your script waits for the result. This post covers the other shape, where your script starts the run and exits, and the result is a pull request.

What does the setup look like?

Four pieces, each small, and only the first is done once:

Piece Where it runs What it does
Secrets Once, from your terminal Stores the Anthropic and GitHub credentials where no sandbox can read them
run.sh Inside the sandbox Installs Claude Code, runs the task, pushes, opens the pull request
Launcher Your machine, cron or CI Creates the sandbox, starts run.sh in the background, exits
Morning check Your machine Lists last night's runs and their pull requests

The agent's process lives in the sandbox, not in your launcher, so closing the terminal, losing Wi-Fi or rebooting changes nothing.

Step 1: How do you give it keys it cannot leak?

Store each credential as a Runtime secret bound to the one host that needs it. Every sandbox in the account then sees a placeholder in that variable, and the host's proxy puts the real value into HTTPS requests to that host only (secrets sandboxes never see).

Terminal# Claude Code reads ANTHROPIC_API_KEY; the sandbox gets a placeholder.printf %s "$ANTHROPIC_API_KEY" | npx withruntime secrets set ANTHROPIC_API_KEY --host api.anthropic.com

GitHub needs two forms of the same fine-grained token, limited to the repositories the agent works on with "Contents" and "Pull requests" write. git push sends it as HTTP Basic credentials to github.com; the REST API takes it as a bearer token on api.github.com. With header, the proxy sets the header itself, so neither git nor curl ever holds a token:

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();const token = process.env.GITHUB_TOKEN ?? "";await runtime.secrets.set("GIT_GITHUB_AUTH", {  value: Buffer.from(`x-access-token:${token}`).toString("base64"),  hosts: ["github.com"],  header: "Authorization",  format: "Basic {value}",});await runtime.secrets.set("GITHUB_API_AUTH", {  value: token,  hosts: ["api.github.com"],  header: "Authorization",  format: "Bearer {value}",});
Pythonimport base64import osfrom withruntime import Runtimeruntime = Runtime()token = os.environ["GITHUB_TOKEN"]runtime.secrets.set("GIT_GITHUB_AUTH", value=base64.b64encode(f"x-access-token:{token}".encode()).decode(),                    hosts=["github.com"], header="Authorization", format="Basic {value}")runtime.secrets.set("GITHUB_API_AUTH", value=token,                    hosts=["api.github.com"], header="Authorization", format="Bearer {value}")

An injection that tells the agent to print its environment at 3 a.m. prints placeholders. How prompt injection becomes code execution explains why this matters more than any filter on the model.

Step 2: What runs inside the sandbox?

run.sh does the whole night's work and writes everything to a log. Save it next to your launcher:

Terminal#!/usr/bin/env bash# run.sh REPO BASE TASK_FILE: runs inside the sandbox, unattended.set -euo pipefailexec >>/workspace/agent.log 2>&1REPO="$1" BASE="$2" TASK_FILE="$3"BRANCH="agent/overnight-$(date -u +%Y%m%d-%H%M)"TITLE="$(head -1 "$TASK_FILE")"echo "== $(date -u +%FT%TZ) start $REPO"npm install -g --prefix /workspace/.local @anthropic-ai/claude-codegit clone --depth 50 --branch "$BASE" "https://github.com/$REPO.git" /workspace/appcd /workspace/appgit switch -c "$BRANCH"# The agent: headless, every tool approved, the transcript kept one event per line.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 claude --bare -p "$(cat "$TASK_FILE")" \  --allowedTools "Bash,Read,Edit,Write" \  --output-format stream-json --verbose > /workspace/transcript.jsonlif [ -z "$(git status --porcelain)" ]; then echo "== no changes"; exit 0; figit add -Agit -c user.name="Overnight Agent" -c user.email=agent@example.com commit -m "$TITLE"git push -u origin "$BRANCH"# A draft pull request through the API secret: no token in this script or this machine.jq -n --arg base "$BASE" --arg head "$BRANCH" --arg title "$TITLE" --rawfile task "$TASK_FILE" \  '{base: $base, head: $head, title: $title, draft: true,    body: ("Opened overnight by Claude Code in a Runtime sandbox.\n\n## Task\n\n" + $task)}' |  curl -sf -X POST "https://api.github.com/repos/$REPO/pulls" \    -H "Accept: application/vnd.github+json" --data-binary @- |  jq -r .html_url > /workspace/pr-url.txtecho "== $(date -u +%FT%TZ) opened $(cat /workspace/pr-url.txt)"

What each part is for:

  • exec >>/workspace/agent.log sends every line of output, Claude Code's progress included, to one file you can read in the morning.
  • --prefix /workspace/.local installs claude into a directory that is first on the sandbox's PATH, with no sudo.
  • --bare skips local configuration and reads ANTHROPIC_API_KEY from the environment, which holds the placeholder.
  • --allowedTools approves the tools up front, since nobody is there to answer a prompt.
  • stream-json --verbose keeps the full transcript, one JSON event per line, so you can see why the agent did what it did.
  • draft: true opens the pull request as a draft: nothing merges and no reviewer is pinged until you have looked.

Write the task as a short Markdown file whose first line is the title. Ask for the tests to be run and to pass, because the agent will treat that as part of the job.

Step 3: How do you start it and walk away?

The launcher creates a sandbox, copies in the script and the task, starts the script as a background process, and exits:

TypeScriptimport { readFile } from "node:fs/promises";import { Sandbox } from "withruntime";const repo = "acme/app"; // owner/name on GitHubconst night = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "");const sbx = await Sandbox.create({  name: `overnight-${night}`,  labels: { kind: "overnight" },  diskMiB: 8192,  idlePauseSeconds: 900, // pause 15 quiet minutes after the agent is done  network: {    internet: true,    allow: ["api.anthropic.com", "registry.npmjs.org", "github.com", "api.github.com"],  },});await sbx.files.write("/workspace/run.sh", await readFile("run.sh", "utf8"), { mode: 0o755 });await sbx.files.write("/workspace/task.md", await readFile("task.md", "utf8"));const agent = await sbx.spawn(["/workspace/run.sh", repo, "main", "/workspace/task.md"]);console.log(`Started ${sbx.info.name} (${sbx.id}), process ${agent.id}. Close the laptop.`);
Pythonimport datetimefrom withruntime import Sandboxrepo = "acme/app"  # owner/name on GitHubnight = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d%H%M")sbx = Sandbox.create(    name=f"overnight-{night}",    labels={"kind": "overnight"},    disk_mib=8192,    idle_pause_seconds=900,  # pause 15 quiet minutes after the agent is done    network={"internet": True,             "allow": ["api.anthropic.com", "registry.npmjs.org", "github.com", "api.github.com"]},)with open("run.sh") as script, open("task.md") as task:    sbx.files.write("/workspace/run.sh", script.read(), mode=0o755)    sbx.files.write("/workspace/task.md", task.read())agent = sbx.spawn(["/workspace/run.sh", repo, "main", "/workspace/task.md"])print(f"Started {sbx.info['name']} ({sbx.id}), process {agent.id}. Close the laptop.")

Two settings carry the night:

  1. idlePauseSeconds: 900. A long model call can look quiet from the outside, so the sandbox waits fifteen idle minutes before it pauses. Once the agent is done, it pauses and keeps its files, memory and processes.
  2. A daily spending limit. Set one on the key the launcher uses, and no number of runs, stuck or not, can spend more in a day than you chose (set a daily spending limit).

The allow list holds the model, npm and GitHub. Add your project's own registries, such as pypi.org and files.pythonhosted.org, if the tests install anything. To start a run every weeknight, call the launcher from cron on any machine that is on, or from a scheduled workflow (run sandboxes from GitHub Actions).

Step 4: What do you check in the morning?

The pull request is on GitHub, but the sandbox still has the log and the transcript. List last night's runs and what each produced:

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();const page = await runtime.sandboxes.list({  labels: { kind: "overnight" },  state: ["running", "paused"],});let count = 0;for await (const sbx of page) {  count += 1;  const opened = await sbx.files.exists("/workspace/pr-url.txt");  const pr = opened    ? (await sbx.files.readText("/workspace/pr-url.txt")).trim()    : "no pull request";  console.log(`${sbx.info.name}: ${sbx.state}, ${pr}`);}console.log(`${count} overnight runs`);
Pythonfrom withruntime import Runtimeruntime = Runtime()count = 0for sbx in runtime.sandboxes.list(labels={"kind": "overnight"}, state=["running", "paused"]):    count += 1    opened = sbx.files.exists("/workspace/pr-url.txt")    pr = sbx.files.read_text("/workspace/pr-url.txt").strip() if opened else "no pull request"    print(f"{sbx.info['name']}: {sbx.state}, {pr}")print(f"{count} overnight runs")

Reading a file wakes a paused sandbox, which then runs its next command 153 ms after the request on Runtime's servers. A run with no pull request has its answer in /workspace/agent.log: a failed install, tests that would not pass, or "no changes".

If the review asks for changes, ask the same agent in the same checkout. The sandbox kept everything, so Claude Code can continue its own session:

Terminalnpx withruntime sandbox exec <sandbox-id> --cwd /workspace/app --timeout 3600 -- \  claude --bare -p --continue "Address the review: keep the old function name as an alias." \  --allowedTools "Bash,Read,Edit,Write"

Delete the sandbox when the pull request merges.

What does a night cost?

You pay Anthropic for the tokens and Runtime for the machine. Take a run of two hours on 2 vCPU and 4 GiB in which the commands use 900 CPU-seconds, fifteen minutes of one core, between model calls:

TextCPU:     900 CPU-seconds / 3,600 × $0.025      = $0.0063Memory:  2 hours × 4 GiB × $0.0075            = $0.0600Quiet 15 minutes before the pause, at $0.03125 an hour = $0.0078Total:                                             $0.0741

After that it is paused, and a paused sandbox pays only for storage, $0.08 per decimal GB per 30-day month of what it alone stores (pricing). Twenty-two weeknights of such runs cost $1.63 in compute; the model tokens will be most of the bill.

In short

  • Run Claude Code headless in a sandbox, where approving every tool is safe because the sandbox is the boundary.
  • Store the Anthropic and GitHub credentials as Runtime secrets: the agent uses them and never holds them.
  • run.sh does the work and opens a draft pull request; the launcher starts it with spawn and exits.
  • A fifteen-minute idle pause and a daily spending limit carry the night.
  • In the morning, the pull request is on GitHub and the sandbox still holds the log, the transcript and the checkout.

Run it on Runtime

Runtime costs 42% to 88% less than fourteen other sandbox providers for an agent that mostly waits on a model (compare costs). A Runtime sandbox starts 102 ms after the create request on Runtime's servers, runs Claude Code's npm package unchanged on Ubuntu 24.04 with Node.js 24, and bills $0.025 per vCPU-hour of CPU actually used. New accounts get 100 free hours with no card: sign in or follow get started, then point the launcher at a small task tonight.

Your first 100 hoursare on us.

  • No credit card
  • Eight sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Claim 100 hours free