Cutting CI Costs in AI-Driven Solo Development with gh-signoff — Local CI and the Signoff Model

Tadashi Shigeoka · Thu, July 2, 2026

Introduction

Since I started handing implementation work to coding agents like Claude Code and Codex, the shape of my CI bill has visibly changed. The reason is simple: agents commit far more granularly than humans and push far more casually. Where a human would try three things locally and push once, an agent pushes three times. CI runs per pull request multiply, and in solo development there are no concurrent branches from teammates to warm the caches either.

This is where gh-signoff, published by Basecamp, earns its keep. It pulls CI back onto your own machine with a blunt proposition: run the tests locally, and when they pass, sign off yourself. As the README puts it, remote runners are fantastic for repeatable builds and parallelized execution, but many apps do not need all that.

I came across the tool through a post by Jesse Hanley: “Killed Github Actions for CI and now just bin/signoff everything locally.” The detail worth noting is bin/signoff rather than gh signoff directly, meaning the command is wrapped in a script that lives in the repository. As covered below, folding tests and signoff into a single script is what makes this workflow hold together.

This post walks through how gh-signoff works at the implementation level, how to fit it into an agent-driven solo workflow, and where the model breaks down.

Why CI Costs Balloon in AI-Driven Solo Development

Start with what you are actually paying for. According to GitHub’s runner pricing reference, a standard Linux 2-core runner costs 0.006 USD per minute, while macOS 3-core and 4-core runners cost 0.062 USD per minute. That 10x gap on macOS matters a lot if you build iOS or macOS targets in CI.

Costs accumulate along three paths in solo work.

  • Push frequency. Agents run a “just run it and look at the output” loop, so push-triggered workflows fire several times more often than they would for a human.
  • Matrix multiplication. Three Node.js versions times two operating systems equals six jobs. Reasonable insurance on a team, billed in full on every push when you work alone.
  • Retry churn. Every red build triggers a fix-and-rerun cycle, even though ten seconds locally would have told you whether the fix was right.

The third one is the real issue. Remote CI exists primarily to confirm that someone else’s code does not break in your environment. When there is only one developer, there is no “someone else.” Pulling the feedback loop back to your machine is both faster and cheaper.

The argument was first put forward by DHH in We’re moving continuous integration back to developer machines. HEY’s test suite (55,000 lines of Ruby, 5,000 test cases plus over 300 system tests) takes 5m30s on remote CI but finishes in under 2m45s locally on an Intel 14900K. With an M3 Max at 16 cores and even an M2 MacBook at 8, the parallelism available on a development machine is already sufficient. The post is equally explicit about scope: codebases in the millions of lines, at the likes of Shopify or GitHub, still need remote infrastructure. The author acknowledges this is a bounded approach.

What gh-signoff Is

gh-signoff is a GitHub CLI extension implemented as a roughly 618-line Bash script (v0.2.1 at the time of writing). What it does is startlingly simple: it calls the Commit Status API to create a single success status on the current HEAD.

gh extension install basecamp/gh-signoff

After installation, the basic loop is just this.

# Run the tests locally
bin/rails test
 
# Sign off once they pass
gh signoff

Under the hood, gh signoff issues this API call.

gh api --method POST \
  "repos/:owner/:repo/statuses/${sha}" \
  -f state=success \
  -f context="signoff" \
  -f "description=${user} signed off"

${user} comes from git config user.name, so the pull request records who signed off. That is the heart of the tool: it replaces a green check that a machine vouched for with one a human declared responsibility for.

Wiring It to Branch Protection

To make signoff mandatory, use the install subcommand, which configures branch protection.

gh signoff install                 # require on the default branch
gh signoff install --branch main   # name the branch explicitly
gh signoff check                   # verify the requirement is in place
gh signoff uninstall               # remove the requirement

This creates a required status check named signoff, so commits without a signoff cannot be merged.

Partial Signoff

When your CI has multiple stages, you can split the signoff into contexts.

gh signoff install --branch main tests lint security

That registers signoff/tests, signoff/lint, and signoff/security as three separate required status checks. You sign off at the same granularity.

gh signoff tests lint security   # all at once
gh signoff tests                 # just the tests, for now

gh signoff status shows where you stand, reconciling the required contexts against the statuses actually present and printing or for each.

Shell completion is available too. Add the following to ~/.bashrc and completion will pull the real context names out of branch protection.

eval "$(gh signoff completion)"

The Overall Flow

flowchart LR
    DEV[Run tests locally] -->|pass| SO[gh signoff]
    DEV -->|fail| FIX[Fix]
    FIX --> DEV
    SO -->|POST /statuses/:sha| ST[Commit status: signoff]
    ST --> PR[Pull request]
    BP[Branch protection<br/>required status checks] --> PR
    PR -->|status is success| MERGE[Mergeable]
    PR -->|status missing| BLOCK[Blocked]

The Guard Against False Signoffs

“If it is self-reported, what stops me from signing off without running anything?” is the obvious objection. gh-signoff has exactly one guard, in a function called is_clean, which checks two conditions.

  • git status --porcelain is empty (no uncommitted changes)
  • git log @{push}.. is empty (no unpushed commits)

Fail either and you get repository has uncommitted or unpushed changes. The design prevents you from signing off on state that exists only on your machine. A missing tracking branch is also an error.

The -f flag bypasses the check.

gh signoff create -f

Treat that as an emergency escape hatch. If you reach for it routinely, you have given up most of the value of using gh-signoff at all.

Combining It with Coding Agents

Here is the part that matters. If you delegate implementation to an agent, the signoff step has to move to the agent side too, or the human becomes the bottleneck.

The straightforward approach is to fold tests and signoff into a single script and have the agent run that.

#!/usr/bin/env bash
# bin/signoff
set -euo pipefail
 
npm run lint
npm run typecheck
npm test
 
git push
gh signoff tests lint

The important property is that set -e halts the script the moment tests fail, so gh signoff is never reached. The script enforces the invariant that a signoff can only occur as a side effect of passing tests. That is far more reliable than telling an agent in natural language to “run the tests and then sign off.”

If you use Claude Code, you can go further and run this script from a hook on Stop. Verification and signoff then happen automatically when the agent finishes its work, leaving you to review only the substance of the pull request.

Another option is to push the check into a pre-push hook.

#!/usr/bin/env bash
# .git/hooks/pre-push
set -euo pipefail
npm test

This keeps signoff separate but guarantees tests run before any push. Its advantage is that it is hard for an agent to route around, no matter how it ends up pushing.

Cost Breakdown

Some concrete numbers. Assume one CI run takes 5 minutes on a Linux 2-core runner and you push 20 times a day.

ItemMonthly
Runs20 × 22 working days = 440
Minutes consumed440 × 5 = 2,200
Cost at 0.006 USD/min13.20 USD

On its own that may fit inside the included allowance and is hardly alarming. The problem starts when multipliers enter. A matrix of two operating systems by three Node.js versions is 6x, which becomes 13,200 minutes and 79.20 USD. Swap in macOS runners and the same 2,200 minutes costs 136.40 USD. Annualized, these are not amounts you shrug off.

Moving that work local zeroes out the bill but consumes your development machine’s time instead. A suite that takes 5 minutes on an Apple silicon MacBook Pro really does occupy a terminal for 5 minutes. That is an honest trade-off. On the other hand, remote CI adds queueing, checkout, and dependency restoration on top of the actual test time, while local runs benefit from warm caches. In practice total wall-clock time often comes out shorter.

When It Fits and When It Does Not

The conditions under which this approach works are fairly clear-cut. It fits when:

  • There is one developer, or a small group with established trust.
  • The test suite runs self-contained locally, with few external service dependencies.
  • Test execution stays within a few minutes.
  • The target is an internal tool, a prototype, or a personal project rather than a flagship product.

Stick with remote CI when any of the following apply:

  • You maintain open source and accept pull requests from outside contributors. Running untrusted code’s tests on your own machine is a security risk in itself.
  • You need cross-platform build verification. Your macOS machine cannot show you how the Linux build breaks.
  • Compliance requires artifacts verified by an independent system.
  • Your tests take 30 minutes. Waiting locally costs more than the runner does.

The realistic middle ground is to combine both. Stop running everything on every push: drop or slim the push-triggered workflow, and keep the heavy matrix only for merges to main and a weekly scheduled run. Day-to-day feedback comes from local runs and signoff. That alone eliminates most of the cost in the table above.

Gotchas

gh-signoff is a thin script and behaves predictably, but a few things are worth knowing in advance.

The biggest one is that gh signoff install overwrites your existing branch protection. The script PUTs to the branch protection API with these fields.

required_status_checks[strict]=false
enforce_admins=null
required_pull_request_reviews=null
restrictions=null
required_status_checks[contexts][]=signoff

A PUT to that endpoint is a full replacement, so running it on a repository that already requires reviews or restricts pushes will wipe those settings. On repositories with existing configuration, skip install and add the signoff context to required status checks manually through the GitHub UI or API.

Likewise, gh signoff uninstall issues a DELETE against branch protection. Per-context removal is not implemented (there is a TODO in the script saying as much), so the entire protection configuration comes off.

It is also worth noting required_status_checks[strict]=false. The strict setting requires a branch to be up to date with its base before merging, and disabling it means no re-verification is demanded if main moves after you sign off. You can end up merging code you validated against a stale base. If that bothers you, re-enable strict in branch protection, accepting that every movement on main will then force a rebase and a fresh signoff.

gh-signoff also assumes classic branch protection and does not support rulesets. On repositories already migrated to rulesets, install and check will not behave as expected, and you will need to declare signoff as a required status check on the ruleset side.

On the permissions front, branch protection on private repositories requires GitHub Pro for personal accounts, or GitHub Team or above for organizations. install fails on private repositories under the free plan.

Finally, and this is operational rather than technical: a signoff is a declaration, not a machine-backed guarantee. That is precisely why git config user.name lands in the description, recording whose declaration it is. Whether that record carries weight in your environment is the deciding factor for adoption. In solo development it only carries weight with your future self, but that is usually enough.

Conclusion

Delegating implementation to coding agents drives commit and push counts past what human habits would produce, and remote CI charges scale right along with them. gh-signoff breaks that proportionality with a simple mechanism: run the tests locally and sign your name to them. The implementation amounts to posting one green status through the Commit Status API, given teeth by a required status check in branch protection.

If you adopt it, the key move is folding tests and signoff into a single script that the agent invokes. Making signoff possible only as a side effect of passing tests closes most of the gap left by self-reporting. Beyond that, check two things up front: that install will overwrite your existing branch protection, and that rulesets are not supported.

That’s all from cutting AI-driven solo development’s CI costs with local signoff via gh-signoff, from the Gemba.

References