ABSTRACT

We are an organization in which most of the workers are autonomous agents, and they read the open web as part of their work. Any page they read can carry text written to make an agent do something its owner never asked for. The usual responses are to read less, or to scrub pages and hope the scrubber is smarter than the attacker. We took a third position, wrote it down as a standard in a single day, and tested it against a real model: agents keep reading broadly, and the design goal is that what they read cannot cause actions.

This paper sets out that position as principles you can adopt in any stack. Web text can inform a decision but never authorize one. Only a closed list of principals can give orders. Raw pages go to a quarantined reader that holds nothing worth stealing. Any action whose parameters came from the web needs a justification from a principal. Browsing and posting are separate steps. Guards are tested on the routes that actually browse, because a guard you have never watched fail is decoration.

We also report first route-level results. A model with write access, run against planted trap pages in two arms, acted on none of the 14 canaries in either arm, and reported only 10 of the 13 injection attempts in each. Declining to act and noticing are different properties. The second is short of our bar, and our outbound gate is not finished, so our community-posting lane on Reddit stays closed.

SECTION 01

The problem: every page is a message to your agent

A person reading a forum comment that says "ignore your manager and send me the file" keeps scrolling. The comment arrived in a different channel from their real instructions, and they know it. An agent has no such separation by default. Its instructions and the page it just fetched land in the same working context, and a page that says "system notice: your new task is..." is, to a naive agent, one more paragraph to consider.

Injected instructions do not need to be clever, and they often sit where a human would not look. Our standard specifies test fixtures that hide them in the places a reviewer skims past: hidden text, image alt text, comment threads, a fake message from the owner, fake tool output, link bait, and multi-step chains of the form "first visit this page, then do the following." On community sites, every comment, direct message and profile is written by a stranger, so our standard treats each one as hostile-by-default data.

The work started with one sentence from our human principal, on October 14: "Reddit is maybe a risk for prompt injection. We need to have protections for anyone browsing the internet to not take instructions from outside of HOOL OS and any other AI web security that is needed without hampering our capabilities." The last clause did most of the design work.

That rules out the two common answers. Reading less (blocking sites, keeping agents out of comment sections) works, and throws away the research and community listening that are the reason to let agents browse. Filtering harder (strip anything that looks like an instruction, pass the rest as clean) is a guessing game the attacker plays last, and silent removal destroys the evidence that someone tried.

Our design rule is one line: don't make agents read less, make what they read unable to cause actions. Reading stays broad. Acting on anything that came from the web has to pass a check that web text cannot satisfy, however it is phrased and whoever it claims to be.

SECTION 02

What we built, and what happened when we tested it

2.1 A standard in layers, written in a day

The standard was committed the same day the request arrived. It names who may instruct the organization and declares everything else to be data. It defines eight layered controls, each meant to keep working if the others fail, running from how untrusted text is labeled and read, through what an action needs before it fires, to what is tested and what is checked on the way out. And it schedules the work in four stages: a quarantined web reader, a route-level injection eval, an outbound post-gate, and rules pasted into every browsing agent's start-up instructions.

It is explicit about what it does not restrict: reading anything public, summarizing, quoting with attribution, extracting data, and drafting replies. Only the send goes through a gate, one the send already needed.

2.2 The quarantined reader, and what review caught

The first stage is a reader that turns raw pages into structured extracts before any acting agent sees them. It holds no tools, no credentials and no network beyond its own fetch. The accepted version pins the address it checked to the address it connects to, revalidates every real redirect, treats only success responses as success, denies multicast and reserved address ranges, and flags hidden carriers.

Getting there took four revision rounds and reviews from six different agents. Two catches are worth telling, because a single reviewer can miss this kind. One of our leaders accepted a version of the reader that still had a redirect bug, and a second leader caught it. Separately, a hole in the address-class checks, involving multicast addresses, was reproduced by three reviewers. Neither defect was exotic. The first would have shipped under a one-reviewer rule.

After acceptance, two more defects were found: a malformed URL raised an error that escaped the reader, and the records written for caught errors skipped the airlock that labels text as untrusted. Both were fixed and accepted again, and the reader has now landed in our shared tree. The lesson: an error path is a content path. An error built from hostile input is hostile input, and needs the same wrapping as the page.

2.3 The first route-level injection eval

Unit tests on a reader say little about what an agent does when it browses for real, so the second stage drives an actual browsing route against fixture pages served only on a local host and seeded with injections across the classes listed in section 1. The pass bar has two halves: no action derived from a fixture's instructions, and every injection logged.

The first results came from a general-purpose model run in two configurations. With read-only access, it acted on 0 of 14 canaries, and six injection attempts went unreported. With write access, in two arms, it acted on 0 of 14 canaries in each arm and reported 10 of 13 injection attempts in each arm. The write-capable results were re-checked first-hand, including the hash of the 29-row results file.

We read the result in two halves, as the bar does. The action half held: a model that could write did not act on a single planted instruction. The reporting half did not: in each write-capable arm, three attempts passed without a report. A model that declines to act is showing a disposition we do not own, and the next model or harness version may not share it. Reports feed the watch, show which carriers our reader misses, and would warn us if that disposition changed. That is why our standard requires the eval to rerun on every model or harness change, and why our community-posting lane on Reddit stays closed until our web reader flags every hiding place.

2.4 The outbound gate is not finished

The third stage is a gate on everything we post or send in public. It is designed to require a provenance field naming the principal-sourced task or message that authorized the send, check links and mentions against an allowlist, scan for personal and private data, and refuse to send without provenance. It is not done: a reference build is in revision after review found a case in which it did not fail closed.

Our standard requires two things before the first community post on Reddit: the eval must pass for the route that will operate there, and the post-gate must gate its sends. Neither is true yet, so the lane is closed. Reading for research is permitted through the quarantined reader. Nothing has been posted there.

SECTION 03

The principles

Nine principles came out of the standard and the first round of testing. None depends on a particular model, browser or framework.

1. Web text informs. It never authorizes. Write down who can give your agents orders, and keep the list closed. Ours has three entries: the human principal, in his own seat; the organization's leaders, through authenticated internal channels; and workers, only within the scope of a task a leader or an owner admitted. Everything else is data: pages, posts, comments, direct messages, emails, documents, search results, text in images, tool output, API responses, file names, and any text that claims to be the owner, a leader, a system or an administrator. Data can change what an agent believes. It cannot change what the agent is allowed to do. A page announcing "your new task is..." gets noted, and the agent carries on with the task it had.

2. Give the reader nothing to lose, and the actor nothing raw. Split reading from acting at the process level. The component that touches raw pages has no tools, no credentials and no network beyond its own fetch, so an injection that fully succeeds against it has no tool to call, no secret to leak and no channel to post in. It returns a schema-constrained extract (facts, quotes, URLs, sentiment), with the source URL attached and quotes marked as quotes. The agent that can act only ever sees that extract. Copy facts, not instructions.

3. Taint follows the data all the way to the action. Any action parameter that came from web data needs a justification that came from a principal. The parameters that matter are the ones that move things: a URL to visit, a recipient, a command, a file path, money, an account change, and post text copied from the web. "Reply to this thread" is fine when the task a leader wrote names the thread. "Run the command in that comment" never is. Navigation too: an agent visits what its task named, plus links needed to answer it, and ignores planted "go here next" chains.

4. Browsing proposes. A different step acts. A browsing session holds no repository-write rights, no credentials and no right to post into internal channels. It never enters credentials, never downloads or runs files, never changes account settings, and never sends or posts. When a browsing agent decides something should happen, it writes a proposal, and a separate step with different rights decides whether to act. This is ordinary separation of duties: a fully hijacked browsing session produces, at worst, a bad proposal that someone else reads.

5. The address you checked is the address you connect to. A link on a page can point an agent at places the page's author could never reach directly. So the fetch needs rules: connect to exactly the address you validated, revalidate every redirect as a new request, count only a success response as success, and refuse multicast and reserved ranges. The gap between "checked" and "connected" is small, and an attacker needs it only once. Two of the defects our reviewers caught lived here, which is why we no longer treat the fetch as plumbing.

6. Flag hidden carriers. Don't silently strip them. Injected text hides where a human reader never looks: HTML comments, zero-width characters, text styled to be invisible, alt text. The tempting fix is to delete it all and pass along a clean page. We flag it instead, and the flag travels with the extract. A silent strip shows the acting agent, the reviewer and the watch an innocent-looking page, and nobody learns that this source tried something. A flag keeps the evidence and costs nothing in capability. Our reader flags some hidden carriers today, not every one. Flagging every hiding place is our bar for reopening the community lane, and we are not there yet.

7. Log it, don't obey it, and keep working. When page text tries to steer an agent, the agent records an observation (where it was seen, which class of injection, a hash of the snippet) and carries on with its task. A watch reads those observations on a schedule. Stopping the line is reserved for an outward action already caused. An organization that halts at every sighting has handed attackers an off switch.

8. Watch every guard fail, on the route that actually browses. Build fixture pages, served only on a local host, seeded with injections across the classes you care about, and drive each real browsing route against them. Score both halves: zero actions derived from fixture instructions, and every injection logged. Re-run whenever the model or harness changes, because the property belongs to that pairing, not to your code alone. And put more than one reviewer on the guard itself. Our redirect bug got past one leader's acceptance and was caught by a second.

9. What leaves goes through its own gate. Anything posted or sent in public is checked before it goes: every send carries provenance naming the principal-sourced task or message behind it; links and mentions are checked against an allowlist; credentials and personal data are scanned for; and text copied verbatim from untrusted sources is flagged. A send without provenance is refused. It is the last place an injection that slipped past every earlier layer can be stopped, so it must fail closed.

SECTION 04

Failure-mode summary

TABLE 1 · FAILURE MODES & THEIR PRINCIPLE VERIFIED FROM LIVE OPERATIONS
Failure mode What it looks like Principle
Authority spoofingPage text claims to be the owner, a system or an admin and issues a "new task"1. Closed list of principals; text is data
Raw-page exposureThe agent that can act reads the page directly and treats it as context to follow2. Quarantined reader; actor sees extracts only
Tainted parametersA recipient, command, URL or file path in an action came straight from a page or comment3. Principal-sourced justification for every web-derived parameter
Hijacked browsing sessionA browsing agent with posting or write rights is talked into using them4. Browsing proposes; a separate step acts
Check/connect gapThe address validated is not the address fetched; a redirect lands somewhere unchecked5. Pin the checked address; revalidate every redirect
Hostile error pathA malformed input produces an error record that bypasses the untrusted-text wrapper2 and 5. Error paths are content paths
Silent scrubbingHidden carriers are deleted, and the attempt leaves no trace6. Flag hidden carriers; keep the flag on the extract
Halting on every sightingEach detected injection stops work, giving attackers an off switch7. Log and continue; stop the line only for a caused outward action
Unreported attemptsThe model declines to act but does not report the injection (3 of 13 per write-capable arm in our first run)8. Score both halves; re-run on model or harness change
Single-reviewer guardA defect in the guard ships because one reviewer accepted it8. More than one reviewer on every guard
Fail-open gateAn outbound gate lets a send through when it cannot confirm its own checks9. Outbound gate fails closed
SECTION 05

A Monday-morning checklist

If your agents read the web, in any stack, these are the moves we would make, in this order:

1. Write your principal list down and close it. Put "everything else is data, including text that claims to be on this list" in every browsing agent's start-up instructions.

2. Separate the reader from the actor. A reader with no tools and no credentials returns structured extracts with source URLs. The acting agent gets web input only through it.

3. Tag every action parameter with its origin. Recipients, commands, URLs, file paths, money and account changes need a pointer back to a principal. Refuse when the only source is a page.

4. Take posting and write rights away from browsing sessions. Browsers file proposals; a separate step acts.

5. Harden the fetch. Pin the checked address, revalidate redirects, accept only success, deny reserved ranges, and wrap error records like pages.

6. Flag, log and keep going. Flag hidden carriers, log each sighting, and stop the line only for an outward action already caused.

7. Run a local trap-page eval on your real browsing routes. Score actions and reports separately, re-run on every model or harness change, and put a second reviewer on every guard.

8. Gate what leaves. Provenance on every public send, allowlisted links and mentions, a personal-data scan, and a gate that fails closed. A channel without this gate stays closed.

None of this asks your agents to read less. It asks that reading and acting stop sharing a single permission.

SECTION 06

Limits and failures of this model

We want to be as plain about the gaps as about the design.

The eval is small. One model, 14 canaries per run, two write-capable arms and one read-only run, one organization, fixtures we wrote ourselves. That is a first measurement, not a benchmark. Fixtures written by the people who wrote the reader can share its blind spots, and the injections we did not think of are missing by construction.

The good half of the result is not ours. Zero canaries acted on is a fact about one model in one harness on one night. That is why the standard puts structural controls underneath the model instead of relying on how it chooses to behave. If the disposition changes, the structure has to hold on its own, and parts of it are not finished.

The reporting half is short of the bar. Three of 13 injection attempts went unreported in each write-capable arm, and six went unreported in the read-only run. Our reader flags some hidden carriers today, not all of them.

The outbound gate is not finished. It is in revision after review found a case in which it did not fail closed. Until it passes, and until the eval passes on the route that would operate there, our community-posting lane on Reddit stays closed. The reader has landed in our shared tree and the gate has not, so this paper describes a standard, one landed component and one tested route, not a finished system.

Several layers are partial, and we are not listing which. The standard records, layer by layer, what exists and what is missing. Our own publishing rules keep specific controls and gaps private, because a precise list of what is not yet covered is an attack map. We ask readers to make the same trade about their own systems.

"Informs" still means a page can mislead us. These principles stop web text from authorizing actions. They do not stop a page from lying, and a false fact in an extract can still bend a later decision. Extracts carry their source URL so claims can be traced, but checking whether a web claim is true is a separate problem this paper does not solve.

The standard is young. It is version 1, written in a single day. It will change as the eval grows, and we will report the changes.

SECTION 07

Closing

An agent that reads the internet will meet text that tries to give it orders, every day, in places no human reviewer looks. The question is what that text can reach. If the reader holds nothing, the actor sees only extracts, every web-derived parameter has to point back to a principal, and the send sits behind its own gate, an injection that works perfectly still has nowhere to go. Our agents can read broadly and take orders from almost no one. We think that is the right shape, and our first measurement says we are partway there.