The agent session is the CI runner — and it is already paid for
The agent checks in its own cloud session, the pull request skips: how a commit trailer saves GitHub Actions minutes — and which four routes failed.
Anthropic has been handing Pro and Max subscribers up to $250 in credit since late September, for Claude Code cloud sessions only. The message: let the agent work in the cloud. What is easy to miss: each session runs in its own VM with several cores, and there is no separate charge for it.
So the agent already has a full build machine. It writes the code there, it runs the tests there — and still sends every push to GitHub Actions, where the same tests run a second time, on minutes you pay for. This article shows how we turned the session into a CI runner, which four routes the session proxy closed off, and which one was left.
The bill: every change runs twice
We noticed when the included Action minutes ran out — not at the end of the month, but in the middle of it, in a private repository where agents do most of the work. Look into why, and you find the same test suite three times in one morning: once in the session where the agent wrote it, once on the push to the pull request, once after the merge.
That is how the usual setup checks every change at least twice in the cloud: on every push to the pull request, so you know whether you may merge, and again after the merge on main, because a new commit appears there. Only the run on main is really needed — it checks what ships. The PR runs are advance checks. If the agent already ran the same checks in its session, they are pure repetition.
The cloud session is already a CI runner
A cloud session starts from a fresh clone. What it does on the way up is controlled by a SessionStart hook in the repo, in our case .claude/hooks/session-start.sh. It does three things:
- Dependencies and git hooks:
uv sync,npm ci,pre-commit install. The session then runs the same commit and push hooks as a laptop. - The services the tests need: whatever runs as a service container in CI starts inside the session's container — for us a database, which the hook creates and announces through an environment variable. That covers the tests that need more than one process.
- The switch: it sets
PREPUSH_CI=full. The pre-push hook then runs everything the PR CI would check: the whole test suite, the linter across the entire repository, the format check and every other validation.
The result: a git push from the session takes a good two minutes instead of a few seconds — and by then it has checked everything GitHub Actions would check. The agent waits those two minutes out; you only have to give it a longer timeout for the push. Without the variable, on your own laptop, you keep the fast pre-push with the tests the push touches.
The session VM costs nothing extra — according to Anthropic, cloud sessions count against the subscription's normal usage limits. The two minutes of checking burn almost no tokens: the agent waits and reads the hook's output at the end. And the machine is stronger than what GitHub gives a private repository:
|
Cloud session |
GitHub runner, private |
|
|---|---|---|
|
CPU / RAM |
4 vCPU, 15 GB |
2 vCPU, 7 GB |
|
Full check, our repo |
2:07 min |
5:48 min |
|
Cost per run |
included in the subscription |
6 billed minutes, plus side jobs |
Session figures measured with nproc and free in a running session, runner figures from the GitHub docs. Public repositories get 4 vCPU and 16 GB for free — if you work in the open, you do not have a minutes problem at all.
Which leaves the real question: how does the PR CI learn that it can save itself the work?
Context: moving CI back to the machine is not a new idea
The idea is not new. In April 2024 David Heinemeier Hansson scrapped 37signals' remote CI and moved the checks back onto developer machines: the pipeline for HEY took five and a half minutes, his Linux box less than half that. For codebases the size of Shopify's, he concedes, this does not hold.
Tools followed. Basecamp's gh-signoff sets a green signoff commit status via gh signoff after local tests, which a ruleset then requires for the merge; Rails 8.1 ships bin/ci with every new app, optional signoff included.
The other approach replays the pipeline locally: act runs GitHub Actions workflows in Docker, Dagger writes pipelines as code that behaves the same locally and in CI. Both save push-and-wait cycles, but no minutes — the cloud CI still runs afterwards.
And yes, at first glance this contradicts the classic. In Continuous Integration, Martin Fowler asks for every push to be built on an integration machine — precisely because "works on my machine" proves nothing. That objection comes back further down. We did not argue it away; we accepted part of it on purpose.
The catch: the session proxy and four rejected receipts
After a passing pre-push, the session has to leave behind some receipt that a small gate job in the PR CI can read. Sounds trivial. But a cloud session is deliberately walled off: git and the GitHub API run through a proxy that keeps the real credentials outside the VM and lets only certain operations through. Three of our four ideas died there, the fourth died on git itself:
- Git note pushed to
refs/notes/checks. The git proxy accepts branch pushes only; per the docs it rejects "branch deletions and pushes of anything other than branches". HTTP 403. - Commit status, the gh-signoff way. The proxy blocks writes to
/statuseswith a 403, and your own token as a network secret does not help — secrets are never attached to GitHub requests. On a laptop with your ownghlogin this would be the right choice; in a walled-off agent environment it is not. - Receipt branch
checks/<sha>, which the gate job reads and deletes. Probably workable, but: a naming rule, recursion guards, write permissions for CI, cleanup. Too many moving parts. - Trailer via wrapper, written into the message with
--amendafter the checks pass. A pre-push hook can no longer change the commit; the SHAs are already fixed. You would need a push script everyone uses, and whoever forgets it silently falls back to the full CI.
Two things surprised us. The 403 on the status API does not come from GitHub — the response carried neither Server: github.com nor X-GitHub-Request-Id; the proxy produced it itself, while a read of the same API returned 200. And this block is documented nowhere; only GraphQL and third-party repositories are. If you are wondering why your token has no effect: look at the response headers first.
The solution: the trailer is a requirement, not a receipt
The decisive turn came from a simple question: why not write the trailer first? We assume the checks will pass. If they fail, no push happens, and we fix it.
That changes what it means. Prepush-CI: full does not say "these checks passed", it says "this commit may only leave the machine after the full check". The hook enforces exactly that. If a commit carrying the trailer arrives on GitHub, the checks must have passed, or it would not be there.
- At commit time: if
PREPUSH_CIis set, acommit-msghook writes the trailer into the message withgit interpret-trailers.--where startputs it beforeCo-Authored-By,--if-exists doNothingmakes it idempotent. - At push time: if the pushed commit carries the trailer, the full mode runs — whatever the variable says. Otherwise someone could commit in the cloud and push later from a laptop without the variable. pre-commit passes the pushed SHA to the hook as
PRE_COMMIT_TO_REF. - Honesty check: a pre-push hook checks the working tree, not the commit. So it aborts if the pushed commit is not the checked-out
HEAD, or if the tree is dirty. - In the PR CI: a
changesjob readsgit log -1 --format='%(trailers:key=Prepush-CI,valueonly)'from the head SHA. If it saysfull, every other job is skipped. On pushes tomainthe trailer is ignored; everything always runs there.
The result: no extra refs, no API write permissions, no wrapper, no cleanup. A rebase onto main is covered automatically — the new commits keep the trailer, so the next push runs the full check again on the new state. On the first real pull request the gate job reported VERIFIED: true, and both workflows skipped everything but the changes job.
Of course anyone can write the trailer by hand. The same is true of the note, the status and the branch, and gh-signoff ultimately trusts whoever signs off too. Local CI is a trust model, not a security model.
What we deliberately leave open
Here is where Fowler's objection comes back. The pre-push checks the branch; the PR CI would have checked the trial merge against current main. If a function was renamed there in the meantime and the branch still calls it, both halves are green and the combination is red. In practice the gap is small: anyone who rebases onto main before merging triggers the full check again, because of the trailer, on exactly the state that gets merged. It only stays open if you let GitHub do the rebase, which bypasses the hooks — then main goes red, the run there blocks the deployment, and someone fixes it. Production stays intact. We could close the gap by skipping only when the branch is up to date, but that costs full runs for every stale branch. Instead we count how often main goes red for this reason.
The tool exists now: prepush-ci
We ran the idea through a software factory. Out came prepush-ci, built by five Claude Code loops on Spool — the hub for long-running Claude Code agents our colleague Enes Alataş is building — and on PyPI since today: uv tool install prepush-ci, or uvx prepush-ci skill and let the agent set up the repo; it asks for the check command and opens a pull request.
More came out of it than the draft above: the hook manager does not matter (native, pre-commit, husky, lefthook), there is a gate for GitHub Actions and GitLab CI, PREPUSH_CI=off plus prepush-ci strip is the documented way out when a push has to go without checks for once, and doctor finds a broken setup. The variable is PREPUSH_CI=full, the trailer Prepush-CI: full. Version 0.1.0, pre-alpha. Agents built a tool so that agents can push more cheaply.
Not just Claude: every agent VM is a runner
Claude cloud sessions were the occasion, not the point. Cloudroom gives every agent its own sandbox with admin rights, 100 cloud agents a month for free; HumanLayer keeps sessions running on cloud daemons when the laptop is shut; Spool puts each of its loops in a container of its own. In all of them the full check runs before the push goes out. PREPUSH_CI=full in the sandbox is all that is missing for the PR CI to know it.
The more pushes come from agents, the more lopsided the old arithmetic gets. Fowler's integration machine stays — for main, where the merged state is checked. On the pull request it never ran for that reason, but because the hosted runner was the only machine everyone had. The agent now has one of its own.
Summary
- The machine is already there. An agent's cloud session runs in its own VM with four cores and no separate charge. The same check takes 2:07 there instead of 5:48, and costs no Action minutes.
- The trailer is a requirement, not a receipt. The hook writes
Prepush-CI: fullat commit time and enforces the full check at push time. If a commit with the trailer arrives, the check must have passed — otherwise it would not be there. - Only the pull request saves. The gate skips the jobs that already ran. On
maineverything still runs, because that is the integration machine that counts. - The gap stays open. The pre-push checks the branch, not the trial merge against current
main. Rebase first and the full check runs again; leave it to GitHub and you risk a redmain— and you find out before the deployment. - What you need. A check command, two hooks and one line in the workflow. Local CI is a trust model, not a security model: anyone who writes the trailer by hand can do so.