Background Coding Agents on Fly.io: From Labeled Issue to Pull Request

Background Coding Agents on Fly.io: From Labeled Issue to Pull Request

Author
Daniel Botha
Daniel Botha
Writer at Fly.io
Published

A background coding agent is a coding agent run unattended on a remote machine: an event such as a labeled issue starts it, it clones the repository, makes a change, runs the tests and opens a pull request for a person to review.

The issue says “add rate limiting to the export endpoint”, someone labels it, and later that hour a branch with a diff and a passing test run is waiting in the pull request list. The agent needed four things to get there: permission to read and push to one repository, a machine to run on, the project’s dependencies installed, and a way to open the pull request when it finished.

Managed products bundle all four. Running it yourself means choosing each one, and the choices decide what the agent can touch, how many run at once and what each pull request costs.


Where Can You Run Background Coding Agents That Open Pull Requests?

Either in a managed agent product, such as GitHub Copilot coding agent, or on infrastructure you control, where each run gets its own isolated machine; for teams that want their own agent, their own limits and no 59-minute ceiling, that machine is a Sprite on Fly.io. The managed route needs no setup and sets the rules for you. The self-hosted route needs a trigger, a credential and a sandbox, and in exchange the run length, the parallelism and the model are yours to set.

What Copilot coding agent limits and charges

GitHub documents the limits of its own agent plainly. A session that runs on GitHub Actions stops at 59 minutes, a limit that “cannot be extended or bypassed”, and each task works on one branch and opens one pull request.[1] Model usage is billed as AI credits at $0.01 each, with the rate per token set by the model.[2] Cloud sandbox sessions are billed separately, at $0.000024 per compute second and $0.000003 per GiB-second of memory.[3]

GitHub Copilot coding agent Self-hosted agent on a Sprite
Session length 59 minutes on Actions, not extendable No lifetime cap on the Sprite; long steps run in the background
Work per task One branch, one pull request One branch and pull request per issue here; the orchestrator can do more
Parallel runs No published figure One Sprite per run, up to the organization’s active-Sprite limit
Agent and model Copilot, with a model picker Any agent CLI and any model
Cost per pull request AI credits ($0.01 each) plus sandbox compute and memory Model tokens plus Sprite CPU and memory time

Copilot figures from GitHub’s documentation[1][2][3], checked 2026-11-09.

Neither side publishes a per-pull-request price, because both bill by usage.


How Should a Background Agent Authenticate to GitHub?

With a GitHub App installation token minted for one run and scoped to one repository, never a personal access token. A personal token carries everything its owner can do, across every repository they can reach, for as long as it lives. An agent run follows instructions from an issue body that anyone with write access, or sometimes anyone at all, can edit, so whatever the token can reach, a hostile issue can try to reach too.

Scope the token to the run

An installation token is created by the app’s backend with an API call, and that call takes two narrowing parameters. repositories limits the token to named repositories, and permissions limits it to named permissions; neither can exceed what the installation was granted.[4] For a clone-to-PR run, that means one repository with contents: write and pull_requests: write, and nothing else. The token expires after one hour, so a run that leaks it leaks a credential for one repository that is useless an hour later.

The token still enters the sandbox, because cloning and pushing need it, and the agent runs in the same sandbox with a shell, so it can read the token too. The scope set at minting, one repository for one hour, is the real limit. A credential broker can keep the token out for GitHub API calls: on Sprites, a GitHub connector stores the credential in the organization, the Sprite calls GitHub through the Sprites gateway, and the connector’s access policy can restrict which API paths a Sprite may reach.


How Does a Webhook Trigger an Agent Run?

A small always-on service receives GitHub’s webhook, checks its signature, answers at once, and starts the run somewhere else. The receiver should never run the agent itself: webhook deliveries expect a quick response, and an agent run takes minutes.

Verify, then hand off

GitHub signs each delivery with the webhook secret, an HMAC SHA-256 of the payload sent in the X-Hub-Signature-256 header and prefixed sha256=.[5] The receiver recomputes it over the raw body and compares the two with a constant-time comparison, dropping anything that fails. Then it filters: an issues event with action labeled and the agreed label, such as agent, is a run; everything else is ignored.

The handoff carries three things: the repository, the issue number and a freshly minted installation token. The receiver creates a sandbox for the run and returns 202. Keeping the receiver separate also gives one place to enforce a concurrency limit and a spend cap, since every run starts there.


What Happens Between Clone and Pull Request?

Seven steps from a known clean state: create the sandbox, clone with the scoped token, install dependencies, run the agent headless, run the tests, open the pull request only if they pass, then destroy the sandbox.

The run script

The steps run as one script inside the Sprite, because an agent run takes longer than a single exec call should be held open. On Sprites where sprite exec takes a timeout, the default is 30 seconds and the ceiling 10 minutes, so the receiver starts the script in the background and polls for a marker file. This is run.sh for a Node repository:

set -e
trap 'touch /run.done' EXIT
gh repo clone "$REPO" /work && cd /work
npm ci
claude -p "Implement issue #$ISSUE. Run the tests until they pass." \
  --allowedTools "Read,Edit,Bash"
npm test
git checkout -b "agent/$ISSUE"
git add -A && git -c user.name=agent -c user.email=agent@users.noreply.github.com commit -m "Fix #$ISSUE"
gh auth setup-git
git -c core.hooksPath=/dev/null push origin "agent/$ISSUE"
gh pr create --fill --body "Closes #$ISSUE"

set -e makes the tests the gate: a failing npm test ends the script before anything is pushed. Pushing with core.hooksPath=/dev/null stops a git hook the agent wrote from running with the token. The receiver drives it:

sprite create $RUN
sprite exec -s $RUN --file run.sh:/run.sh \
  --env REPO=$REPO,ISSUE=$ISSUE,GH_TOKEN=$TOKEN,ANTHROPIC_API_KEY=$MODEL_KEY -- \
  sh -c 'setsid nohup sh /run.sh > /run.log 2>&1 &'
# poll: sprite exec -s $RUN -- test -f /run.done
sprite destroy --force $RUN

The model key comes from the receiver’s own secrets; a dedicated key with a spend limit set at the model provider caps what one runaway run can cost. For a Python repository, npm ci becomes uv sync and npm test becomes pytest. The Sprites CLI reference covers every flag used here.

Warm the cache once, not every run

A fresh Sprite repeats the dependency install every run. The alternative is one warm Sprite per repository, created with a checkpointable disk rather than an ephemeral one: clone and install once, take a checkpoint, and before each run restore it and pull the latest commit, so the install fetches only what changed. Runs on that repository then take turns on the warm Sprite, while parallel runs each get a fresh Sprite and pay for the full install.

Claude Code runs non-interactively with -p and a prompt, and --allowedTools limits which tools it may use without asking.[6] When the tests fail, the log in /run.log holds the failure, and the receiver can start a second attempt with it in the prompt or open a draft pull request that says what failed.


How Much Does One Agent Pull Request Cost?

Two meters: the model tokens the agent spends, and the compute time of the machine it runs on. The model share depends on the model, how much of the repository the agent reads and how many attempts it takes.

The compute half

Fly.io’s pricing page lists Sprite CPU time at $0.0385 per CPU-hour and memory at $0.021875 per GB-hour of actual use.[7] As an illustration of the arithmetic, not a measurement: a run that keeps two CPUs busy for 15 minutes with 4 GB in use costs 2 × 0.25 × $0.0385 plus 4 × 0.25 × $0.021875, about $0.04 of compute. The same page meters CPU as cumulative use rather than wall-clock time, so a run waiting on the model pays for memory, not CPU.

The model half has no fixed figure: it is the provider’s per-token price multiplied by what the agent reads and writes, and a large repository read end to end costs more than a narrow fix. The receiver is where a spend cap belongs, because it can refuse to start a run once the day’s budget is gone.


Background Coding Agents on Fly.io

Fly.io runs both halves of this system. The webhook receiver is a small app on a Fly Machine that stays running, verifies GitHub’s signature, mints the installation token from the app’s private key held as a Fly secret, holds the model key the same way, and creates the run. Each run is a Sprite: a hardware-isolated Linux microVM with its own kernel. The issue author controls the agent’s instructions, so treat whatever the agent runs as untrusted; in a Sprite it runs in its own VM, apart from the receiver and from every other run, holding only the one-hour token for one repository. A Sprite’s egress is open by default, and a network policy can narrow it to GitHub, the package registry and the model provider.

Sprites come with Claude Code and other coding agents preinstalled alongside Node.js and Python, so a run needs no image of its own. A Sprite has no lifetime cap, so a long test suite or a second attempt after a failing test does not hit a 59-minute wall. Parallelism is one Sprite per run, up to the organization’s active-Sprite limit, and a checkpointed Sprite per repository lets sequential runs start warm.

For agents that should keep working on the same branch after review comments, a Sprite can sleep instead of being destroyed and wake with its filesystem intact when the next comment arrives; ephemeral vs persistent sandboxes covers that choice. Each pull request can also get a running preview of its own, which per-PR preview environments walks through. The isolation model is covered in agent sandboxes, and the cost of running Claude Code this way in Claude Code sandbox cost.


Frequently Asked Questions

Is it possible to run Claude Code in the cloud?

Yes. Claude Code runs non-interactively with claude -p and a prompt, so it can run on any remote Linux machine. On Fly.io, Sprites ship with Claude Code preinstalled, and a webhook receiver can start a run in its own Sprite for each labeled issue.

Can a background agent push directly to main?

Yes, if its token allows it, which is why it should not. Have the agent push a branch named for the issue and open a pull request, and protect main with a branch rule so a person reviews every agent change, whether the agent runs on Fly.io or anywhere else.

How many agents can run in parallel?

As many as the organization’s active-Sprite limit allows. On Fly.io each run is its own Sprite, and within that limit the concurrency and spend caps set in the webhook receiver decide how many run at once.

Should each run get a fresh environment?

Yes, or a clean restore of a known state. On Fly.io either create a new Sprite per run or restore a checkpoint taken after the dependencies were installed, on a Sprite with a checkpointable disk; both start the agent from the same state.

How do I stop an agent from leaking the GitHub token?

Assume the agent can read it and make that matter little. Mint a GitHub App installation token per run for one repository with only contents and pull request write access, which expires after an hour. On Fly.io, a Sprites GitHub connector can also keep the token out of the Sprite for GitHub API calls.

What happens if the agent’s tests fail?

Nothing is pushed if the run script stops on a failing test. The receiver on Fly.io can then start a second attempt with the failure in the prompt, or open a draft pull request that says what failed.

Can the same setup review pull requests instead of writing them?

Yes. Trigger on the pull_request event instead of issues, check out the branch in a Sprite on Fly.io, ask the agent for a review, and post it as a comment with a token scoped to pull request write access.


Sources

  1. ^ GitHub Docs, “GitHub Copilot on GitHub.com and GitHub Mobile”. docs.github.com/en/copilot/concepts/agents/coding-agent/about-coding-agent. Checked 2026-11-09.
  2. ^ GitHub Docs, “Models and pricing for GitHub Copilot”. docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing. Checked 2026-11-09.
  3. ^ GitHub Docs, “Billing for cloud and local sandboxes for GitHub Copilot”. docs.github.com/en/billing/concepts/product-billing/cloud-and-local-sandboxes. Checked 2026-11-09.
  4. ^ GitHub Docs, “Generating an installation access token for a GitHub App”. docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app. Checked 2026-11-09.
  5. ^ GitHub Docs, “Validating webhook deliveries”. docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries. Checked 2026-11-09.
  6. ^ Anthropic, “Run Claude Code programmatically”. code.claude.com/docs/en/headless. Checked 2026-11-09.
  7. ^ Fly.io, “Fly.io pricing”. fly.io/pricing. Checked 2026-11-09.

Give each run a Sprite

A hardware-isolated Linux computer per agent run, with Claude Code preinstalled.

Sign up