Your robots.txt has an AI block in it. You pasted it at some point, it looked right, and you have not read it since. This post is a script that tells you what it is actually enforcing, because in my experience the answer is usually "not what you think" in one of four specific ways.
The short version of the problem: every major vendor now runs three bots, not one.
| Vendor | Training | Search / retrieval | User-triggered fetch |
|---|---|---|---|
| Anthropic | ClaudeBot |
Claude-SearchBot |
Claude-User |
| OpenAI | GPTBot |
OAI-SearchBot |
ChatGPT-User |
| Perplexity | PerplexityBot |
(same bot indexes) | Perplexity-User |
Google-Extended |
Googlebot |
n/a |
Blocking the training bot costs you nothing. Blocking the search bot removes you from that vendor's answers. Most pasted block lists hit both, because they match on a vendor prefix instead of on individual tokens.
And the promises differ per vendor, which is the part that decides your implementation:
-
Anthropic states all three of its crawlers honour robots.txt, including the user-triggered
Claude-User. -
OpenAI documents that robots.txt rules may not apply to
ChatGPT-User. So a robots.txt entry is a credible control across Anthropic's whole family, and is not sufficient for OpenAI's fetcher. Same three-tier shape, different enforceable surface.
1. Dump what you are actually serving
Not the file on disk. What the origin returns. On WordPress these differ constantly: with no physical robots.txt, core generates a virtual one, so editing the file you think exists is a silent no-op.
BASE="https://example.com"
# what the world sees
curl -s "$BASE/robots.txt" | grep -iE 'user-agent|disallow' | sed 's/^/ /'
# is it physical or virtual? a physical file usually carries an ETag / Last-Modified
curl -sI "$BASE/robots.txt" | grep -iE 'etag|last-modified|x-cache|cf-cache-status'
If your edits are not in that first output, stop here. Nothing else you configured is running either.
2. Flag deprecated tokens
Anthropic retired Claude-Web and anthropic-ai. They are harmless to keep, but their presence dates the file: a rule set naming them was written in 2024 and has not been reread since, which means every other decision in it is also a 2024 answer.
curl -s "$BASE/robots.txt" \
| grep -iE 'claude-web|anthropic-ai|cohere-ai|omgili' \
&& echo "^^ deprecated tokens present - this file needs a full reread"
3. Test enforcement per bot, in both directions
This is the part that matters, and the assertion nobody writes is the second one: the search bot must still get a 200.
#!/usr/bin/env bash
# ai-crawler-audit.sh - verify per-bot policy against a live origin
BASE="${1:?usage: ai-crawler-audit.sh https://example.com}"
# agent | expected: BLOCK or ALLOW
BOTS=(
"ClaudeBot|BLOCK"
"Claude-SearchBot|ALLOW"
"Claude-User|ALLOW"
"GPTBot|BLOCK"
"OAI-SearchBot|ALLOW"
"ChatGPT-User|ALLOW"
"PerplexityBot|BLOCK"
"Google-Extended|BLOCK"
"Googlebot|ALLOW"
)
fail=0
for entry in "${BOTS[@]}"; do
ua="${entry%%|*}"; want="${entry##*|}"
code=$(curl -s -o /dev/null -w '%{http_code}' -A "$ua/1.0" "$BASE/")
case "$want:$code" in
BLOCK:40[0-9]|ALLOW:200) status="ok" ;;
*) status="MISMATCH"; fail=1 ;;
esac
printf '%-18s want=%-5s got=%-3s %s\n' "$ua" "$want" "$code" "$status"
done
exit "$fail"
Edit the BOTS array to match the policy you actually want, then run it in CI or a cron. The point is that the expectations are declared in one place and checked, instead of living in someone's memory.
Two failure modes it catches immediately:
-
ClaudeBotreturns 200. Your rule is not running. Usual causes: virtual robots.txt, a CDN answering in front of the origin rule, or a host that ignores.htaccess. -
Claude-SearchBotreturns 403. Your pattern is too broad. A regex ofClaudematches all three Anthropic tokens, so you refused training and deleted yourself from Claude's answers in one line. ## 4. Fix the too-broad pattern
Anchor on full tokens, one per bot you actually mean. The clever compact regex is how the over-block happens.
apache (.htaccess):
<IfModule mod_rewrite.c>
RewriteEngine On
# training crawlers only - retrieval bots deliberately absent
RewriteCond %{HTTP_USER_AGENT} (ClaudeBot|GPTBot|PerplexityBot|CCBot) [NC]
RewriteRule .* - [F,L]
</IfModule>
nginx:
map $http_user_agent $ai_training_bot {
default 0;
"~*ClaudeBot" 1;
"~*GPTBot" 1;
"~*PerplexityBot" 1;
"~*CCBot" 1;
}
server {
if ($ai_training_bot) { return 403; }
}
Note what is not in either list: no Claude-SearchBot, no OAI-SearchBot, no Googlebot. That omission is the policy.
5. Verify the caller is who it claims
The user-agent string is public and forgeable, so treat it as a claim rather than an identity. Anthropic and OpenAI both publish crawler IP ranges, which makes the claim checkable: a request presenting as ClaudeBot from an address outside the published set is provably lying.
# pull the IPs your log attributed to a given bot, then check them against
# the vendor's published prefixes before trusting the label
awk '/ClaudeBot/ {print $1}' /var/log/nginx/access.log | sort -u | head -50
Confirm the current endpoint and JSON schema for each vendor's published ranges before wiring this into automation. Both vendors have changed the format at least once.
Where a plugin earns its place, and where it does not
Everything above is a hand-maintained rule set, and hand-maintained is a real option: a competent sysadmin's nginx map and a plugin-managed rule produce the identical 403. The difference is upkeep. Tokens get added, renamed, and deprecated, syntax differs across Apache and nginx, and a botched .htaccess edit takes the whole site down rather than one rule.
That maintenance gap is the honest case for something like WP Ghost, which keeps a list of 30+ AI and scraper user agents with per-bot toggles and applies them at a server-level firewall ahead of PHP, or for equivalent edge-managed bot rules if you already run everything through a CDN. Pick based on how many sites you maintain and who is responsible when a vendor adds a fourth bot.
What none of them do, including that one: unmask a scraper that lies. A user-agent policy governs the population that identifies itself. A residential proxy pool presenting a Chrome string is a rate-limiting and traffic-shaping problem with a different toolkit, and any tool advertising "blocks bad bots" as one checkbox is collapsing two populations that need different answers. Consent enforcement and extraction defence are separate projects; the audit above is squarely the first one.
Top comments (0)