Uncle Bob Stopped Reading the Code

He barely looks at the code anymore and reports 5x. What matters is whether the things he looks at instead can fail the run, or only nod at it.

Uncle Bob Stopped Reading the Code

Let's Call It Peek-a-Boo Engineering

Robert C. Martin, the man who wrote Clean Code and spent two decades telling the profession to read every line it ships, posted this on August 19: "It is amazing how much work it is to wrestle these agents to my will. Don't get me wrong, it's crazy productive. But it's also a lot of hard, focussed work. All my software engineering and problem solving skills are brought into play -- even though I barely look at the code." Twenty-five hundred people liked it. A few days later he spelled out the loop he runs now, run the app, have an agent diagnose and file issues, have an agent fix them, have an agent audit the fixes, have an agent repair what the audit found, go back to the top, and when someone asked why he wasn't reviewing the output, he answered plainly: "I don't want to read the code because that slows the whole process down. The last thing a squad of agents needs is me looking over every line of code."

Two days before that first post, The Register published an account of what happens at an outfit with a considerably larger blast radius than a retired consultant's side project when the code review stops and nothing takes its place. On the June 18, a commit co-authored by GitHub Copilot Autofix landed in Snowflake's public snowflake-connector-net repository. It removed the sanitized-input pattern a GitHub Actions workflow had been using and replaced it with direct string expansion inside a run block, which is the textbook shape of script injection in CI. Five days later, on the twenty-third, Wiz's autonomous red-team agent found the workflow during a routine sweep of public repositories, opened an issue with a title crafted to break out of the echo, and walked off with a Jira token that gave it read access to Snowflake's engineering, security compliance, and bug bounty projects. Snowflake patched it the same day, rotated the token, and confirmed from audit logs that Wiz was the only outsider through the door. Nobody got hurt. An AI wrote the bug, an AI found it, and the humans in between had signed off on the change.

Last week we closed with a run on the controls that keep an agent honest, and promised that this one would change altitude and talk about where it's genuinely safe to let go of the wheel. Uncle Bob and Snowflake, in the same week, are the two ends of that question. One is a veteran deliberately not reading the code because he built something else to catch what reading would catch. The other is a change that got the ordinary kind of review, a human looking at a diff a bot labeled as a fix, and shipped a hole. The difference between them is a piece of industrial-safety vocabulary worth borrowing: an interlock. On a press brake or a lathe, an interlock is the mechanism that makes the machine physically unable to run while the guard is open. It doesn't advise, and it can't be persuaded. Letting go of the wheel is legitimate only when the thing you hand it to is a check that can fail the run, and a person nodding at a diff isn't that at all.

COMPANION SCRIPT

Companion script for this issue: interlock. Point it at a pull request and it refuses to let a bot-authored or bot-co-authored change through on review alone. It requires that the named deterministic checks ran and passed, and it scans the diff for the two moves that produced the Snowflake hole: a sanitizer or escaping step removed, and a raw github.event expansion added inside a workflow run block. Either one fails the run with the offending line printed. Hand-raiser keyword: INTERLOCK. The complete version is inline in the Quick Tip below.

What He Looks At Instead

The inventory of what took reading's place is the interesting part of Uncle Bob's week, because he's been unusually specific about it, and every item on that list sorts into one of two bins. In a thread with Kent C. Dodds he argued that AI has made high, meaningful coverage "within easy reach of even the most casual projects," and that the CRAP metric and mutation testing "usually improve the tests considerably." Those are interlocks. A mutation score is a number produced by a program that changes your code and checks whether your tests notice; it can't be flattered, it can't be talked down, and it can be wired to fail a build. A complexity ceiling is the same kind of thing. When he says he barely looks at the code, this is the machinery that lets him mean it.

The other bin is the audit. He has an agent audit each finished task, and he's noticed, with evident amusement, that the auditor "often finds things to fix," and that the same model "almost always passes the second audit." He also posted the instruction that shows which bin the audit belongs in: when you ask the agent to audit itself, "make sure you read it's responses very carefully. Especially the part where it talks about gaps and things that were NOT done. You'll often be surprised." Read that as a control and it's issue #25. The audit is a brief. It's a well-informed opinion from a party that can't be held to it, and Uncle Bob is still the one reading the brief and deciding. He let go of the wheel on the code and kept both hands on it for the audit, and that's the correct allocation, because the audit is the one thing on his list that can't fail the run on its own.

The ratio is the part of his list he doesn't advertise, and it's the part worth stealing. In the reply that put his gain at 5x, the number comes after "all the tools, checks, and ceremony I impose upon them to keep them honest and their output high quality." He watches agents do small tasks ten to twenty times faster than he could, and the ceremony eats most of that back before it becomes the five. That's the price of letting go. You don't get to skip the reading for free; you pay for it up front, in interlocks, and the productivity that's left over is the real number.

FOR FURTHER READING

Snowflake Is What "Reviewed" Looks Like Without An Interlock

Now put the Snowflake commit through the same two bins. Whatever review it got (and a public repository at a company that size has branch protection) had a diff in front of it with Copilot Autofix in the co-author line, arriving under a label that says, in effect, "this is the fix." Issue #25 was about this failure, the moment the bot's summary becomes the thing the human rubber-stamps, and here it is in the wild with a red-team agent on the other end. The reviewer was the only check that could've caught the change, and the reviewer was the one check in the chain that a confident label could soften.

What the check would've needed to see is small. Inside a GitHub Actions run block, an expression such as github.event.issue.title, wrapped in the runner's double-brace syntax, is expanded by the runner into the shell script before the shell ever sees it. Whatever an anonymous user types into an issue title becomes program text with the workflow's permissions and its secrets. That's why the repository had a sanitized-input pattern in the first place, and it's why the commit that removed it wasn't a refactor. A diff that deletes a sanitizer and adds a raw event expansion inside run: has a signature, and a signature is a thing a program can check. The Register later clarified that Wiz couldn't determine whether Autofix introduced the flaw or only failed to correct it during its own review, and it doesn't matter. Either way, a bot touched the trust boundary of a CI workflow and a human ratified it, and nothing between them was built to refuse.

The timeline matters more than the mechanism: five days from commit to exploit, by an agent doing routine sweeps of public repositories, not by a person who was targeting Snowflake. Any workflow with that shape, in any public repo, is now findable at machine speed. The bar for what counts as "reviewed" has to move accordingly. A person looking at a bot's diff was an adequate control when the adversary was also a person with a finite afternoon. It isn't one anymore.

An Interlock Can Fail The Run. A Reviewer Can Only Decline To.

The working definition applies to anything on your own list. An interlock has three properties. It's deterministic: the same change produces the same answer every time, which is what issue #26 was about and why a model at temperature zero doesn't qualify. It can block: a failed interlock stops the run, the merge, or the send, rather than filing a comment. And it can't be argued with: there's no prompt, label, or co-author line that changes its answer, which is the property the Snowflake reviewer lacked and the property issue #28's fuse had by construction. Tests, mutation scores, complexity budgets, lint, SAST rules, and the fusebox ceiling are interlocks. An LLM reviewer, an agent auditing its own work, and a tired human approving a diff at four in the afternoon are advisors. Advisors are valuable; they're not the thing you hand the wheel to.

The same distinction explains the other observation Uncle Bob made this week, about scale. "One agent makes us go faster. Two makes us go even faster. But does that scale linearly?" His answer from experience: six agents arranged in an assembly line worked well, and six running in parallel produced "very bad results," with agents that "create inappropriate interfaces, double up on functionality," and end in "the horrible cascade of merges." CooperBench put a number on the same thing in January: across six hundred paired coding tasks, two agents cooperating succeeded about 30% less often than each agent handling both tasks alone, which the authors named the curse of coordination. An assembly line works because every station is an interlock for the one before it; nothing reaches station three that station two's checks didn't pass. Parallel agents with no interlocks between them are six advisors with commit access, and that's the configuration a mutation score, a contract test, or a merge gate exists to prevent.

Where It's Safe To Let Go

Last week's issue promised an answer, and here it is: it's safe to let go of the wheel inside the interlocks and nowhere else, and "inside" has a testable meaning: the work is either reversible or it lands behind a check that can fail it. Read-only work qualifies almost by definition, the log triage of issue #24, summarization, diagnosis, an agent filing issues into issues.md the way Uncle Bob's first station does. Draft output qualifies, pull requests and staged changes and generated tests, as long as the thing that promotes them is an interlock and not a nod. Bounded batch work qualifies once it's under a fuse. That's a large envelope, larger than the human-in-the-loop-forever crowd will admit, and the reader who just absorbed four issues of controls has earned the other half of the argument: with a mutation score, a golden set, a pinned snapshot, and a fuse in place, you've earned the right to stop reading, and continuing to read every line is spending your engineers on a check the machine already runs better.

The other edge of the envelope is just as sharp. Anything that ratifies stays human; the audit in Uncle Bob's loop is still read by Uncle Bob. Anything with an irreversible side effect stays human or stays behind a deterministic gate: sends, payments, deletes, production deploys. And after this June, one more category belongs on that list explicitly: any change to CI configuration, workflow files, or the sanitizers inside them, whatever the author, whatever the label. Those files are the trust boundary. A bot editing the trust boundary is the one diff a human must read in full, and the interlock's job is to make sure it never arrives without that reading having happened.

QUICK TIP

Refuse The Diff That Removes The Guard

interlock is a pre-merge gate for bot-authored changes. Given a pull request, it checks whether any author or co-author is a bot identity, and if so it requires two things before it will exit zero: every check name you list in INTERLOCK_CHECKS must have completed successfully, and the diff must not remove a sanitizing or escaping line or add a raw github.event expansion inside a workflow run block. It prints the exact line that tripped it. Set INTERLOCK_DIFF to a local diff file to run it offline.

#!/usr/bin/env bash
# interlock.sh - refuse bot-authored PRs that lack passing checks or touch a guard.
# usage: interlock.sh OWNER/REPO PR_NUMBER
#   INTERLOCK_CHECKS   comma-separated check names that must have passed (default: test,lint)
#   INTERLOCK_BOTS     regex of bot identities (default matches Copilot, dependabot, *[bot])
#   INTERLOCK_DIFF     path to a diff file; skips gh entirely for offline runs
#   INTERLOCK_PASSED   newline-separated check names to treat as passed (offline only)
set -euo pipefail

repo="${1:-}"; pr="${2:-}"
checks="${INTERLOCK_CHECKS-test,lint}"
bots="${INTERLOCK_BOTS:-copilot|autofix|dependabot|\[bot\]}"
fail() { echo "INTERLOCK: OPEN - $1"; exit 1; }

if [ -n "${INTERLOCK_DIFF:-}" ]; then
  diff_text=$(cat "$INTERLOCK_DIFF"); authors="${INTERLOCK_AUTHORS:-copilot-autofix[bot]}"
  passed="${INTERLOCK_PASSED:-}"
else
  [ -n "$repo" ] && [ -n "$pr" ] || { echo "usage: interlock.sh OWNER/REPO PR_NUMBER"; exit 2; }
  diff_text=$(gh pr diff "$pr" -R "$repo")
  authors=$(gh pr view "$pr" -R "$repo" --json author,commits \
    -q '[.author.login] + [.commits[].authors[].login, .commits[].authors[].name] | unique | .[]')
  passed=$(gh pr checks "$pr" -R "$repo" --json name,state \
    -q '.[] | select(.state=="SUCCESS") | .name')
fi

# Human-only PRs pass straight through; review remains the control for those.
echo "$authors" | grep -qiE "$bots" || { echo "INTERLOCK: CLOSED - no bot author"; exit 0; }

# 1. Every named deterministic check must have actually passed.
IFS=',' read -ra required <<< "$checks"
for c in "${required[@]:-}"; do
  [ -n "$c" ] || continue
  echo "$passed" | grep -qx "$c" || fail "required check '$c' did not pass"
done

# 2. A removed sanitizer/escape line is a guard coming off.
removed=$(echo "$diff_text" | grep -E '^-[^-]' \
  | grep -iE 'saniti[sz]|escape|quote|shellcheck|allowlist|whitelist' || true)
[ -z "$removed" ] || fail "sanitizer removed:"$'\n'"$removed"

# 3. A raw event expansion added inside a workflow run block is script injection.
added=$(echo "$diff_text" | grep -E '^\+[^+]' \
  | grep -E '\$\{\{ *github\.event\.[a-z_.]*(title|body|ref|label|name|message|login)' || true)
[ -z "$added" ] || fail "raw event expansion added:"$'\n'"$added"

echo "INTERLOCK: CLOSED - checks passed, guards intact"

Run it against a diff shaped like the Snowflake commit and it trips on the first line it should:

$ cat snowflake-shaped.diff
--- a/.github/workflows/triage.yml
+++ b/.github/workflows/triage.yml
-        TITLE=$(sanitize_input "$ISSUE_TITLE")
-        echo "New issue: $TITLE"
+        echo "New issue: ${{ github.event.issue.title }}"

$ INTERLOCK_DIFF=snowflake-shaped.diff INTERLOCK_CHECKS=test INTERLOCK_PASSED=test ./interlock.sh
INTERLOCK: OPEN - sanitizer removed:
-        TITLE=$(sanitize_input "$ISSUE_TITLE")
$ echo $?
1

The tests passed, which is the situation that fooled the reviewer, and the interlock refused anyway on the first guard it found coming off. In the real repository, the reviewer had the same diff in front of them and approved it. The difference is that the script can't be told the change is a fix.

Quick Wins

🟢 Easy (~15 min): Sort every check on one pipeline into the two bins: can it fail the run, or does it only comment? Any bot review, AI summary, or self-audit that sits in the "required" column of your branch protection moves out of it today. Advisors advise; they don't ratify.

🟡 Medium (~1 hour): Add a CODEOWNERS rule that routes every change under .github/workflows/ to a named human, and turn on the branch-protection setting that requires the most recent push to be approved by someone other than the pusher. A bot editing the trust boundary now can't arrive without a full human read.

🔴 Advanced (half day): Wire interlock into the merge queue for one repository, with INTERLOCK_CHECKS set to your real test and lint job names, and add a mutation-testing job to that list. Then pick one agent workflow whose output lands behind those gates and stop reading its diffs for a week. Measure what the interlocks catch. That's the reading you were doing, done by something that can't be flattered.

Next Week

There's a queued topic that this June made harder to keep putting off: the connectors we're handing agents, and why the protocol everyone adopted in a year looks a lot like the one the web spent a decade learning to distrust. More next Tuesday.

Uncle Bob stopped reading the code, and the software profession's most reliable scold about reading the code was right to. He didn't stop checking it; he moved the checking into machinery that can refuse, and he kept his own hands on the one control in his loop that only advises. Snowflake had a human read a bot's diff, which is the arrangement most teams still call review, and a second bot found the hole in five days.

Whether to let go of the wheel was never the question; what you hand it to is. Inside the interlocks, for work that can be reversed or refused, let go, and stop paying engineers to re-run a check a program runs better. At the trust boundary, and anywhere a change can't be taken back, the wheel stays in a hand you can hold to it.

A reviewer can decline to approve. An interlock can refuse to run. Build the second kind and you've bought the first kind's afternoon back.

P.S. Standing up agents with the interlocks wired in before anyone stops reading the diffs is the kind of work NodeBridge does for a living, and the read-only, deterministic spine of the TestScout MCP suite is the same instinct pointed at your test suite. If this issue found you with a bot in the required-reviewers column, forward it to whoever owns branch protection. If someone forwarded it to you, subscribe at bashmatica.com.

NODEBRIDGE AUTOMATION SOLUTIONS

The hardest part of getting agents to work for you is making sure they remember what they need to and forget what they don’t.

NodeBridge Automation Solutions specializes in “second-brain” setups for your LLM to make sure that the artifacts you need your agents to run from are evergreen, while the stale data and rotting context are properly shed, saving time you time and tokens.