Three months ago I wrote that letting go of the wheel was the wrong instinct — that accepting agent-written code without really understanding it is the AI equivalent of merging a PR after a 30-second glance. I want to be clear up front: nothing in this post walks that back. I still pore over every piece of delivered work. What’s changed is the size of the loop I’m reviewing. I used to hand Claude a single issue and check the result; now I hand it an entire feature and check the result. Same hands on the wheel — much longer laps.
This post is about the machinery that makes the longer lap possible: a container that makes a dangerous flag safe, skills that close the gap between what Claude knows and what I want, and the same boring test suites and pipelines we should have been building all along. Fair warning — as much of my spend now goes on analysis and design as on delivery, and I don’t just mean my time. The token bill says the same thing: specifying the work costs as much as doing it.
The loop got wider
The biggest change isn’t any single tool — it’s where the effort goes. A feature now starts with Claude and me writing a Product Requirement Document together, a workflow I’ve borrowed from Matt Pocock. Then Claude breaks the PRD down into deliverable issues, and I sanity-check the decomposition the way I’d review a colleague’s sprint planning. Only then does the actual build start: an orchestrator sequencing the work and dispatching it to developer and reviewer sub-agents, each role with its own deliberately limited access to the GitHub repo.
The unit of delegation grew from an issue to a feature. The price of that growth is paid up front, in analysis and design — and the token bill proves it.
One detail that took me a while to land on: the orchestrator isn’t a sub-agent. It’s a skill, running in the outer Claude process, because sub-agents can’t spawn sub-agents. That non-obvious constraint dictates the whole architecture.
---name: afk-orchestratedescription: Unattended runner that drives a small batch of ready issues to merged. Spawns isolated developer and pr-reviewer agents, loops on review feedback, and queues anything needing a human for the morning. Use when the user wants to work a PRD's issues automatically, run the dev loop overnight, batch-merge ready tickets, or "AFK build issues #N..".---
The two sub-agents it spawns:
---name: developerdescription: Implements a single GitHub issue end to end on a feature branch using the afk-work-issue skill (TDD inner loop, unattended plan-check) and hands the open PR back to the orchestrator. Acts as the dobbstech-dev GitHub App. Runs unattended — never prompts. Does not review or merge its own work; the pr-reviewer is the review gate.tools: Bash, Read, Write, Editmodel: sonnet---
---name: pr-reviewerdescription: Handles one open PR end to end — reviews it against coding standards and architecture using the afk-code-review skill, then either merges it or pushes back. Acts as the dobbstech-review GitHub App. Has authority to merge approved PRs. Escalates anything needing a human to the orchestrator.tools: Bash # gh cli for all GitHub interactionsmodel: sonnet---
There’s a lot packed into those few lines. Separation of duties is enforced by identity, not honour — the developer acts as one GitHub App, the reviewer as another, so the developer literally cannot merge its own work and the repo’s audit trail shows two distinct actors.
Regular readers might smell a contradiction here: I’ve spent years arguing that humans should collaborate up front rather than hand finished code over a wall for review. I stand by every word of that — for people. Hand-offs between humans are expensive: context gets lost, feedback arrives late, and the loop stretches to days. Between agents, a hand-off costs seconds and nobody’s ego is bruised by the third round of pushback. The pr-reviewer objects, the developer picks the feedback up immediately, and the cycle repeats as tightly as you like. AI actually benefits from separated contexts more than humans do, while paying almost none of the price humans pay for the separation. This isn’t me discovering a love of review gates — it’s the tightest dev/review cycle I’ve ever had, and it happens to run through two identities.
Yes, the pr-reviewer has genuine merge authority — an approved PR merges with nobody watching. That’s the boldest call in the whole setup, and it’s exactly why the review gate is a separate identity running its own review skill rather than the developer marking its own homework (more on homework later). And notice the escalation paths: the reviewer escalates anything needing a human to the orchestrator, and the orchestrator “queues anything needing a human for the morning”. The run doesn’t pretend to be fully autonomous. It triages.
AFK dies at the permission prompt
My favourite version of AFK looks like this: three hours running trails, glancing at my phone via /remote-control, watching “PR merged” emails arrive one by one.
That whole picture dies instantly at the first permission prompt. Claude Code’s prompts are the right default, but an agent that pauses to ask “may I run dotnet test?” in a terminal nobody is looking at isn’t being safe — it’s being idle, and I don’t find out until I’m home. The answer is the flag with the least reassuring name in modern software: --dangerously-skip-permissions. The name is not marketing. On your host machine, “dangerous” means everything you can touch, it can touch. I wasn’t prepared to run that on my own machine, and you shouldn’t be either. What I was prepared to do was run it somewhere that can’t hurt me.
The fix for a dangerous flag isn’t courage. It’s a smaller blast radius.
So Claude Code lives in a hand-rolled container. The full Dockerfile is in this gist — every comment in that file is something that bit me — but the header tells you the shape of it:
# Hand-rolled Claude Code AFK dev container — .NET 10## Provides an isolated environment where Claude Code can run with# --dangerously-skip-permissions safely:# - .NET 10 SDK (from Microsoft's official devcontainer image)# - Persistent ~/.claude volume so a Max-subscription login survives runs# - Default-deny egress firewall (see project-firewall.sh)FROM mcr.microsoft.com/devcontainers/dotnet:1-10.0
The blast radius is shrunk twice. The mount boundary means the only writable thing in the container is the one repo I mounted — worst realistic case, Claude makes a mess that git can restore in seconds. And the default-deny egress firewall means a misbehaving agent can’t reach anything I didn’t allowlist. Filesystem and network, both fenced.
Two excerpts worth calling out. The version pin, because the CLI can’t (and shouldn’t) update itself in there:
# Pinned deliberately. The CLI cannot self-update in this container (npm global dir# is root-owned, and the container is --rm anyway), so this pin is the single source# of truth for the version. Bump it and rebuild to update.ARG CLAUDE_CODE_VERSION=2.1.201
And the firewall stanza, including a trap for anyone building their own:
# Firewall script — note the DISTINCT name (not init-firewall.sh).# The Anthropic devcontainer *feature* installs its own init-firewall.sh to# /usr/local/bin/ and will silently overwrite a file at that path if you ever# layer the feature on. A different filename avoids that trap.COPY project-firewall.sh /usr/local/bin/project-firewall.shRUN chmod +x /usr/local/bin/project-firewall.sh \ && echo "vscode ALL=(root) NOPASSWD: /usr/local/bin/project-firewall.sh" \ > /etc/sudoers.d/firewall \ && chmod 0440 /etc/sudoers.d/firewall
Sharp eyes will notice that the user Claude runs as can execute that firewall script as root. Before you head to the comments: the script is apply-only. There’s no teardown or flush path, so the only thing the agent can ever do with its root access is turn the firewall on. Worst case, Claude re-applies its own cage.
Writing skills: mind the gap
I love the SOLID principles — they’ve stood the test of time rather better than their author. To me they’re the edge of the blade: apply them appropriately and keeping everything else clean gets much easier. So when I found a comprehensive-looking SOLID skill online, I wired it into my TDD skill without a second thought.
My next single-issue cycle sat at “cogitating…” for the best part of an hour before I killed it and asked Claude what the problem was. The answer, delivered with the patience of a colleague explaining something slightly awkward, was that Claude already knows the SOLID principles — it has read more C# than I ever will. Restating them added nothing but weight. What it actually needed from me was the part no training set contains: when I want each principle applied, and how.
Don’t write down what good code looks like. Write down where your definition of good differs from everyone else’s.
So I rewrote the skill. Here’s the Open/Closed section from the version I actually use:
### Open/Closed Principle (OCP)**When this matters:**- When a class represents a behaviour that is likely to be modified in future (e.g., a rule engine, a data processor).- When a class represents a domain concept that may have multiple implementations (e.g., a payment gateway).**Patterns to use:**- Strategy pattern for varying algorithms- Template method for varying steps- Dependency injection for varying implementations- Prefer composition over inheritance**Red flags:**- If a change impacts unrelated code, it's a sign that the class is not open for extension.- If changes to a class regularly require modifying existing code in a way which impacts its usage, it's a sign that the class is not closed for modification.
Not one word defining OCP. The skill assumes complete fluency and only supplies judgement: when this matters, which patterns I prefer, and the red flags that let Claude self-diagnose a violation. That when/how/red-flags structure has become my template for every convention I write down. A skill isn’t teaching. It’s disambiguation — Claude already knows what the Open/Closed Principle is; what it can’t know is when I think it matters.
There’s a pleasing symmetry in how I got here, too. In the last post, the watershed moment was asking Claude how to use Claude. This time it was asking Claude why it had stalled. The tool remains its own best manual.
Claude can’t mark its own homework without a marking scheme
Here’s the least fashionable opinion in this post: the thing that most determines whether a feature-length agent run works is not the model, the prompts, or the sub-agents. It’s whether your repo has the same boring safety net we’ve been telling ourselves to build for twenty years — fast unit tests, honest integration tests, a UI suite that actually runs, and a pipeline that refuses to pass broken work. Claude, like any developer, needs to be able to verify its own work. While I’m out on the trails, the tests aren’t a quality gate at the end — they’re the only reviewer in the room.
AI doesn’t make your test suite less important. It makes your test suite the only adult in the room.
The marking scheme is written down where Claude can’t miss it:
## Definition of doneAn issue is not complete until, inside the container:- `dotnet build` passes with zero warnings- `dotnet test` is green — unit AND integration suites- Anything touching razor/React has a passing Playwright run- The change is committed with a message explaining *why*Never report work as complete with skipped or deleted tests.If a test seems wrong, stop and say so instead.
But tests only tell you the code behaves; they don’t tell you the change is the one you planned. So I’ve added a second signal. When Claude and I write the PRD, it includes the expected impact at a code level — which modules are new, which get modified, which projects won’t be touched at all — and the relevant slice of that list travels into each issue. The reviewer’s job then includes comparing the delivered footprint against the planned one: did this issue touch what it said it would, and nothing it said it wouldn’t? I allow drift proportionally — the smaller the change, the more drift I’ll tolerate percentage-wise. A two-file issue growing a third file is noise. A forty-file issue growing another ten is a design decision nobody approved.
The CI/CD pipeline is the independent second gate. The developer agent verifies inside the container; the pipeline verifies on neutral ground. Same reason we never trusted “works on my machine” from humans.
Review didn’t disappear — it moved
If any of this sounds like I’ve stopped reading the code, I’ve written it badly. The review didn’t disappear — it moved to the feature boundary, and it changed shape. When a run finishes, I pore over what’s been delivered, but I’m reviewing how the codebase is growing, not just whether a diff is correct. I also point analysis skills at the solution to tell me what a feature’s worth of agent work has done to it.
When something puzzles me, I ask Claude to walk me through the decision and we discuss alternatives — a code review where the author is present, which regular readers will recognise as a hill I’ve died on before. And when I simply disagree, I make the change by hand and record the preference in the CLAUDE.md. That’s the part no human junior can offer:
Every review finding becomes a standing instruction. The corrections compound.
Over to you
If you’ve widened your own loop — from changes to issues, or issues to features — I’d love to know what made it possible, and what snapped the first time you tried. And if you’re spending more on analysis and design than you expected, does that feel like overhead to you, or like the job finally getting the attention it deserved?