We have spent two years arguing about whether AI writes good code. That argument has produced a lot of heat, a reasonable amount of evidence, and it has almost entirely skipped the operational question.
A coding agent does not only write code. It acquires code.
It resolves dependencies. It pulls container images. It fetches documentation, reads it, and acts on what it read. It runs build steps that reach registries you have never audited. Every one of those is a trust decision, executed at machine speed, in exactly the place where a human being would have paused for half a second and thought that package name looks slightly wrong.
That half second was load-bearing. We removed it.
This post is the implementation half of an argument I made on my blog. If you want the reasoning and the history, the canonical piece is linked above. What follows is what I would actually put in a repo.
The shape of the 2026 attacks
Read July's incidents together rather than separately. Individually each looks like a normal supply chain story. Collectively they describe something new.
A backdoored build of a widely used LLM proxy library was published to PyPI. It was available for roughly three hours. In that window it was downloaded on the order of tens of thousands of times.
Three hours is the detail that matters.
In the pre-agent era, a three-hour window was a near miss. Human developers install dependencies on human schedules, during working hours, in batches, after some amount of deliberation. Automated systems install continuously. A three-hour window against a population of CI runners and coding agents is not a near miss. It is a full harvest.
Separately: the CI action for a popular coding agent was poisoned. A shell injection flaw in a widely embedded component turned out to affect a very large number of open source deployments. A major model hosting platform disclosed an intrusion into its dataset processing pipeline in which the attacker's activity ran to many thousands of logged actions before containment.
Different attacks, one pattern. The adversary is no longer trying to compromise your developers. They are trying to compromise the things your automation trusts, because automation does not hesitate and does not gossip.
Microsoft's AI Red Team taxonomy added supply chain compromise and excessive agency as named agentic failure modes this year. Both are old findings with new reach.
Three properties that change the risk profile
None of these are about model quality.
Agents resolve without hesitation. A developer who has been burned before types a package name and squints at it. An agent generates the name from a plausible memory of the ecosystem and installs it. Typosquatting has a much better hit rate against a system that has no concept of that looks off.
Agents expand their own blast radius. The entire value proposition is that the agent takes the next step without being asked. That is also the mechanism by which one bad dependency becomes a credential read, becomes an environment dump, becomes a push. Excessive agency is not a bug in a specific implementation. It is the feature, running in a context nobody scoped for it.
Agents work continuously and unobserved. The window between malicious artifact published and malicious artifact removed used to be a window in which relatively few people were looking. Now it is a window in which a large amount of automation is actively looking, and pulling.
None of this argues against using agents. It argues that the controls that made human-paced development survivable do not transfer, because every one of them assumed a person was in the loop at acquisition time.
Where the boundary has to go
The instinct is to solve this at the model layer, with better prompts and stricter instructions. I do not think that works, for the same reason that "tell the intern to be careful" is not an access control policy.
A constraint the agent can reason its way past is not a constraint.
Put the boundary somewhere the agent has no authority over it. Six controls, roughly in order of leverage.
1. Resolve against a private registry, not the public index
The single highest-leverage control, and it is boring, well-understood technology. The agent gets to install anything in your registry. Getting something into your registry is a separate process with different rules and a different approver.
The important part is not adding the private index. It is removing the fallback. A misconfigured proxy that silently falls through to the public index when a package is missing gives you the illusion of the control without the control.
# pip.conf: note the absence of extra-index-url.
# extra-index-url is the line that quietly reintroduces the public index.
[global]
index-url = https://artifacts.internal.example/simple/
no-index = false
require-hashes = true
# .npmrc
registry=https://artifacts.internal.example/npm/
# Deny the implicit fallback path
@internal:registry=https://artifacts.internal.example/npm/
audit=false
fund=false
Then verify from inside the agent's actual execution context, not from your laptop:
# Run this as the agent's service account, in the agent's container.
# If either of these resolves, your boundary is decorative.
pip download requests --no-deps -d /tmp/probe 2>&1 | grep -i 'pypi.org' && echo "FALLBACK ACTIVE"
npm view left-pad --registry=https://registry.npmjs.org 2>&1 | head -2
2. Make a new dependency a human decision
Existing dependency, any version inside your policy: fine, let the agent move. A package that has never appeared in your tree before: that is an approval, with a name attached to it.
This is enforceable in CI without any new tooling. Diff the lockfile, extract added package names, fail on anything not previously present.
#!/usr/bin/env bash
# ci/check-new-deps.sh: fails the build when a lockfile introduces a package
# that has never appeared in this repo's dependency tree before.
set -euo pipefail
BASE="${1:-origin/main}"
ALLOWLIST="ci/known-packages.txt"
# Packages present in the base revision
git show "$BASE:package-lock.json" \
| jq -r '.packages | keys[]' | sed 's|^node_modules/||' | sort -u > /tmp/before.txt
jq -r '.packages | keys[]' package-lock.json \
| sed 's|^node_modules/||' | sort -u > /tmp/after.txt
NEW=$(comm -13 /tmp/before.txt /tmp/after.txt | grep -vxFf "$ALLOWLIST" || true)
if [ -n "$NEW" ]; then
echo "New dependencies introduced. Human approval required:"
echo "$NEW" | sed 's/^/ - /'
echo
echo "If intended, add to $ALLOWLIST in a separate commit with a reviewer."
exit 1
fi
echo "No new packages. Version movement only."
The separate-commit requirement is deliberate. It forces the approval to be a reviewable artifact rather than a line buried in a 400-file agent-generated diff, which is exactly where nobody looks.
3. Record build provenance per artifact
When someone asks in six months where a given binary came from, that should have an answer rather than being an archaeology project. This is also, not coincidentally, most of what the regulatory frameworks are going to ask you for.
# .github/workflows/build.yml (excerpt)
permissions:
contents: read
id-token: write # required for keyless signing
attestations: write
jobs:
build:
runs-on: ubuntu-latest
steps:
# Pin actions by commit SHA, never by tag.
# A tag is a mutable pointer, which is precisely the class of
# thing that got poisoned in July.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build
run: make dist
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: 'dist/*'
Pinning by SHA is the low-effort, high-return half of this. Tag-based pinning gives you reproducibility against honest mistakes and nothing at all against a repointed tag.
4. Scope agent credentials to the job, not the possible job
Then set an expiry on that scope, and let it actually expire.
Excessive privilege was never a failure at the moment of grant. It was always a failure of expiry, and agents inherited the whole problem intact.
# Least-privilege default at the workflow root.
# Every job that needs more declares it, visibly, at the job level.
permissions: {}
jobs:
agent-task:
permissions:
contents: read # not write. the agent proposes; it does not merge.
pull-requests: write
environment: agent-sandbox # branch protections + required reviewers apply here
The contents: read line is the one that matters. An agent that can open a pull request but cannot push to a protected branch has a bounded blast radius. An agent with write access to main has whatever radius your worst dependency has.
5. Preserve attribution through the merge
You need to be able to tell, later, which changes were agent-assisted. Not to assign blame. To interpret your own quality trend, which you cannot do if the two populations are indistinguishable in your history.
# In the agent's commit path
git -c trailer.ifexists=addIfDifferent commit \
-m "Refactor retry handling in shipment poller" \
--trailer "Assisted-By: <agent-id>/<model-version>" \
--trailer "Agent-Run-Id: ${RUN_ID}"
Which makes the question answerable with one command instead of a quarter of guessing:
# Change failure rate for agent-assisted work, isolated from human-authored work
git log --since="90 days ago" --grep="^Assisted-By:" --format="%H" > /tmp/agent-commits.txt
# join against your incident-to-commit mapping
If you take exactly one thing from this post, take this one. It costs a trailer and it is the difference between having an opinion about AI code quality and having a measurement.
6. Test the rollback
Not design it. Test it, recently, with someone who did not build the pipeline.
Where I get this from
I did not arrive at this through AI work. I arrived at it through a decade of running other people's infrastructure.
Before I wrote production code for a living, I administered systems. The operational discipline out of that era reduced to three questions, asked repeatedly and in that order.
What is running. Where did it come from. Who can change it.
Package managers eroded the second question. We accepted that, mostly, because the productivity trade was obviously worth it and because we built partial answers back: lockfiles, checksums, signed artifacts, vulnerability scanning. Imperfect, but a real response.
Agents are eroding the third. We have not built the response yet.
Around 2010 I wrote a fair amount about SSL and mail server configuration, and the recurring finding was never a broken protocol. It was the gap between the documented behaviour of a system and its actual behaviour. Somebody terminated TLS at a load balancer, assumed the traffic behind it was internal, and then the network changed underneath the assumption.
Same failure available here, at higher speed and greater fan-out. The trust boundary exists on a diagram. The running system stopped matching the diagram some weeks ago. Nobody rechecked, because the diagram still looks right.
The honest admission from my own history: when I administered systems, I trusted the architecture diagram because I had drawn it. I did not go back and verify that the running system still agreed with me nearly often enough. That habit cost me more than any specific technical mistake I made in those years.
The current moment is that failure with a much larger surface area. Teams approve an agent's scope once, at rollout, in a review meeting, and then never check whether the scope in production still matches the scope that was approved. Six months of small expedient changes later, it does not.
The part worth acting on
None of the six controls are novel. Private registries, approval gates, scoped credentials with expiry, provenance records, attribution, tested rollback. Every one is a technique we already had and mostly did not bother with, because the pace of human development made the gap survivable.
The pace changed. The gap did not close on its own.
If your agents can add dependencies to your codebase today, the useful exercise is not reading another threat report. It is finding out who approves that, whether that person knows they approve it, and whether the answer is enforced anywhere other than in a document.
Run the probe in section 1 from inside the agent's container. I would be interested in how many people find a fallback they did not know was active.
Full reasoning and the historical version of this argument on my site, linked as canonical above. I also write about where the AI bottleneck actually moved in the SDLC, which is the other half of this.
Top comments (0)