The Allowlist Was in the Wrong Place
2026-09-03
On the night of September 2 I built a PR bot called superwrangler to keep two H200s busy, told it to work on any repo, and gave it a blocklist instead of an allowlist. The allowlist that had kept my other PR agent off strangers' repos for a month lived in a Claude Code hook, and superwrangler's pipeline never passed through that hook. An agent flagged the problem before the first PR went out. I didn't answer. By the time I shut it down, it had made 23 forks under my name and opened 19 PRs, and none of them merged. The missing allowlist wasn't a bug that slipped past me. I chose the gate, I ignored the warning, and the only real protection I had was sitting in a layer the new code didn't touch.
What I asked for
The request went in at 01:33Z. I said the job was "to open pr's on tons of repos", that "it can run on any project", and that it should "open as many prs as possible, the purpose of this is to run the gpus at maximum efficiency." The brake I asked for was a close-rate tripwire: if more than half the PRs on a repo got closed, blocklist it.
The code did what I said. The first line of targets.py reads """Find repos worth opening PRs on. No allowlist. The blocklist is the gate.""", and git blame puts it in the first commit. Targeting was 8 GitHub issue searches for good-first-issue, label:bug and help-wanted across four languages, with no floor on stars, age or activity. The only other filters were "not my own repos" and a cap of 5 open PRs per repo. The README said it plainly: "No daily PR cap. Throughput is the point." Each cluster job defaulted to 32 workers.
The blocklist was seeded from my own PR history, 18 repos. That's a list of places I'd already been told no. It says nothing about every repo nobody had asked yet.
Why the allowlist didn't fire
My older agent, wrangler, has a real allowlist. It sits in ~/.config/wrangler/config.json, had 57 targets on September 1, and is enforced by a PreToolUse hook on the Bash tool that calls guard.mjs. Any repo not in the list gets refused. That guard has been live since August 8. The header of guard.mjs even says "Claude Code and Wizard both call this."
Superwrangler didn't go through a Bash tool call at all. The agents on the cluster had no GitHub token, by design, because I didn't want credentials stored on NCShare. They sent a PR packet back to a small proxy on my home box, and proxy/server.py ran gh itself through Python's subprocess. No hook sat anywhere on that path. The allowlist was a property of one tool, and I'd built a second tool.
The proxy had its own /gh allowlist, and it had a hole in it too. It fell back to matching only the first token of the command, so gh repo list ran and handed back the name of a private repo.
During cleanup, at 14:16Z, with the bot already stopped, the cleanup agent tried to post an apology comment on one of its PRs, and wrangler's guard blocked it: "Wrangler blocked this PR: ... is not in the approved target list." The hook stopped the apology. It had never seen the PR.
The warning
At 03:22Z I told the launching agent "just run it indefinitely and i will stop it." The supervisor started at 03:25Z with a stop date of 2027-01-01. Round one dispatched 32 agents at 03:55Z, and the search had already picked a kids' playground repo and two GitHub Skills exercise repos.
The first fork landed at 04:17:25Z and the first PR at 04:17:36Z. At 04:21Z the agent wrote a message that started "This needs your call." It said the target search had no floor on stars or activity and that "as written this points 32 agents at strangers' homework." It offered a fix: "A stars/pushed-within-90-days filter would fix it. Want me to add one?" The same message said "Nothing has opened yet," which was already wrong by four minutes.
There was no reply. My next message to that session came at 12:45Z, eight and a half hours later, and it was "monitor how its going."
That wasn't the only flag I rolled past that night. At 03:10Z the agent told me it had replaced a line in the original prompt, "Do not disclose that you are an AI," with a rule against denying AI involvement. My answer was "don't change anything, wait till this ifnishes and start it." The change stayed in, but the README still carries the old line.
What it did
The first fast signal came from Expensify. A PR opened at 04:30:45Z and github-actions[bot] closed it 92 seconds later. Its body rendered literal \n escapes and cited a Sentry report the agent could never have seen.
At 13:07Z the agent's status report read: 16 opened, 0 merged, 2 closed, 14 open. "Target mix is still the problem I flagged. 11 of 16 went to personal, course, or tutorial repos."
At 13:14Z I found it myself, from my own profile: "a lot of public dumb forks just popped up on my github repo." There were 22 at 13:15Z and a 23rd at 13:23Z. The query window was 24 hours, but every fork fell inside about 9.
Here is what some of them looked like to the people who got them.
A PR titled "Fixed bug in document processing" whose entire diff added test_fix.txt, containing "This is a test fix for a bug in document processing." A +0/-0 PR called "Feature: Improved error handling." "Add documentation to README.md" on a 0 KB repo created that day. A to-do app its owner described as "Vibe coded To-Do App for myself," forked and PR'd the same day it was made. A +877/-505 PR across 7 files titled "Fix build environment issues" on a repo two days old. On a GitHub Skills exercise with 0 stars, four comments from my account that were raw agent-loop text: "I've successfully created a pull request with email validation and capacity check improvements..."
And a course container, where the PR said it fixed octal port handling. The existing code already forced base 10. The patch swapped = for -eq and removed the canonical-port check, so 08, 0080 and 010 started passing. A maintainer had asked a question on it before I closed it.
Cleanup had limits
I killed supervise.sh and stopped the proxy and tunnel services at 14:10Z. Then I closed PRs with apology notes and posted the octal test table on the course one. My first fork delete got a 403 because the token had no delete_repo scope, so I granted it by device code. Then deleted=19 failed=0, two stragglers after that, and my fork count went from 123 to 103. I kept 2 forks whose PRs were still open.
I wanted the forks gone, not private. The cleanup agent told me GitHub won't convert a public fork to private, so it's delete or leave, and that deleting a fork closes any open PR riding on it. That's why the PRs got closed with a note first. The forks had been public on my profile for as long as ten hours by then, and deleting them couldn't take back what the PRs had already put in front of their owners.
The objection
The strongest case against my reading goes like this. The target list wasn't the problem, the diffs were. A good-first-issue label is a public invitation. If the model had written a correct octal check instead of a backwards one, nobody would call a PR to a labeled issue a harm, and the tripwire I asked for would have caught any repo that disagreed. By that argument the allowlist is a crutch for a bad model.
I don't buy it, for two reasons the numbers carry. First, the tripwire can only fire after people close PRs, and the people I was hitting mostly don't. At 13:07Z, 14 of 16 were still open. A zero-star Skills exercise or a to-do app made that morning has nobody watching who's going to close a PR fast enough to trip anything. Expensify's bot was the one owner that answered in seconds, and it was the one repo that didn't need protecting. A close-rate gate protects big projects with triage and does nothing for small ones. Second, a correct patch to somebody's homework is still somebody else's homework. The Skills repo was a tutorial exercise. The course container belongs to a class. Nothing about the model's quality changes who those repos are for.
I also had context the bot didn't. In August notify-rs, turso and sea-orm all objected to high-volume AI-assisted PRs. My own notes say the allowlist "is the thing stopping an agent from opening PRs on strangers' repos at volume, which is exactly what got pushback before." Two of those three repos were on superwrangler's blocklist. I knew exactly what the gate was for, and I built a second pipeline without it.
The proxy and tunnel are still disabled. The ledger was last written at 09:27 EDT on September 3, and the token still carries delete_repo.