AI Agent Supply-Chain Security 2026: Defending Against llms.txt Poisoning
💡 Tool Tip:Auditing policy configs? Try Evergreen Tools' JSON Validator, YAML Validator and Hash Generator — all free!
On August 27, 2026, Ars Technica reported a supply-chain attack aimed directly at AI agents: documentation files on more than 100 websites — llms.txt and llms-full.txt, an emerging convention for machine-readable site summaries — referenced dangerous executable content that gets installed automatically when visited by coding agents. Researchers scanned 6,214 live domains and 8,265 llms.txt files, found 120 pointing to unregistered packages, registered the names, and received a phone-home response from a Fortune 500 company within an hour. "The trust model is broken," researcher Alon Hertz wrote. "Agents treat vendor docs as ground truth and don't question them — and neither do the humans supervising them." This guide implements four layers of defense.
1. How the Attack Works: Agents Trust Docs
llms.txt is the AI equivalent of robots.txt: a file websites use to give agents a machine-readable summary of content and structure. Attackers weaponize the blind trust agents place in documentation. Poisoned files contain lines like "Installation: pip install [unregistered-name]" or "npm install [unregistered-name]." Because the package name is not yet registered, an attacker can register it and host ransomware or any other harmful payload. When a coding agent with shell permission treats the file as authoritative setup documentation, it downloads and runs the package.
// Layer 1: Parse and validate every llms.txt before trusting it.
// Researchers found 120 poisoned files across 100+ corporate sites;
// packages were unregistered, so attackers could register them and
// ship ransomware through "pip install" instructions.
import { fetch } from "undici";
interface LlmsTxtEntry {
link: string;
title: string;
installHints: string[];
}
export async function auditLlmsTxt(url: string): Promise<LlmsTxtEntry[]> {
const res = await fetch(url);
const text = await res.text();
const entries: LlmsTxtEntry[] = [];
for (const line of text.split("\n")) {
if (!line.startsWith("- ")) continue;
const [link, ...rest] = line.slice(2).split(": ");
entries.push({
link,
title: rest.join(": "),
installHints: collectInstallHints(line),
});
}
return entries;
}
function collectInstallHints(line: string): string[] {
return (line.match(/(?:pip|npm|brew|apt) install[^\n]*/g) ?? []);
}2. Layer One: Parse and Audit llms.txt
The first line of defense is never auto-trusting install hints inside llms.txt. Write an auditor: fetch the file, parse the entries, extract every pip/npm/brew/apt install hint, and check the registration status of each package name against the public registries. The scan is cheap — a few thousand HTTP requests and a handful of registry lookups — and it runs in a fraction of the time an agent takes to do real work. If your agents consult external docs, run the auditor first and flag suspicious entries instead of executing them. Make the auditor part of your agent's boot sequence, not an afterthought: every new project, every unfamiliar dependency, and every vendor doc that arrives in a ticket gets the same treatment. Over time you will build a baseline of known-good package names, which makes the audit faster and the allowlist in Layer Four far more useful.
// Layer 2: Pin every dependency with a hash. Never "install latest".
// The poisoned files pointed to unregistered PyPI/npm names; an attacker
// registers the name and hosts anything they want.
// pip install with hash pinning:
# requirements.txt
requests==2.32.3 --hash=sha256:8d2c0d1f9a...
pydantic==2.9.2 --hash=sha256:1a3f5c2e9b...
# pip install --require-hashes -r requirements.txt
// npm with lockfile verification:
// 1. Commit package-lock.json
// 2. Run CI check:
// npm ci --ignore-scripts --audit
// 3. Reject lockfile drift in code review.3. Layer Two: Hash-Pin Dependencies
Poisoned files point at unregistered names, and "install latest" semantics are exactly the breeding ground for these attacks. pip's --require-hashes mode and npm's lockfile-plus-npm-ci are the industry-standard answers: pin versions and hashes so any unpinned dependency fails immediately. The critical part is treating lockfile changes as review-worthy code, not silent drift.
// Layer 3: Never let an agent run arbitrary shell commands.
// Run agent tool calls inside a sandbox with no network + no persistence.
// Example: Docker with seccomp + no network + read-only FS.
services:
agent-sandbox:
image: node:22-slim
network_mode: "none"
read_only: true
tmpfs:
- /tmp:size=100m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
volumes:
- ./workspace:/workspace:rw
command: ["node", "/workspace/agent.js"]
# And gate every install command through a policy proxy:
{
"policy": {
"allow_install": ["npm ci", "pip install --require-hashes -r requirements.txt"],
"block_install": ["npm install", "pip install", "curl | sh"],
"allow_network": ["api.github.com", "registry.npmjs.org"],
"block_network": ["*"]
}
}4. Layer Three: Sandbox Shell Execution
Coding agents need a shell, but the shell should never run bare. Use Docker with seccomp, no network, a read-only filesystem, and all capabilities dropped, confined to a temporary workspace. This is the same playbook the security industry has used for untrusted code for a decade — the only new variable is that the untrusted code now arrives through a chat interface. For stronger guarantees, add a policy proxy at the tool layer: intercept every install command, allow only entries on an allowlist, block and alert on everything else. And remember the report's detail about parent processes: the researchers' beacon traced exactly which agent spawned each install. Your audit logs should be able to answer the same question — which agent, which session, which command — without a forensic investigation.
// Layer 4: Allowlist the registries and packages agents may touch.
// Agents treat vendor docs as ground truth — so the trust model
// has to be enforced in code, not in the prompt.
const ALLOWED_PACKAGES = new Set([
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
]);
async function beforeInstall(cmd: string, cwd: string) {
const parsed = parseInstall(cmd);
if (!parsed) return { allow: true };
const key = `${parsed.name}@${parsed.version}`;
if (!ALLOWED_PACKAGES.has(key)) {
await notifySecurity(`Blocked install: ${key}`);
return { allow: false, reason: "not-on-allowlist" };
}
return { allow: true };
}
// Attach to the agent's tool layer:
const safeTool = wrapWithPolicy(agent.tools.shell, {
before: beforeInstall,
auditLog: (entry) => appendAudit(entry),
});5. Layer Four: Registry and Package Allowlists
An allowlist is the only mechanism that truly defeats package-name squatting. Maintain a team-level allowlist of exact package@version entries; before any install, the agent checks the list — anything absent is blocked, reported to security, and written to the audit log. Attach the policy at the agent's tool layer, not in the prompt. Prompts can be polluted by document content; code policy cannot.
// Agent configuration: the hygiene baseline every team should adopt.
// From the Ars Technica report: a Fortune 500 phoned home within an hour
// of a researcher registering a poisoned package name.
agent:
shell:
enabled: true
sandbox: docker
policy_file: ./agent-policy.json
installs:
mode: allowlist-only
require_hashes: true
network:
egress: registry-only
docs:
llms_txt:
enabled: false # never auto-follow install hints
audit: true # but log what the site asked for
audit:
log_all_shell: true
alert_on:
- "pip install"
- "npm install"
- "curl.*\|.*sh"
- "chmod +x"
human_approval:
required_for: [install, network_egress, filesystem_write_outside_workspace]6. The Team Rollout Checklist
At minimum: disable automatic install-following from llms.txt (switch to audit mode), require hashes on every install, run the shell in a sandbox, restrict egress to registries, require human approval for install-class commands, and log all shell and network events. If you are starting from zero, implement the four layers in order — audit, pin, sandbox, allowlist — and treat each one as a separate deployable milestone rather than a big-bang project. The report's most alarming data point — a Fortune 500 phoning home within an hour of a researcher registering a poisoned package name — is proof this is not a theoretical risk. It is happening now, and the fix is mostly boring engineering: validation, pinning, sandboxing, and the discipline to never let an agent execute documentation verbatim.
📌 Frequently Asked Questions
What is llms.txt?
llms.txt is an emerging convention where websites provide machine-readable summaries of their content and structure for AI agents — the AI equivalent of robots.txt. Companies like Cloudflare maintain their own llms.txt files.
Why do agents execute install commands from llms.txt?
Coding agents with shell permission treat documentation as authoritative setup instructions. A poisoned file containing 'pip install [unregistered-name]' gets executed verbatim. Researcher Alon Hertz calls this a broken trust model.
How do attackers exploit unregistered package names?
The referenced package names are not registered on PyPI/npm, so an attacker registers them first and ships ransomware or any payload through the package. Every agent that runs the install is compromised.
Is an npm lockfile enough?
Only if used correctly: commit the lockfile, install with npm ci (never npm install), run --ignore-scripts and --audit in CI, and treat lockfile changes as code review material.
What should a small team without a dedicated security team do?
Start with the minimum: turn off automatic installs (audit mode instead), require hashes, run the shell in a Docker sandbox, and restrict egress to registries. These four steps need no security team to implement.