Content-Signal in robots.txt never reached the AI crawlers: our own bug

Content-Signal in robots.txt never reached the AI crawlers: our own bug

Last verified: September 18, 2026
13 min read
Guide
500+ WP projects
Technical SEO

Our AI training reservation was addressed to nobody. The line Content-Signal: search=yes, ai-input=yes, ai-train=no sat in the robots.txt file of wppoland.com exactly once, inside the User-agent: * group, with twenty named crawler groups underneath it. Under RFC 9309 none of those twenty groups inherits anything from the star group.

GPTBot, ClaudeBot, PerplexityBot, Google-Extended and the rest of the list read only their own sections, and the line was not in any of them. Every crawler we had bothered to name by hand was the one crawler that could not see what we had written for it.

We are writing this up because it is our file and our mistake. We had reviewed robots.txt for whether it accidentally blocked something, and never for whether a declaration in it reaches its addressee at all. Those are two different questions, and only the first one has a tool that answers it.

#What was actually in the file

The structure looked the way most files we have seen look: one general group at the top, then a list of named bots with extended Allow rules for machine endpoints.

User-agent: *
Content-Signal: search=yes, ai-input=yes, ai-train=no
Allow: /
Disallow: /cdn-cgi/

Sitemap: https://wppoland.com/sitemap-index.xml
Agentmap: https://wppoland.com/.well-known/ai-catalog.json

User-agent: GPTBot
Allow: /
Allow: /llms.txt
Allow: /facts.json

User-agent: ClaudeBot
Allow: /

User-agent: PerplexityBot
Allow: /

It looks tidy. The signal is there, the named groups are there, the agent endpoints are listed out separately.

The problem is that those two halves never touch each other. Reading the file as a document, you see a policy at the top and a set of permissions below it, and the eye supplies a relationship between them that the parser does not. A parser does not read documents. It matches one token against a list of names, keeps the block that wins, and throws the remainder of the file away before it has read a single directive.

#Why one line does not cover the whole file

The client compares its product token against the names in the User-agent lines, case-insensitively, and applies one group: the one that names it. The asterisk is not the least specific pattern, it is the fallback, and RFC 9309 reaches for it only when no group names the client at all. Once a named group exists, the client applies only that group’s rules and the star group stops applying to it entirely.

That last sentence is the whole bug. It reads as obvious in the specification and stops being obvious the moment you hold it next to a file you wrote yourself, because a file you wrote yourself carries your intent, and the intent is invisible to the reader that matters.

This is not a special case for Content Signals. The same rule has always governed Allow and Disallow, which is exactly why a file where somebody added Disallow: /wp-admin/ only to the star group protects nothing against a bot with its own section further down.

The difference is what happens next. With Disallow the consequence is visible: the page shows up in the index or it does not, and a week of indexing data tells you which. With Content-Signal nothing is visible, in either direction. A rights declaration has no feedback loop, so a wrong one and a right one produce the same observable history for as long as you care to watch.

Content Signals is a Cloudflare proposal, not part of RFC 9309, and Cloudflare turns the policy on by default in its managed robots.txt file. The group mechanism the line lives inside, though, is entirely an RFC 9309 mechanism, so the one-group rule binds it just like everything else in that file. A new directive in an old container inherits the container’s rules, including the ones the person adding the directive never thought to look up.

#Three signals, and what they do not promise

The specification defines three signals, each taking the value yes or no:

SignalWhat it covers
searchbuilding a search index and returning results, meaning links and short snippets
ai-inputfeeding content into an AI model at answer time, meaning RAG and answer grounding
ai-traintraining and fine-tuning models

Our set, search=yes, ai-input=yes, ai-train=no, is a deliberate choice, and the trade-off inside it is worth stating plainly. We want to be cited in generative answers, because that is now one of the channels through which somebody finds us. We do not want to hand over the corpus for training, because a trained model carries the text forward with no path back to the site it came from. Saying yes to the first and no to the second is us accepting that the same crawler may be doing both jobs on different days, and that we are relying on it to tell the days apart.

No signal at all means a third thing, different from no: the site operator neither grants nor refuses permission by this route. Silence is not a refusal here, which is the whole reason the line exists.

None of these signals blocks anything. It is a line of text in a file the client may fetch or not, read or not, and respect or not. The server answers the request regardless of what that file says.

Blocking is a separate layer, evaluated ahead of the application, and you build it with WAF rules, rate limits or bot identity verification. Those cost something: they run on every request, they can be wrong about a human, and they need maintenance as the identity signals change. A declaration costs nothing and enforces nothing, which is a reasonable trade as long as you know which of the two you just shipped. The signal is worth exactly as much as an unambiguous message sent to the right address. Ours was unambiguous and sent to the wrong one.

#Check your own file with one command

It takes a few seconds and works on any domain:

curl -s https://yourdomain.com/robots.txt \
  | awk 'tolower($0) ~ /^user-agent:/ {ua++} tolower($0) ~ /^content-signal:/ {cs++} END {print "User-agent groups: " ua "\nContent-Signal lines: " cs}'

One caveat before you act on the output: this counts User-agent lines, not groups, and the ABNF in RFC 9309 allows several User-agent lines to open a single group, so a file that stacks names that way will report a shortfall it does not have. If your file does that, read the second command’s list rather than the first command’s arithmetic.

If the group count is higher than the signal count, the difference tells you how many groups cannot see your reservation. Ours read twenty one to one.

The more precise variant prints the names of the groups where the signal is missing:

curl -s https://yourdomain.com/robots.txt \
  | awk '/^[Uu]ser-agent:/ {if (name != "" && !sig) print "no signal: " name; name=$2; sig=0} /^[Cc]ontent-[Ss]ignal:/ {sig=1} END {if (name != "" && !sig) print "no signal: " name}'

The second command is the one worth wiring into a pipeline, because its output is a list to fix rather than a number to interpret.

#How to fix it

The fix is boring: repeat the Content-Signal line in every group it should cover. There is no shorthand, no compact form and no inheritance you can switch on.

User-agent: *
Content-Signal: search=yes, ai-input=yes, ai-train=no
Allow: /
Disallow: /cdn-cgi/

User-agent: GPTBot
Content-Signal: search=yes, ai-input=yes, ai-train=no
Allow: /
Allow: /llms.txt
Allow: /facts.json

User-agent: ClaudeBot
Content-Signal: search=yes, ai-input=yes, ai-train=no
Allow: /

User-agent: PerplexityBot
Content-Signal: search=yes, ai-input=yes, ai-train=no
Allow: /

We shipped that change on 18 September 2026, the day this post went out. The signal now stands in 18 groups out of 23, and the five we skipped are scrapers that already carry a Disallow from us, where a permission statement would only muddy what we are saying to them.

While making the fix you have to settle one thing the repeated line does not settle by itself: whether the same set of signals fits every group. For an indexing crawler and for a crawler collecting a training corpus the answer differs, and a file with twenty identical lines suggests nobody thought about it. A group where search=yes makes no sense, because the client builds no index, deserves its own line, and writing that line is the point at which the file stops being a copy-paste and starts being a policy.

#The second thing we were missing: groups per role

The same reading of the file turned up that we had a ClaudeBot group and neither of the other two: Claude-User, which handles user-initiated fetches, and Claude-SearchBot, which handles search grounding. Those are three different roles and three different intents on the client side. A bot fetching a page because a human just pasted its address into a conversation is doing something other than a bot building a corpus, and it makes sense to answer it differently.

The effect of the missing groups is exactly the one above, seen from the other end: a client with no section of its own falls back to the star group. In our case that meant Claude-User saw the correct signal, because the star group had it.

So in a file where the signal failed to reach twenty named bots, it did reach the unnamed ones. Precisely the opposite of what we intended, and produced by the same rule working exactly as written.

#Why this bug survives review so easily

Three reasons, all structural, none of them carelessness.

A robots.txt file reads top to bottom and the general section looks like the parent. Putting anything into it triggers the intuition you carry from CSS or from server configuration, where a general setting is the default and a specific one overrides it. RFC 9309 has no overriding. It has the selection of one group and the discarding of the rest of the file.

Second reason: no tool reports it. The robots.txt testers check whether a given URL is allowed for a given bot, and they know nothing about Content-Signal, because it is not an RFC directive. The line passes as a comment in the validator’s eyes and as policy in a human’s, and both readings are internally consistent.

Third, and the important one: a rights declaration has no return signal. Place a Disallow wrong and a week later you see it in the indexing report. Place a Content-Signal wrong and nothing happens that you could observe, and the absence of an event looks identical in both cases. This class of defect has to be found by reading the file, not by watching outcomes, which is an uncomfortable thing to schedule because reading has no trigger.

#Lines outside groups mislead, because those really are global

There is one thing in the file that actively suggests the wrong intuition. Alongside User-agent groups, robots.txt has records that are independent of groups, and Sitemap is one of them. In our file it sits right after the star group:

Sitemap: https://wppoland.com/sitemap-index.xml
Agentmap: https://wppoland.com/.well-known/ai-catalog.json

Sitemap applies to the whole file no matter where you put it or how many groups sit below it. Nobody repeats it twenty times and nobody has to.

A person who knows how Sitemap behaves, and who sees Content-Signal placed in the same region of the file two lines above it, has every right to assume the line behaves the same way. It does not. One is a global record, the other a group-scoped directive, and nothing in the syntax signals the difference. Both are a two-part line with a colon, sitting next to each other.

The practical takeaway when reading somebody else’s file: indentation and ordering mean nothing, the only thing that means anything is whether a given directive is defined as group-scoped. Allow, Disallow and Content-Signal are. Sitemap is not.

#What this post does not settle

Two things stay out of scope on purpose, because they are not ours to settle.

The first is the legal effect of the reservation. Content Signals refers to a rights reservation expressed in copyright law, and text to that effect does sit in the comment at the top of our file. Whether and when such a reservation holds is for a lawyer, not for the person editing robots.txt. Our contribution is technical only: if the declaration is to carry any weight, it has to at least reach the client it addresses, and ours did not.

The second is the behaviour of individual operators. We did not measure which crawler reads Content-Signal, which one respects it, and what any of them do with inconsistent signals across groups. Without our own measurement we have nothing to say here, and we are not going to repeat someone else’s numbers.

The fix we describe is worth making regardless, because it repairs a file that says something other than its author intended, and that is a defect in itself.

#What to do about it on the process side

Checking the signal’s reach is a one-off job of a few minutes, but keeping it right is not a one-off. Every new group added to the file, and they get added regularly, because the list of AI crawlers grows month over month, is a new group without a signal until somebody remembers it. That is why the second command above belongs in your tests, not in a note. A rule that checks itself survives; a rule written in a document survives until the next person edits the file without having read it.

It also helps to remember what share of traffic we are talking about. In our August 2026 measurement, written up in the post on bot traffic to a small site, 72% of requests did not come from a browser. The robots.txt file is the only place where we talk to that majority at all, and the only thing that majority knows about our terms. If a sentence in it is addressed to the wrong group, that is not a soft error, it is silence.

Which leaves a question worth asking about your own file before you close this tab: when did anyone last read it as a machine would, one group at a time, rather than as a page?

#Short checklist

  • Count the User-agent groups and the Content-Signal lines in your file. If the numbers differ, the difference is the number of groups with no reservation.
  • Repeat the line in every group it should cover. Repetition is the correct form here, not a duplicate.
  • Decide deliberately whether every group gets the same set of signals. Twenty identical lines usually means nobody decided.
  • Check whether you have separate groups for the roles of one provider, for example user-initiated fetches and corpus collection.
  • Wire the check into a pipeline. The crawler list grows and every group added starts out without a signal.
  • Do not tell yourself the signal blocks anything. Blocking is a layer in front of the application, this is a declaration.
Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If visibility in Google and AI systems matters, I can build the content architecture, FAQ, schema, and internal linking needed for SEO, GEO, and AEO.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Does a crawler read the star group if it has its own group in robots.txt?#
No. RFC 9309 has the client apply one group, the one naming its product token, and treats the star group as a fallback used only when no group names it. There is no inheritance from the User-agent star group. If GPTBot has its own section, the star group does not apply to it at all.
Where should the Content-Signal line go so that it actually works?#
In every group it is meant to cover, separately. A single line in the star group covers only the robots that have no section of their own. Repetition is the correct way to write this, not duplication to be cleaned up.
Is Content-Signal part of the robots.txt standard?#
No. Content Signals is a Cloudflare proposal, an extra line inside the robots.txt file. It does inherit the group semantics of RFC 9309, so the same one-group rule that governs Allow and Disallow governs it too.
Does ai-train=no stop a model from training on the content?#
No. It is a declaration, not an enforcement mechanism. The server still answers the request, and a text file has no way to force anything on the client side. The value of the signal is that it is unambiguous and sent to the right recipient.
How do I check my own file in one command?#
Fetch robots.txt and count the Content-Signal lines against the User-agent lines. If there are twenty groups and one signal, nineteen groups cannot see your reservation. The ready-made command is in the body of this post.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles

Google goto: redirects in search results

Since 26 August 2026, links in Google results go through google.com/goto instead of straight to the page. What this changes in analytics, in rank tracking tools and in WordPress, and what it does not change at all.

Googlebot and JSON-LD: a single unescape pass

Google changed its JSON-LD extraction and now applies only one pass of HTML unescaping. Double-escaped entities are no longer unrolled, so the block stops parsing and the structured data disappears. How to measure your own corpus and how to encode it correctly.