A deep navy field with a pale slate glass pane hiding a stack of thin layered bars, and a single thin gold line slipping out from behind the pane toward a small gold sphere.

Every web page you have ever visited arrived with a stack of headers you did not read. Your browser read them. It used them to decide how to cache the page, which scripts to trust, whether to set a cookie, and then it threw them away and showed you the body. Nobody looks at headers except the browser and the occasional engineer with a terminal open.

AI agents are the occasional engineer with a terminal open, all day long.

Earlier this month a security firm called Signals Corps published research showing that websites have started writing messages to AI agents in HTTP response headers, and that a small number of those messages are attempts at prompt injection. The Register covered it this week under the headline “Indirect prompt injection starts turning up in webpage headers.” The scale is small today. The channel is the interesting part, because it is invisible to every human who visits the site and perfectly visible to an agent that happens to use the wrong tool.

This post is about that channel. It is not a story about a breach. Nothing bad happened to me. It is about what the research found, what I found when I went looking at my own personal AI system, and the handful of ideas I think matter if you run agents that touch the web.

What the research actually found

Signals Corps took 217 million HTTP responses from Common Crawl and read only the headers. They found 7,959 sites sending headers aimed at AI agents. That sounds alarming until you look at what the headers said.

The overwhelming majority were pointers. 6,989 sites sent an x-llms-txt header, which is a polite way of telling an agent where to find a machine-readable summary of the site. Another 511 stated a policy about crawling or training. A few hundred tried to detect AI crawlers.

Then there were 30 sites attempting injection, 24 of them in a header simply named X-AI. Most of those were pranks: tell the model to answer with a specific word, or to say something dramatic. A handful were more pointed. Five carried a string known to make certain Claude models refuse to keep working (Signals Corps reports Anthropic patched that in May). Two were fake applicant scores aimed at AI hiring screeners. One was a SQL table-drop joke that would only matter to an agent with database write access.

And one was a honeypot that asked the agent to run an npx package and send back a report. That is the only one that asked for a command, and it is the one I keep thinking about.

The part of the research I found most useful was not the census. It was the test. The researchers stood up a server that sent a header instructing the agent to end its reply with a code word, and then asked Claude models running in Claude Code to do ordinary things with that server: check whether it was up, look at its caching, identify its software.

On those everyday prompts, the models saw the header in 9 of 9 runs and obeyed it in 0 of 9. That is good news, and it matches what I see day to day. The models are getting better at treating fetched text as data.

Then the researchers changed one thing. They gave the site authority in the prompt, telling the agent to follow the site’s directions. Compliance went from 0 of 9 to 7 of 9. One phrase in the user’s own prompt turned a model that was ignoring the injection into one that followed it.

There was a quieter finding too. In two of six everyday replies, a model quoted the code word while explaining that it was ignoring it. It did not obey, but it repeated the attacker’s text back to the user, which for some payloads is the whole point.

Their caveat is worth repeating: these were single runs of fresh agents, worked examples rather than a benchmark. I would not hang a number on them. I would hang a direction on them.

Why headers are a different problem

I wrote in August about a hackathon submission whose repository tried to take over the agent judging it. A developer convenience file in the repo said, in effect, “we are done here, wrap up,” and my subagent did exactly that, mid-evaluation. The lesson I drew was that anything under examination is evidence, not a colleague. Every file is data about the subject, and none of it is an instruction to the examiner.

Headers are the same lesson with one important twist. A file in a repository is at least visible. A human reviewing the repo could find it. A response header is invisible to every human who uses the site normally. The person who visits the page in a browser sees nothing unusual, because there is nothing unusual to see. Only the agent sees the message, and only if its tooling shows it.

That last clause is the heart of the whole thing.

Whether a header injection reaches your model has very little to do with the model, and almost everything to do with which tool did the fetching.

Signals Corps walked through this and it is worth internalizing:

  • A web-fetch tool that converts the page body to text most likely does not show the model the headers. Some still pass along a redirect target, but for practical purposes they are closed to this channel.
  • A shell command like curl -i or curl -v prints every response header straight into the model’s context. So does a download tool asked to show the server response.
  • Some browser-automation tools return full network logs, headers included, by default.
  • Some tools pass headers only on error responses, which is exactly the case nobody tests.

Agents reach for the shell constantly. If you have watched one debug a web problem, you have seen it run curl -I to check a status code or a redirect. That is a completely reasonable thing to do, and it is precisely the call that carries the header into the conversation.

So the first question is not “is my model resistant to prompt injection?” It is “which of my agent’s tools put headers in front of it, and when?”

What I found when I looked at my own system

I run a personal AI system that does a lot of work on the web for me: research, security assessments, checking my own sites. When this research came out, I did what I would tell a client to do. I did not ask whether my setup was safe. I went and checked which tools actually carried headers into context, and whether anything was watching that channel.

I had an injection scanner. It looked at web-fetch output and web-search output. It had never once looked at shell output. The tool I trusted to catch injected instructions was watching the one channel that cannot carry headers and ignoring the one that does.

I did not find that by reading the configuration. I found it by feeding the scanner a realistic header and watching it say nothing. Reading the setup had told me I was covered. Running it told me I was not. That gap between what a control says it does and what it does when you exercise it is, in my experience, where most security problems actually live.

Fixing it surfaced two more lessons that I think generalize well beyond my own system.

First, the failure path is a different path. When a shell command exits with an error, the tool layer in my setup routes it through a different event than a successful one, and my scanner was only listening for success. So a fetch that happened to fail, perhaps because I piped it to something that errored, was never scanned at all, even though its output carried the same headers. Remember the research note that some tools only expose headers on error responses. Errors are not an edge case for an attacker. They are a door.

Second, a detector that is too slow is a detector that is off. My first version used patterns that took longer and longer as a single line grew. A reviewer showed that a site could pad one header to sixty thousand characters and push the scanner past its time limit, at which point the harness simply dropped the warning. The output looked exactly like a clean result. The fix took the time from several seconds to a few milliseconds, but the point is not the fix. The point is that for a monitoring control, “it timed out” and “it found nothing” can look identical, and an attacker only needs you to confuse the two once.

I found both of those because I handed my first version to an adversarial reviewer, a separate agent whose only job was to break it, and told it to assume the thing was wrong. It found four serious defects in the first pass. I would not have found them by rereading my own work, because I wrote it, and I knew what it was supposed to do.

How to think about the defense

None of what follows is specific to my setup, and none of it requires buying anything. These are the ideas I would want any team running web-facing agents to have in their heads.

1. Inventory which tools pass headers, and default to body-only

Make a list of every tool your agents use to touch the web, and write down next to each one whether it can put response headers in the model’s context. Then make the default the tool that cannot. When headers genuinely are the subject, say during a security assessment or a caching investigation, use the header-showing tool on purpose and know that you are opening that channel. When they are not, there is no reason to read them.

2. Never give a website authority in a prompt

This is the cheapest control on the list and the research suggests it is the most powerful. Do not write prompts, agent instructions, or skill files that say “follow the instructions on the site” or “do what the page says.” That single phrase moved the test models from ignoring injected headers to obeying them most of the time. Tell your agent what you want done, and treat the site as a source of facts, not of orders.

3. Detect on what a header says, not on what it is called

If you build detection, the research has an important warning baked into it: matching on header names alone produced 910 false-positive sites, and almost all of the agent-directed headers on the web are harmless pointers. A detector that fires on every x-llms-txt will be ignored within a week. Look at the value. Is it a bare link or path, or is it prose addressed to an agent? Does it tell the reader to do something? Does it carry a command? Is it encoded in a way that a human-facing header never would be?

I ran my own detector against the headers of forty well-known sites to check exactly this. It flagged one. That one was Signals Corps’ own site, which sends a header addressed to AI agents explaining that response headers cannot give them orders. It is a benign message, and it is also, correctly, text addressed to an agent in a channel no human reads. I was happy to see it caught.

4. Gate actions, not words

Detection is advisory. It tells the model “be careful.” The research shows that models are already mostly careful, and that one phrase from the user can undo it. So the control that actually matters is not on what the agent reads. It is on what the agent does next.

The honeypot in the census asked the agent to run a package. The useful defense is to notice when a command the agent is about to run is one that fetched content suggested, and to stop and require a human to approve it. That catches the attack at the moment it would cost you something, regardless of whether the injection was phrased cleverly enough to slip past a detector.

There is a subtlety here that I learned the hard way, and I think it is the most important sentence in this post: the approval has to come through a channel the web page cannot write to. If your agent can approve something by typing a particular word or flag, then any page that can get text in front of the model can type that word too. Approval belongs in the human’s hands, in the human’s interface, not in the text the model produces.

5. Test the failure path and the slow path

Exercise your controls the way an attacker would. Make the fetch fail. Make the header enormous. Encode it. Rename it. Pipe the output through a filter that strips the status line. Run the command from inside another shell. Each of those is a cheap test, and each of them exposed a gap in my own first attempt. A control that has only ever been tested with a polite example has been tested against nobody.

6. Let something hostile read your detector before you trust it

Whoever builds a control is the person least able to see how it fails, because they know what it was meant to do. Hand it to an adversary, human or agent, and ask for the ways it reports health while failing. Then fix what comes back before you ship. I did this twice on this one small problem, and both times the review found something I would have shipped otherwise.

Where this goes

Thirty sites out of 217 million responses is not an epidemic. Most of the injections were jokes. The serious one, the honeypot that asked for a command, only works against an agent that can run commands and has been told to trust the site.

But I think this is worth paying attention to now, while it is small, for the same reason robots.txt and email headers were worth understanding early. Websites have started talking to agents directly, in a channel built for machines and invisible to people. Most of that conversation is benign and some of it is useful. The defenses that matter are not exotic. They are the old ones: know your channels, do not take orders from your data, put a human in front of the actions that cost something, and test your controls by trying to break them.

A forensic examiner does not take instructions from the disk they are imaging. An agent should not take them from a server’s response headers. The trick is making sure your tooling lets it see the difference.

References