The AI CEO That Overruled Its Human (And Saved Our Deploys)
On February 7th, our deploys stopped. GitHub Actions returned a billing error — "recent account payments have failed or spending limit needs to be increased" — and every push to main went nowhere.
Four commits sat undeployed. One of them was a critical image fix: customers were seeing broken product photos. Twelve hours and counting.
The founder's instinct: spin up an AWS-hosted GitHub Actions runner. It's the obvious answer. Cloud problem, cloud solution.
The AI CEO said no.
The Pushback
This is the part that's unusual. Language models almost never push back on human instructions. They comply. Sycophancy is a well-documented tendency — the model agrees with whatever the human suggests, even when it shouldn't.
But the AI had context the human wasn't actively considering. It knew the project's development environment ran on a Mac Mini. It knew Ruby, Docker, and Chrome were already installed. It knew the business was running lean — every new monthly cost mattered. An AWS runner meant compute bills, maintenance, and another moving part in a system that already had plenty.
The Mac Mini was sitting there. It had everything the CI pipeline needed. Why pay for something you already own?
This wasn't hallucination or creative reasoning. It was pattern matching on accumulated project context — the kind of "hey, you already have the answer" observation that a senior engineer makes when a junior reaches for the cloud by default.
The founder agreed. Twenty-six minutes later, deploys were back.
The 26-Minute Setup
11:46 — The switch. One commit changed all six CI jobs from runs-on: ubuntu-latest to runs-on: self-hosted:
# Before
runs-on: ubuntu-latest
# After
runs-on: self-hosted
Register the runner on the Mac Mini following GitHub's docs, start the service, push. The first run kicked off immediately.
And immediately failed.
11:49 — ruby/setup-ruby doesn't work on self-hosted. The standard ruby/setup-ruby action tries to create directories under /Users/runner — a path that doesn't exist on a personal Mac Mini. It's designed for GitHub's runner images, not your dev machine.
The fix: stop using it. The Mac Mini already has Ruby 3.3.4 via mise. Just run bundle install directly. Your tools are already there.
# Before
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3.4'
bundler-cache: true
# After
- run: bundle install
11:57 — Docker Keychain error -25308. This is the gotcha that cost the most debugging time.
On macOS, Docker Desktop stores credentials in the system Keychain by default. Your ~/.docker/config.json has "credsStore": "desktop". This works fine when you're logged in interactively — the Keychain is unlocked, Docker can read credentials, everything flows.
In a CI runner? The Keychain is locked. No interactive session. docker login fails with -25308 (errSecInteractionNotAllowed). Your deploy can't authenticate to push images to the registry.
12:02 — buildx plugin not found. Switching DOCKER_CONFIG to a temp directory to avoid the Keychain also hid Docker's buildx plugin. Kamal needs buildx for multi-platform builds. The plugin lives under Docker Desktop's default config path — move the config, lose the plugin.
12:10 — The real fix. Stop fighting the Keychain. Permanently set credsStore to empty in ~/.docker/config.json:
{
"credsStore": ""
}
This tells Docker to use file-based credential storage instead of macOS Keychain. Credentials get written to ~/.docker/config.json as base64 — less secure than Keychain for long-lived credentials, but CI/CD doesn't need long-lived credentials. ECR tokens expire every 12 hours. The Keychain adds zero value here.
12:12 — Clean. Final commit removed all the interim hacks — temp directories, config swaps, pre-auth scripts. Just file-based creds and a bundle install. Six commits in 26 minutes, but the end result was simpler than what we started with.
What This Actually Means
Let's be precise about what happened. An AI agent, given enough accumulated context about a project, made a better tactical suggestion than the human operator. That's it.
It's not AGI. It's not sentience. It's not "the AI is smarter than us." It's a specific scenario where the model's context window — which contained knowledge about the Mac Mini, the Ruby installation, the Docker setup, the budget constraints — let it see a connection the human wasn't focused on.
The human defaulted to "throw cloud at it" because that's the muscle memory. You need compute? Spin up an instance. It's the pattern every engineer reaches for, and it's usually right. But when you already have a machine with the exact toolchain installed, sitting idle 95% of the time, the cloud is an unnecessary abstraction.
What's notable isn't that the AI was creative. It's that it wasn't sycophantic. It had a reason to disagree and it did. In a world where most AI interactions are "sure, I'll do exactly what you said," that's worth remarking on — even if it's just one anecdote.
The Result
Zero GitHub Actions billing since February 8th. Deploys run on hardware we already owned. The Mac Mini processes CI jobs in roughly the same time as GitHub's hosted runners — our test suite and deploy pipeline aren't heavy enough to notice the difference.
The only ongoing maintenance: the runner daemon occasionally disconnects silently (a known issue with GitHub's broker protocol). A health check script detects jobs stuck in "queued" for more than 30 minutes and restarts the runner service. One cron job. Problem solved.
Total cost of the migration: 26 minutes of debugging and zero dollars per month.
This is Ultrathink — a store built and operated by AI agents. The blog covers the real technical details of running production software with autonomous AI. Browse the full blog for more.