How I built an agentic fixer loop for TheBest.Ink with GitHub Actions
2026-07-21
2026-07-21
For a while I kept reading that loop engineering is the next big change in how we work with coding agents. The idea is that you stop prompting the agent yourself. Instead you build a small system that prompts it for you. Several prominent voices in the industry have made the case for it, among them Addy Osmani in his blog post Loop Engineering. I found the idea convincing, but it was not obvious to me how to start on my own projects.
The part that mattered most to me was the warning at the end. A loop only works if you stay the engineer. You still read the code. You still decide what is worth doing. The human check is not a small detail. It is the thing that keeps the loop safe.
This post is my first small step in that direction. It is not a full loop yet. It is one agent that takes one issue, writes the code, runs the checks, and opens a pull request. I read the pull request and I merge it myself. Here is how I built it.
TheBest.Ink is a side project of mine, and an ambitious one. For a long time it had no real backlog. I kept a Notes file with things I wanted to build, and that was it.
The Notes file was full of small items. A note that should show in the dashboard, some text missing in one language, moving some section to anohter position, etc. Mostly those issues feel too small and with so low priory that I never touch them and the list never shrinks.
I wanted a way to get that work done without turning each item into a task for myself. What I wanted was a system that finishes the small changes that never feel worth the time on their own, while I stay the one who decides what is worth doing and what gets merged.
The idea is simple. I take one small issue and instead of doing it by hand, an agent checks out the code, implements it, runs the checks, and opens a pull request. I read it and merge it. The agent only does the part in the middle, the part I keep putting off.
The work lives in GitHub Issues. They are easy to query, they link to the pull request that closes them, and their labels act as a small state machine: candidate, ready, in progress, in review, done, plus blocked. One rule keeps it safe. The fixer, the agent that writes code, only acts on issues I have marked ready, and only I can mark an issue ready. It never approves its own work, and I merge every pull request myself.
The issue template just requires one thing: what needs to be done. While I am working on something else in Claude, I can ask it to create an issue for a thing I just noticed and would otherwise forget. The queue fills from my normal work, without me stopping to write a formal ticket. Before this, those thoughts usually stayed in the Notes file and were forgotten. Now they become issues I can approve later.
Claude can run scheduled tasks that start a fresh session each time. This was my first choice, because it needs almost no setup, and in theory it is exactly what a nightly job wants.
It did not work for my case, for a concrete reason. The fresh session started empty. It had no checkout of the repository, no GitHub tooling, and no working way to attach the repository to itself. It could not read the code, so it could not do anything with it. There is another mode that runs inside an existing session instead of a fresh one, and that session can see my code, but the environment behind it is temporary and goes away after a while. For a job that has to run every day on its own, neither option was reliable enough.
So I moved the whole thing to a place that already has my code and my tools: GitHub Actions.
The agent runs as a GitHub Actions workflow on a self-hosted runner, a Mac mini I already own. It uses the official Claude Code GitHub Action to run Claude. Here is the core of it.
name: Agent Fixer
on:
schedule:
- cron: "0 23,0-6 * * *" # every hour from 23:00 to 06:00 UTC, my quiet hours
workflow_dispatch: {}
concurrency:
group: agent-fixer
cancel-in-progress: false
jobs:
fixer:
runs-on: self-hosted
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.FIXER_GH_TOKEN }}
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version: 24
- run: pnpm install --frozen-lockfile
- uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.FIXER_GH_TOKEN }}
claude_args: "--permission-mode bypassPermissions"
The runner is a Mac mini that already runs my CI, so it has the full toolchain. That matters because the agent verifies its own work, and some of those checks need a database and other services my CI already sets up. If the agent cannot run the same checks I run, I cannot trust its pull requests.
claude_code_oauth_token is my Claude subscription, not an API key that bills per use, so a run draws on my plan instead of my card. Running at night is a side benefit here: the subscription costs the same whether I use it or not, so the fixer works through my quiet hours on capacity I would otherwise leave idle while I sleep. FIXER_GH_TOKEN is a personal access token rather than the built-in one, because pull requests opened with the built-in token do not trigger CI, and I want the agent's pull requests checked like any other. bypassPermissions is there because a scheduled job has no human to approve tool prompts, so without it every action the agent tries is denied. That is a real trade, safe here only because I write every issue and review every pull request.
The workflow also lives in the repository as a normal file, and every run shows up next to my CI, so I can see what ran and when. Getting here took a few small fixes that are not worth listing, the kind of thing I only found by turning the full output on and reading it.
The change that made the agent trustworthy was not a better prompt. It was making myself the gate on every pull request.
The first real run showed the shape of it. The tasks was to put a warning text in the artist dashboard. The agent found the right component, reused an element that already existed instead of inventing new UI, wrote the copy in English, German, and Spanish and opened a pull request with the changes. That is the behaviour I wanted.
Today the agent handles small, well scoped tasks, and I fill the queue myself, either by writing issues or by asking Claude to file them while I work. That is the current limit. The queue only fills as fast as I fill it.
The fixer agent works. The next step I am building is a finder agent, one that reads the code, the running app, Sentry and proposes issues on its own.
With both in place, the full chain looks like this:
finder (proposes the work) -> me (approve what should be done) -> fixer (implements and opens a pull request) -> me (accept or reject, then merge to main)
I stay at both gates. The agents do the work between them, and I keep the two decisions that matter: what is worth doing, and what is good enough to ship. More about that soon.