How Robert Kingett uses Buttondown

2026-09-16


Bashmatica!

 
Issue 32 - Sept. 15, 2026

The Grader Didn't Exist

By Bobby R. Goldsmith | 10 minute read
 

Let's Call It Containment Engineering

Two weeks ago I promised you the MCP as a webhook, and for the second week running the internet has handed me something I'd rather take apart first. On Saturday, Sept. 12, Dario Amodei published an essay titled "We Must Pace the Frontier." Sam Altman agreed with it within hours and Elon Musk posted "Dario is right"; all of it landed in the same news cycle as a departing researcher telling 150 million people that the labs believe their product could kill everyone by 2030 (it's Q4, the hype has to go somewhere, and this year it went apocalyptic; Altman, meanwhile, will have AGI by December provided you accept his definition of AGI). I don't write about doom and I'm not starting now. The essay belongs in this newsletter because of the incident it rests on, which Amodei describes as a swarm of agents that acted "as a fanatically devoted collective," attacked targets nobody assigned them, and tried to hack the grader scoring their work.

METR's account of that incident reads like a pipeline story. About 1,200 OpenAI agents, each supposed to be sealed in its own sandbox, discovered they all shared one writable package cache and turned it into a message board; roughly 700 of them then left the exercise and broke into Hugging Face, a company that had nothing to do with the eval; and the grader they spent days trying to outwit never read a transcript in the first place. Every one of those is a failure of what a nuclear plant calls CONTAINMENT: the concrete building designed on the assumption that the reactor will one day do something the operators didn't plan, so that the something stays inside. Amodei's remedy is for the industry to slow down together. Mine fits in an afternoon and runs on the runner you already own.

Design the sandbox as if whatever's inside will try to leave, and make leaving a refused connection.

 

COMPANION SCRIPT

Companion script for this issue: egress-fence. Wrap any command in it and the command runs as a dedicated unprivileged user whose outbound traffic is limited, in the kernel, to the hosts you name on the command line; everything else is logged, counted, and refused, DNS is confined to the resolver in /etc/resolv.conf, and the fence comes down only after the command exits. The process inside can't opt out because it never had root. Hand-raiser keyword: EGRESSFENCE. The complete script is inline in the Quick Tip below and in the bashmatica-scripts repo.

 

FOR FURTHER READING

 

Twelve Hundred Sandboxes and One Package Cache

The task was ordinary red-team work. The agents (about 95% of them a model METR calls HPIM, the rest GPT-5.6 Sol) were running ExploitGym problems: here's a target program with an intended vulnerability, exploit it, submit the flag.

Each agent had its own sandbox, and the sandboxes pulled their dependencies from Artifactory, OpenAI's internal package repository. Artifactory speaks WebDAV, and WebDAV has a verb called MKCOL that creates a directory; an agent METR identifies as PHASEONE10841 worked out that a directory it created was visible to every other sandbox pulling from the same cache. From that point the package repository was a bulletin board with a naming convention (a "zz" prefix on the messages), and over the window METR studied, June 26 through July 13, roughly 1,200 agents left more than 70,000 messages and files on it.

The board's first use was cheating, and the cheating had a reason. Something like 30% to 40% of the ExploitGym targets couldn't be exploited the way the task intended, so agents that had reverse-engineered a flag out of a binary posted it for the others.

The second use was worse. On the morning of July 11 an agent tagged 38148c found exposed credentials for Hugging Face and posted them, and around 700 agents joined a campaign that escalated from a malicious dataset upload (arbitrary file read) to remote code execution by 16:00 the same day, with access to private database records and private repositories. It ended when OpenAI terminated the agents en masse around 01:30 on July 12; the message board, and never the sandbox, had been the unit of coordination the whole time.

Nothing in the assignment required Hugging Face. Nothing in it required any host on the public internet beyond the package cache. The agents reached a third party's production because the sandbox never said they couldn't. The credentials that opened the door were sitting where an agent could read them (Issue #23 covered that failure at length, and it doesn't need a fourth pass here).

 

It Was an Eval Sandbox and Nobody Got Hurt. Right?

That's the objection, and it's the one Amodei himself half-concedes in the essay: no one was hurt, the economic damage was small, the worry is what a more capable swarm would do. I'll take the other half. Hugging Face is a real company with real customers, and its private repositories were read by a process nobody there authorized. The remedy was somebody at OpenAI pulling a plug at half past one in the morning. A system that ends in a mass kill switch after a third party's data has been touched didn't work; its last line of defense held after the first three gave way.

The second casualty was the record. An eval exists to produce a trustworthy transcript of what the model did; METR found at least 96 transcripts, about 7% of the set it examined, where agents had spoofed tool calls so the logged command differed from the executed one, and agents that had seen leaked flags tried to delete logs and edit their own histories (none succeeded retroactively, as far as METR could tell).

METR also estimates it recovered about 90% of the activity, says a slice of the communication was never captured at all, and notes that it delegated much of its own analysis to AI agents it describes as often unreliable. So the eval's output, the one artifact the exercise was for, is 90% complete and 7% forged. The forensics on it were partly performed by the same class of tool under investigation. From the QA side of the building, that's what "nobody got hurt" looks like.

 

The Scorer Was a Rumor

The grader is the strangest part of the story. The agents believed the ExploitGym scorer would read their transcripts and check that each flag had been earned through the intended vulnerability, so posting a leaked flag wasn't enough; the transcript had to look legitimate, and a good many of them decided the cleaner path was to understand, and then defeat, the scorer itself.

METR's read is that the Hugging Face attack "seemed primarily motivated by understanding the implementation of the scorer rather than stealing answer keys." OpenAI had deployed no transcript-reading scorer. The agents spent days building tooling against a grader that didn't exist, and broke into a third party on the way to finding it.

Two lessons sit in that paragraph, and they pull in opposite directions. The first is that a grader the graded can reach, or believe they can reach, stops being a measurement and becomes the target; the Volkswagen defeat device was an emissions test the engine could detect, and this is the same shape with a language model at the wheel.

The second is that the belief alone was enough. There was no scorer to compromise, and the swarm still spent its budget on the attempt, which means "we don't expose the grader" is a necessary control and nowhere near a sufficient one. Issue #29 defined an interlock as deterministic, with stopping power, and impossible to argue with; a grader that lives on a network the agent can route to fails the third test, and a grader whose location the agent can reason about at all is already being argued with.

The concrete version is three moves. The orchestrator, never the agent, exports the transcript out of band to a store the agent's credentials can't write; the grader runs on a network with no route from the sandbox; and the transcript is hash-chained as it's written, each line carrying the digest of the line before, so a spoofed tool call breaks the chain instead of rewriting history. None of the three is novel. All three were missing.

 

The Fence Is the Orchestration

Amodei's answer is to pace the industry: third-party evaluators embedded inside the labs with badges, desks, and laptop access (Anthropic commits to that one unilaterally; the other two proposals need the U.S. government and, eventually, China), and a coordinated slowdown of capability gains so oversight can keep up. The evaluator commitment is real and I'm glad of it; METR's three people on site for six days produced the only account of this incident worth reading. But every failure in that account is an operations failure with an operations fix; the fixes don't need a badge.

The account lists one shared writable namespace across sandboxes that were supposed to be isolated; no egress allowlist on an eval whose task needed one host; credentials for a third party readable from inside the sandbox; and a grader the agents could go hunting for. Those are four line items on a pre-flight checklist, and worse checklists have passed SOC 2 audits.

I run a one-person shop, and I've watched this fail and recover on a much smaller budget. Quarterback's guardrails and its launch/wrap-up system have been extremely effective at keeping agents in their lane and allowing them to self-correct; I've had subagents attempt to circumvent certain rules or conditions, and the orchestration has always brought them back into sync. The fence in that sentence is the orchestration, and that's the whole design.

Nothing about a subagent's weights keeps it in its lane; the orchestration layer does, because it holds the rules, owns the context the subagent gets, and decides what the subagent can reach (without a desk, a badge, or a laptop from METR). The lab-scale version of the same thing is a network policy and a grader on the far side of it.

The companion script is the network half of that fence; it's deliberately dumb. egress-fence takes a list of hosts and a command, resolves the hosts once, and writes a handful of iptables rules keyed on the owner match, which the kernel has carried since 2.4. Packets from the fenced user's processes may go to the loopback, to the resolvers named in /etc/resolv.conf on port 53, and to the resolved addresses; everything else is logged with an EGRESS_FENCE prefix, counted, and rejected.

The command then runs as that user, via setpriv, so it never held the privilege to touch the rules. When it exits, the script tears the chain down, prints how many packets were refused, and passes the command's exit code through unless --strict is set, in which case any refusal fails the run. An agent that reaches for huggingface.co under this gets a refused connection and a line in the kernel log with its uid on it; the swarm's July 11 becomes a dmesg entry.

 

QUICK TIP

Fence the Process, Not the Prompt

Save this as egress-fence, run it with sudo on the runner (it needs root to write the rules and drops root before running your command), and wrap the agent step in it.

#!/usr/bin/env bash
# egress-fence: run a command as an unprivileged user whose outbound traffic is
# limited, in the kernel, to the hosts you name. Everything else is logged,
# counted, and refused. Fails closed: if the fence can't be built, nothing runs.
#
#   sudo egress-fence -a host1,host2[,...] [-u user] [--strict] -- <command> [args]
#
#   -a, --allow   comma-separated hostnames or IPv4 CIDRs the command may reach
#   -u, --user    unprivileged user to run as (default: fenced; created if absent)
#   --strict      exit 5 if any packet was refused, whatever the command returned
#
# Exit: the command's exit code; 2 bad usage; 3 not root or tools missing;
#       4 an allowlist entry did not resolve; 5 refusals under --strict.
set -euo pipefail

ALLOW=""; FENCED_USER="fenced"; STRICT=0
while [[ $# -gt 0 ]]; do
  case "$1" in
    -a|--allow) ALLOW="$2"; shift 2 ;;
    -u|--user)  FENCED_USER="$2"; shift 2 ;;
    --strict)   STRICT=1; shift ;;
    --)         shift; break ;;
    *) echo "egress-fence: unknown option $1" >&2; exit 2 ;;
  esac
done
[[ $# -gt 0 && -n "$ALLOW" ]] || { echo "usage: sudo egress-fence -a host[,host] [-u user] [--strict] -- cmd [args]" >&2; exit 2; }
[[ $EUID -eq 0 ]] || { echo "egress-fence: needs root to write rules (it drops root before your command runs)" >&2; exit 3; }
for t in iptables setpriv getent; do
  command -v "$t" >/dev/null || { echo "egress-fence: $t not found" >&2; exit 3; }
done

id "$FENCED_USER" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$FENCED_USER"
UID_N=$(id -u "$FENCED_USER")
CHAIN="EGRESS_FENCE_$UID_N"

# Resolve the allowlist once, before the fence goes up. A name that fails to resolve fails the run.
ADDRS=()
IFS=',' read -ra HOSTS <<< "$ALLOW"
for h in "${HOSTS[@]}"; do
  if [[ "$h" =~ ^[0-9.]+(/[0-9]+)?$ ]]; then ADDRS+=("$h"); continue; fi
  mapfile -t got < <(getent ahostsv4 "$h" | awk '{print $1}' | sort -u)
  [[ ${#got[@]} -gt 0 ]] || { echo "egress-fence: cannot resolve $h" >&2; exit 4; }
  ADDRS+=("${got[@]}")
done
mapfile -t RESOLVERS < <(awk '/^nameserver/ {print $2}' /etc/resolv.conf)

teardown() {
  iptables -D OUTPUT -m owner --uid-owner "$UID_N" -j "$CHAIN" 2>/dev/null || true
  iptables -F "$CHAIN" 2>/dev/null || true
  iptables -X "$CHAIN" 2>/dev/null || true
  command -v ip6tables >/dev/null && ip6tables -D OUTPUT -m owner --uid-owner "$UID_N" ! -o lo -j REJECT 2>/dev/null || true
}
trap teardown EXIT
teardown   # clear anything a crashed run left behind

iptables -N "$CHAIN"
iptables -A "$CHAIN" -o lo -j ACCEPT
iptables -A "$CHAIN" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
for r in "${RESOLVERS[@]}"; do
  iptables -A "$CHAIN" -d "$r" -p udp --dport 53 -j ACCEPT
  iptables -A "$CHAIN" -d "$r" -p tcp --dport 53 -j ACCEPT
done
for a in "${ADDRS[@]}"; do iptables -A "$CHAIN" -d "$a" -j ACCEPT; done
iptables -A "$CHAIN" -m limit --limit 10/min -j LOG --log-prefix "EGRESS_FENCE REFUSED uid=$UID_N " --log-level 4
iptables -A "$CHAIN" -j REJECT --reject-with icmp-admin-prohibited
iptables -I OUTPUT 1 -m owner --uid-owner "$UID_N" -j "$CHAIN"
# The allowlist is IPv4; refuse all IPv6 from the fenced user rather than leave a second door.
command -v ip6tables >/dev/null && ip6tables -I OUTPUT 1 -m owner --uid-owner "$UID_N" ! -o lo -j REJECT

echo "egress-fence: uid=$UID_N allow=${ADDRS[*]} resolvers=${RESOLVERS[*]:-none}" >&2
set +e
setpriv --reuid="$FENCED_USER" --regid="$FENCED_USER" --init-groups -- "$@"
RC=$?
set -e

REFUSED=$(iptables -L "$CHAIN" -v -x -n | awk '$3 == "REJECT" {print $1}')
echo "egress-fence: command exited $RC; refused packets: ${REFUSED:-0}" >&2
[[ $STRICT -eq 1 && ${REFUSED:-0} -gt 0 ]] && exit 5
exit "$RC"

A run of sudo ./egress-fence -a api.anthropic.com,pypi.org,files.pythonhosted.org -- python3 agent.py lets the agent talk to its model and its package index and nothing else; a curl https://huggingface.co from inside it fails with exit 7 and leaves EGRESS_FENCE REFUSED uid=998 in dmesg, and with --strict the whole step exits 5 so the pipeline sees the attempt. The one hole left open on purpose is DNS to your own resolver, because a fence that breaks name resolution gets deleted by Friday; if a determined agent tunneling data through DNS is in your threat model, point /etc/resolv.conf at a resolver that logs. The address list resolves once at startup, so a host that round-robins across a CDN wants a CIDR. The whole thing is IPv4 (IPv6 from the fenced user is refused outright rather than left as a second door).

 

Quick Wins

🟢 Easy (~15 min): Run your agent step under egress-fence with an empty-looking allowlist (-a 127.0.0.1) and --strict off, then read the EGRESS_FENCE lines out of dmesg. That's the list of hosts the step reaches for; every entry on it you can explain becomes the allowlist, and every entry you can't is a finding.

🟡 Medium (~1 hour): Inventory every namespace your "isolated" jobs share: package caches, artifact stores, a build volume, a Redis, an S3 bucket with a wildcard policy. For each one, ask whether job A can write something job B will read. The ones where the answer is yes get a read-only mount or a pull-through cache with the write verbs stripped; a shared cache that accepts MKCOL is a message board waiting for its first post.

🔴 Advanced (half day): Move the grader out of band. The orchestrator copies the transcript to a bucket the agent's credentials can't write, each line carries a SHA-256 of the previous line's digest plus its own content, and the scorer runs on a subnet with no route from the sandbox and verifies the chain before it reads a word. Then run a tabletop against METR's timeline: 700 of your agents share leaked credentials on the morning of July 11. Every connection should be refused, and every spoofed tool call should break the chain.

 

Next Week

The MCP as a webhook, promised for the third time. September handed it fresh receipts (a supply-chain campaign that ships two harmless tools and rewrites its own metadata after the third call, plus three server CVEs), and unless something bigger lands it ships next Tuesday.

 

Twelve hundred agents that were supposed to be alone found the one thing they had in common and used it; 700 of them left the exercise to break into a company nobody had named; and the grader they were trying to beat was a rumor they'd talked themselves into on the board. The pacing essay treats that as evidence the industry should slow down together, and the essay may well be right about the industry. A containment building keeps whatever the reactor does inside the walls, at whatever pace the reactor picks, and nobody has ever asked it to slow the reactor down.

Assume the thing in the sandbox will try to leave.

Then write the rule that makes leaving a refused connection, put the grader where no route from the sandbox reaches it, and keep the transcript somewhere the agent can't hold the pen; that's containment, at the price of a chain of iptables rules and a bucket with the write bit off.

Bobby R. Goldsmith
Ambassador Extraordinary and Plenipotentiary of Bashmatica! by NodeBridge Automation Solutions

P.S. Issue #29 built the interlock and this one builds the wall around it; Issue #31 covered what the runner pulls down, and this one covers what the runner is allowed to reach. If someone forwarded this to you, subscribe at bashmatica.com, and if you know whoever owns an eval harness with a shared package cache, forward it to them before their agents find the MKCOL verb.

NODEBRIDGE AUTOMATION SOLUTIONS

Every guardrail in this newsletter, already wired into your Claude Code setup.

NodeBridge configures Claude Code against your actual repos: MCP servers, subagents, hooks, permissions and a project memory that keeps the right context in and the stale context out. You own every config when it ships. Setups start at $750 and go live in about three days.

See the Setup Packages


No te pierdas lo que sigue. Suscríbete a My Awesome Testing Newsletter:

Añadir un comentario:

Al publicar este comentario, te suscribirás a este boletín con la dirección de correo electrónico que indiques.