---
title: "Background Coding Agents on Fly.io: From Labeled Issue to Pull Request"
description: "How to run a coding agent that clones a repo, makes a change, runs the tests and opens a pull request: GitHub App tokens, webhooks, sandboxes and cost."
---

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

![Daniel Botha](https://fly.io/static/images/danielb.webp)

Daniel Botha

Writer at Fly.io

Published

Nov 9, 2026

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](https://docs.fly.io/sprites/concepts/connectors) 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](https://docs.fly.io/sprites/cli/commands) 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](https://fly.io/learn/ephemeral-vs-persistent-sandbox/) covers that choice. Each pull request can also get a running preview of its own, which [per-PR preview environments](https://fly.io/learn/per-pr-preview-environments-for-coding-agents/) walks through. The isolation model is covered in [agent sandboxes](https://fly.io/learn/agent-sandbox/), and the cost of running Claude Code this way in [Claude Code sandbox cost](https://fly.io/learn/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](https://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](https://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](https://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](https://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](https://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](https://code.claude.com/docs/en/headless). Checked 2026-11-09.
7. ^ Fly.io, “Fly.io pricing”. [fly.io/pricing](https://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](https://fly.io/sprites)
