WSS
Web Specification Studio Home
On this page
BlogAEOCrawlingRobots.txtAI AgentsRFC 9309Bot VerificationPublished

Robots.txt and AI Crawlers: Which Bots Follow It, Which Ignore It (12 Failures)

A 403 on /robots.txt grants AI crawlers full access. Run a two-minute diagnostic check, fix 12 silent production failures, and audit bot behavior with a log script.

Production Reference Card

Covered AI Crawlers
GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-SearchBot, Claude-User, Google-Extended, Google-Agent, PerplexityBot, Perplexity-User, Applebot-Extended, Meta-ExternalAgent, Bytespider, CCBot
Core Specifications
RFC 9309 (Robots Exclusion Protocol), RFC 9110 (HTTP Semantics), RFC 9421 (HTTP Message Signatures)
Primary Failure Modes
Named group overriding the wildcard group, 403 read as allow, 500 KiB truncation, BOM rejection, CDN and file disagreement, staging exposure, spoofed User-Agent headers
Last Verified
14 October 2026 against vendor documentation and RFC 9309. Next review: 13 November 2026.

Three configuration failures account for most unwanted AI crawler access: a named user-agent group silently replacing the wildcard group, a 403 on /robots.txt granting full crawl access under RFC 9309, and edge CDN settings contradicting the file’s directives.

robots.txt is an advisory plaintext file at the root of a domain that instructs web crawlers which URL paths they are permitted to fetch. It carries zero cryptographic authentication and zero technical enforcement. A compliant crawler follows the rules. A misconfigured crawler or a headless scraper ignores them.

Most unwanted crawler access on production sites comes from these configuration errors, not from bots deliberately breaking rules.

This guide details a two-minute diagnostic check, 12 silent production failures, edge telemetry from 125,385 live requests, and a prioritized remediation checklist.

Two-minute diagnostic check

Run these five commands in your terminal against your live domain. Each unexpected output points directly to a numbered failure detailed below:

# Check 1: status code (expect 200)
curl -sI https://example.com/robots.txt | head -n 1

# Check 2: file size in bytes (expect well under 512,000)
curl -s https://example.com/robots.txt | wc -c

# Check 3: byte order mark (expect clean output, anything except efbbbf)
curl -s https://example.com/robots.txt | head -c 3 | xxd -p

# Check 4: number of User-agent groups (more than 1 requires checking group overrides)
curl -s https://example.com/robots.txt | grep -ic '^user-agent'

# Check 5: edge response per bot (compare against what your file intends to allow)
for ua in GPTBot OAI-SearchBot ClaudeBot Claude-SearchBot PerplexityBot Googlebot; do
  echo "$ua $(curl -s -o /dev/null -w '%{http_code}' -A "compatible; $ua/1.0" https://example.com/)"
done
CheckBad resultWhat it meansSee detailed section
Check 1403 or 404Crawlers assume full permission to crawl everythingFailure 2 of 12
Check 1500, 503, or 429Crawlers may stop fetching the entire domainFailure 2 of 12
Check 2Near or above 512,000 bytesRules near the bottom are silently discardedFailure 3 of 12
Check 3efbbbfInvisible BOM skips the first rule blockFailure 4 of 12
Check 42 or more groupsNamed bot groups replace wildcard rules entirelyFailure 1 of 12
Check 5403 for a bot the file allowsCDN edge settings contradict the robots.txtFailure 12 of 12

Check 5 inspects User-Agent string handling. Rules that verify IP ranges will not match requests dispatched from a local terminal.

Recent changes affecting bot policy

Verified against vendor updates through 14 October 2026:

Recent changeWhy it mattersOfficial source
Cloudflare split AI controlsAI crawler controls reorganized into Search, Agent, and Training. Blocking Training can reach Googlebot, Bingbot, and Applebot. Rollout varies by account.Cloudflare Radar and Dashboard
Anthropic crawler updateAnthropic updated crawler documentation to list ClaudeBot, Claude-SearchBot, and Claude-User. Older tokens like Claude-Web and anthropic-ai are legacy.Anthropic Help Center
Google-Agent classificationGoogle lists Google-Agent among user-triggered agents. User-initiated fetchers skip robots.txt.Google Search Central
OpenAI fetcher policyOpenAI states robots.txt rules may not apply to ChatGPT-User queries. A GPTBot rule does not control live user fetches.OpenAI Developer Documentation
IETF aipref working groupActive working group developing standardized AI usage preference vocabularies. Major crawlers do not follow these drafts yet.IETF Datatracker

Verification sources and methodology

We do not rely on speculation or marketing summaries. Every claim in this guide is grounded in verifiable sources:

  • Primary specifications: RFC 9309 (Robots Exclusion Protocol), RFC 9110 (HTTP Semantics), RFC 9421 (HTTP Message Signatures).
  • Vendor documentation: Official crawler guides from OpenAI, Anthropic, Google, Perplexity, and Apple updated through October 2026.
  • Production edge telemetry: 30 days of live Cloudflare edge analytics covering 125,385 requests and 1.66 GB of traffic.
  • Diagnostic scanner tests: First-party automated audits from our live Agent Ready Scanner.

Production telemetry from our edge

Our site runs entirely on Cloudflare Pages with Cloudflare Workers, without an origin server behind it.

The data below represents 30 days of live edge telemetry collected between September 13 and October 12, 2026. These numbers count inbound requests at the global edge before any caching layer takes effect.

Traffic baseline

MetricValue
Edge requests, 30 days125,385
Daily average requests4,179.5
Peak day10 October, 8,314 requests
Unique visitors, sum of daily uniques24,660
Daily average unique visitors822
Peak unique visitors8 October, 1,337
Data served1,784,844,892 bytes (1.66 GiB)
Peak day bandwidth10 October, 123.61 MB

Traffic distribution across the 30-day window:

  • Requests per unique visitor: 5.08 overall, with a daily median of 5.22.
  • Average response size: about 13.9 KB per request.
  • Peak day versus average: October 10 carried 1.99 times the daily average volume.

Cloudflare calculates unique visitors by hashing client IP addresses. Recurring automated scanners and scraper networks are captured in the total.

Days worth inspecting

The ratio of requests per unique visitor is a dependable signal for separating ordinary human browsing from automated crawler sweeps:

DateRequestsUniquesRequests per unique
13 Sep3,9844468.93
10 Oct8,3141,0198.16
6 Oct6,5608527.70
20 Sep5,1926947.48

October 10 was a Saturday. It registered our highest request count of the month along with a request-to-unique ratio of 8.16, well above the 5.22 baseline median.

October 6 also stands out, serving 88.26 MB of bandwidth.

Because standard Cloudflare analytics do not log raw User-Agent headers, edge totals cannot prove whether an individual spike originated from AI scrapers, search indexing, or human sessions.

Figure 1: 30-day edge traffic telemetry

Click any day to inspect requests, unique visitors, bandwidth, and sweep signals.

8k 5k 2.5k 0 13 Sep 20 Sep 27 Sep 1 Oct 6 Oct 10 Oct
Selected Day 10 Oct (Monthly Peak)
Monthly Peak Volume (Ratio: 8.16)

Edge requests

8,314

Unique visitors

1,019

Reqs / unique

8.16

Data served

129.6 MB

Registered 1.99x our daily volume with an elevated request-to-unique ratio, characteristic of multi-worker crawler sweeps.

Figure 1: Interactive 30-day edge traffic telemetry recorded on Cloudflare Pages.

Requests to disallowed routes

Our production telemetry recorded repeated GET attempts against three specific routes explicitly forbidden in our robots.txt file:

RouteRobots.txt ruleStatus returned
/api/chatDisallow: /api/404. The Pages Function implements POST only. GET requests fail immediately.
/.well-known/oauth-protected-resourceExplicit Disallow410 Gone with X-Robots-Tag: noindex, nofollow
/.well-known/webauthnExplicit Disallow410 Gone with X-Robots-Tag: noindex, nofollow

A fully compliant crawler would not request these paths.

Software clients searching for authentication capabilities or security policies frequently hit .well-known endpoints during discovery routines. These clients are specialized tools, not web scrapers that parse robots.txt.

Without raw log headers, the requester behind each hit cannot be identified. Per-bot compliance was not measured from this dataset, because aggregate Cloudflare analytics do not include the user-agent field.

What the data shows: automated software requested paths that robots.txt forbids. Hard HTTP status codes at the edge stopped those requests from returning content.

Auditing our own robots.txt

Our live robots.txt uses a single User-agent: * group with Allow: / alongside specific disallow paths for /search/, /api/, /cdn-cgi/, /*$*, and seven .well-known endpoints.

Evaluating this configuration against RFC 9309 exposes two findings:

First, a single wildcard group governs every bot equally. Because we do not declare named bot groups, we avoid the named-group override trap. At the same time, this single-group configuration cannot treat training scrapers differently from search engines.

Second, the rule Disallow: /*$* is ineffective for wildcards. Under RFC 9309 §2.2.3, the dollar sign acts as an end-of-URL anchor only when placed at the very end of a pattern. In the middle of a string, standard parsers treat $ as a literal character.

Agent Ready Scanner self-audit

We ran our live diagnostic tool, the Agent Ready Scanner, against our domain. The audit returned an overall score of 53 out of 100, placing the site at Level 2 (Machine-Actionable Readiness):

CategoryScoreResult
Discoverability83%Pass. robots.txt returns 200, sitemap-index.xml is active, and Link headers declare alternate Markdown representations.
Content Representation83%Pass. Server returns Markdown for Accept: text/markdown, /llms.txt passes checks, and the DOM is server-rendered.
Bot Access Governance13%Flagged

The governance score reflects the site’s open-access policy, not a technical bug.

Our site policy grants open access to all AI tokens, including GPTBot, ClaudeBot, and PerplexityBot. We do not separate model training scrapers from search indexing.

For a software business or content publisher, an unsegmented policy allows scrapers to harvest your data without giving you search visibility in return.

What aggregate edge data cannot show

Aggregate analytics have clear blind spots:

  • They cannot show request totals broken down by individual crawler token.
  • They cannot prove whether a specific bot hit a disallowed path.
  • They cannot show whether user-triggered fetchers followed the stated directives.

Resolving these questions requires per-request access logs. We provide an automated Python audit script below to analyze those logs on your own server.

Which bot does what: training vs search vs user retrieval

AI operators deploy distinct bot fleets for different tasks. A rule written for one fleet does not apply to the others:

AI operatorTraining fleetSearch indexerUser-triggered fetcher
OpenAIGPTBotOAI-SearchBotChatGPT-User
AnthropicClaudeBotClaude-SearchBotClaude-User
GoogleGoogle-Extended (token)GooglebotGoogle-Agent, user fetchers
PerplexityNone documentedPerplexityBotPerplexity-User
AppleApplebot-Extended (token)ApplebotApplebot
MetaMeta-ExternalAgentNoneMeta-ExternalFetcher
ByteDanceBytespiderNoneNone
Common CrawlCCBotNoneNone

Tokens like Google-Extended and Applebot-Extended are policy controls, not distinct HTTP user agents. When Google crawls your site for Gemini training, the request carries the standard Googlebot user-agent string. Google checks your robots.txt for the Google-Extended token to decide whether that content can be fed into model pre-training.

What each vendor documents about robots.txt

Vendor commitments vary widely across training, search, and user retrieval:

Bot tokenDocumented behaviorSource
GPTBotFollows robots.txt. A Disallow opts out of training.OpenAI official docs
OAI-SearchBotFollows robots.txt. An opt-out removes content from ChatGPT search citations.OpenAI official docs
ChatGPT-UserRobots.txt rules may not apply because a human user initiated the request.OpenAI official docs
ClaudeBotFollows robots.txt. A Disallow excludes content from future training cycles.Anthropic official docs
Claude-SearchBotFollows robots.txt. Disabling reduces search citations.Anthropic official docs
Claude-UserFollows robots.txt. Disallow stops user query retrieval.Anthropic official docs
Perplexity-UserGenerally ignores robots.txt because an active user requested the URL.Perplexity official docs
Google user fetchersIgnore robots.txt rules entirely.Google Search Central
BytespiderWidely reported in developer forums and secondary reports to ignore robots.txt. No first-party documentation found.Secondary reports

What blocking each category costs

Blocking crawlers creates different trade-offs depending on the bot type.

Cloudflare Radar tracks crawl-to-referral ratios, comparing pages scraped against human visitors sent back:

Operator and botCrawl-to-referral ratioMeasurement periodSource
Anthropic ClaudeBot~38,000 requests per 1 referralJuly 2025Cloudflare Radar
Anthropic ClaudeBot~286,000 requests per 1 referralJanuary 2025Cloudflare Radar

While exact figures fluctuate across reporting cycles, the underlying economic reality is consistent:

Training crawlers take your content and server bandwidth while sending almost zero visitors back.

Search crawlers deliver real traffic and referral links.

For most web properties, a split policy is the most effective choice: opt out of training, stay eligible for search citations.

Twelve production failures

Failure 1 of 12: a named group replaces the wildcard group

A crawler selects the one group that best matches its product token. It does not merge that group with User-agent: *.

User-agent: *
Disallow: /admin/
Disallow: /api/

User-agent: PerplexityBot
Allow: /

PerplexityBot matches its own group, so the wildcard Disallow lines do not apply to it. Both /admin/ and /api/ are open to that crawler.

Repeat every shared rule inside each named group:

User-agent: PerplexityBot
Disallow: /admin/
Disallow: /api/
Allow: /

Token matching is case-insensitive. Several User-agent lines stacked above one rule set share that rule set.

Failure 2 of 12: status codes on /robots.txt change the policy

RFC 9309 §2.3.1 defines how a crawler treats each response to the robots.txt request. An error can produce the exact opposite of your intended policy:

Status on /robots.txtRFC 9309 specificationCrawler action
200 OKParse rulesEnforced
3xx redirectFollow at least 5 consecutive hopsRules from final response apply
4xx (404, 410, 403)File is unavailableCrawlers assume full permission to crawl everything
5xx (500, 503)Server is unreachableAssume full disallow while error lasts

A deployment that routes /robots.txt to a 404 removes every Disallow rule at once.

A WAF or bot rule that returns 403 on /robots.txt grants full crawling access.

Google documents that it treats 429 on robots.txt like a server error. Exclude /robots.txt from challenges, rate limits, and authentication.

Failure 3 of 12: the file passes 500 KiB

RFC 9309 §2.5 lets a crawler cap parsing and requires the cap to be at least 500 kibibytes. Google documents the 500 KiB limit. Content after the limit is discarded.

Sites that generate thousands of Disallow lines for faceted navigation can easily pass it. If your AI rules sit at the bottom, the parser never reads them.

Move AI user-agent groups to the top or shrink the file.

Failure 4 of 12: a byte order mark skips the first rule

Windows tooling and some CMS exports prepend a Byte Order Mark (\xEF\xBB\xBF). A parser that does not strip it reads the first line as an unknown directive and skips the first record.

Several large parsers tolerate a BOM, but not every crawler does. Strip it immediately:

sed -i '1s/^\xEF\xBB\xBF//' public/robots.txt

Failure 5 of 12: path rules do not match what authors expect

Under RFC 9309 §2.2.2, the longest matching path wins. Rule order is irrelevant.

User-agent: GPTBot
Disallow: /
Allow: /docs/public/

For /docs/public/architecture.html, Disallow matches 1 character (/) and Allow matches 13 characters (/docs/public/). The longer match wins, so the request is allowed. On equal length, Allow is preferred.

Section 2.2.3 defines two pattern characters:

  • An asterisk matches any sequence of characters.
  • A dollar sign anchors the end of the URL, and only at the very end of a pattern.

Paths are case-sensitive, and rules match by prefix. Writing Disallow: /fish also blocks /fishing.

Our site’s own Disallow: /*$* rule illustrates this trap: in the middle of a pattern, a parser treats $ as a literal character.

Failure 6 of 12: Crawl-delay and noindex do not mean what they imply

Crawl-delay sits outside RFC 9309. Anthropic documents support for it. Google does not support it, and OpenAI does not document it.

Even where a vendor supports it, a crawler with many concurrent workers can exceed your intended rate. Limit rates at the edge.

Noindex inside robots.txt is dead. Google retired it in 2019, and RFC 9309 omits it. Use a response header instead:

X-Robots-Tag: noindex, noarchive

Google also documents that nosnippet, max-snippet, and data-nosnippet limit how page text appears in AI Overviews. Tags such as noai and noimageai belong to no standard.

Failure 7 of 12: Disallow hides the noindex header

A crawler blocked by Disallow: /internal/ never requests the page. It never sees the X-Robots-Tag header either.

If other sites link to that URL, search engines can still display the bare URL without a snippet.

To keep a URL out of search results, allow crawling and serve the noindex header.

Failure 8 of 12: user-triggered fetchers skip the file

When a user asks an assistant to read a URL, the platform sends a live request. Vendors disagree on whether robots.txt governs it.

OpenAI and Perplexity note rules may not apply. Google states its user-triggered fetchers ignore it. Anthropic states Claude-User follows it.

A robots.txt rule alone gives no guarantee across vendors. Some platforms route fetches through proxy networks, arriving with generic browser headers and unlisted IPs.

Failure 9 of 12: spoofed user agents pass as real bots

The User-Agent header is arbitrary plain text:

curl -A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.3; +https://openai.com/gptbot" https://example.com/

Unauthorized scrapers frequently disguise themselves as ordinary desktop browsers. No robots.txt rule reaches a client that does not declare itself.

Verify bot identity using the method each operator documents:

OperatorDocumented verification mechanism
GoogleReverse DNS to googlebot.com or google.com with forward DNS match, or public IP JSON
OpenAIPublished CIDR prefixes at openai.com/gptbot.json, searchbot.json, chatgpt-user.json
AnthropicPublished IP ranges at claude.com/crawling/bots.json
PerplexityPublished IP range JSON feed
curl -s https://openai.com/gptbot.json | jq -r '.prefixes[] | .ipv4Prefix // .ipv6Prefix'

A request carrying a bot token from an address outside the operator’s published list is spoofed.

Failure 10 of 12: IP blocking hides robots.txt

Anthropic documents that blocking its crawler IPs can stop its bots from reading robots.txt, so your opt-out is never seen.

Disallow in the file first. Reserve IP blocking rules for traffic that fails verification.

Failure 11 of 12: staging and preview environments are open

Preview deployments are public by default on many platforms and inherit no robots.txt policy. A public GitLab issue recorded Bytespider alone making 12,090 requests in 30 days against review app deployments.

For every non-production environment:

  • Serve a restrictive robots.txt covering AI tokens.
  • Add X-Robots-Tag: noindex for search engines.
  • Put authentication in front of previews. Advisory files are not access barriers.

Failure 12 of 12: the CDN contradicts the file

A file that allows a bot does not mean the edge allows it. The crawler reads the allow rule, then receives a 403 on every page.

Cloudflare reorganized its AI crawler controls into Search, Agent, and Training categories. As of October 2026, our Cloudflare dashboard still shows a single toggle labelled AI Scrapers and Crawlers under Security > Bots.

Check which controls your account shows. Check whether a block also applies to Googlebot, Bingbot, and Applebot before enabling it, and run Check 5 from the top of this article after every change.

Production templates

Template 1: allow search citations, block training

Each named group repeats your private path rules:

User-agent: *
Disallow: /admin/
Disallow: /private/
Disallow: /api/

# Training crawlers and control tokens
User-agent: GPTBot
User-agent: ClaudeBot
User-agent: Google-Extended
User-agent: Applebot-Extended
User-agent: Meta-ExternalAgent
User-agent: Bytespider
User-agent: CCBot
Disallow: /

# Search indexers and user-triggered fetchers
User-agent: OAI-SearchBot
User-agent: Claude-SearchBot
User-agent: PerplexityBot
User-agent: ChatGPT-User
User-agent: Claude-User
User-agent: Perplexity-User
Disallow: /admin/
Disallow: /private/
Disallow: /api/
Allow: /

Template 2: block all AI bots

User-agent: GPTBot
User-agent: OAI-SearchBot
User-agent: ChatGPT-User
User-agent: ClaudeBot
User-agent: Claude-SearchBot
User-agent: Claude-User
User-agent: Google-Extended
User-agent: PerplexityBot
User-agent: Perplexity-User
User-agent: Applebot-Extended
User-agent: Meta-ExternalAgent
User-agent: Meta-ExternalFetcher
User-agent: Bytespider
User-agent: CCBot
User-agent: Diffbot
Disallow: /

Blocking search indexers removes your site from AI answer citations. Some user-triggered fetchers may still reach the site.

Policy selection by site type

Platform typeTrainingSearch indexUser fetchEdge controls
Publisher with ad revenueBlockAllowAllowRate limit, verify IPs
Technical documentationAllow or block by licensing intentAllowAllowRate limit
Ecommerce catalogBlockAllowAllowRate limit, bot score
Paywalled or member contentBlockBlockBlockAuthentication, not robots.txt
Staging and previewsBlockBlockBlockAuthentication gate

Usage preferences and rights reservation

Cloudflare’s Content Signals Policy defines search, ai-input, and ai-train signals in robots.txt.

The IETF aipref working group is drafting a shared vocabulary for AI usage preferences and headers. Major crawlers do not follow these drafts yet.

In the EU, text and data mining exceptions can depend on machine-readable opt-outs (robots.txt or W3C TDMRep). None of these enforce access; all rely on voluntary compliance.

Enforcement at the edge

Cloudflare WAF rules

Cloudflare rules enforce bot policy without Worker code. Confirm field names against current documentation before deploying:

Rule to block selected AI user agents on application paths:

(http.request.uri.path starts_with "/api/") and (http.user_agent contains "GPTBot" or http.user_agent contains "ClaudeBot" or http.user_agent contains "Bytespider" or http.user_agent contains "CCBot")

Set the rule action to Block.

Rule to return permanent errors for unpublished system paths:

(http.request.uri.path eq "/.well-known/oauth-protected-resource") or (http.request.uri.path eq "/.well-known/webauthn")

A Pages Function or Worker can return 410 Gone for these paths, signaling permanent absence.

Exclude /robots.txt from every rule by appending:

(http.request.uri.path ne "/robots.txt")

Cloudflare Worker implementation

This Worker rejects selected AI user agents on restricted paths and leaves /robots.txt untouched:

export default {
  async fetch(request: Request): Promise<Response> {
    const userAgent = request.headers.get("user-agent") || "";
    const url = new URL(request.url);
    const aiBotPattern = /(GPTBot|ClaudeBot|Bytespider|CCBot|Meta-ExternalAgent)/i;

    if (url.pathname === "/robots.txt") {
      return fetch(request);
    }

    if (aiBotPattern.test(userAgent) && url.pathname.startsWith("/api/")) {
      return new Response("Forbidden: AI crawler access restricted on this path.", {
        status: 403,
        headers: {
          "Content-Type": "text/plain",
          "X-Robots-Tag": "noindex, nofollow",
        },
      });
    }

    return fetch(request);
  },
};

A user agent match can be spoofed in both directions. Pair it with IP range verification on sensitive endpoints.

Static headers on Cloudflare Pages

A _headers file in the build output sets security headers for static assets without a Worker:

/internal/*
  X-Robots-Tag: noindex, noarchive

Rate limiting on Nginx

For sites outside Cloudflare, this pattern maps matching user agents to a rate-limited zone:

map $http_user_agent $ai_bot_key {
    default "";
    ~*(GPTBot|ClaudeBot|Claude-SearchBot|PerplexityBot|Bytespider|CCBot|Meta-ExternalAgent) $binary_remote_addr;
}

limit_req_zone $ai_bot_key zone=ai_crawler_limit:10m rate=2r/s;
limit_req_status 429;

server {
    location / {
        limit_req zone=ai_crawler_limit burst=5 nodelay;
    }
}

Return HTTP 429 with a Retry-After header, and never apply the limit to /robots.txt.

Cryptographic bot authentication

RFC 9421 defines HTTP Message Signatures. The IETF Web Bot Auth effort applies them so crawlers sign requests and servers verify them against a public key ledger. Adoption is in early stages.

Measure bot behavior in access logs

Counting what AI bots do on a specific server answers questions vendor documentation cannot. The script below reads combined access logs and reports:

  • Total requests and /robots.txt fetches.
  • Requests to paths that robots.txt disallows for that token.
  • Requests originating from IP ranges the operator publishes.
  • Status code distributions.

It parses robots.txt with Protego, implementing longest-match and wildcard rules. Python’s built-in urllib.robotparser applies rules in file order and does not match RFC 9309 behavior.

Per-request logs on Cloudflare Pages

Cloudflare analytics expose aggregates, while the audit script needs one line per request with a user agent:

  1. Logpush streaming HTTP request logs to cloud storage.
  2. Worker Logging inside edge functions.
  3. GraphQL Analytics API queries grouped by user agent.

Convert exported logs to combined format, or adjust the LINE pattern at the top of the script.

Running the audit

pip install protego
curl -s https://openai.com/gptbot.json -o gptbot.json
curl -s https://openai.com/searchbot.json -o searchbot.json
curl -s https://openai.com/chatgpt-user.json -o chatgpt-user.json
curl -s https://claude.com/crawling/bots.json -o claude-bots.json
curl -s https://example.com/robots.txt -o robots.txt

python ai_bot_audit.py /var/log/nginx/access.log robots.txt \
  --ranges gptbot.json searchbot.json chatgpt-user.json

Sample output from a synthetic test log with placeholder addresses illustrates the format:

token                   requests   robots_fetches   hits_on_disallowed   disallowed_verified   verified_ip   unverified_ip
GPTBot                         4                1                    1                     1             3               0
ClaudeBot                      1                0                    1                     0             0               1

Evaluate the results in this order:

  • Disallowed hits from verified IPs: proves an operator compliance failure or a stale robots.txt cache.
  • Disallowed hits from unverified IPs: proves spoofing or proxy fetchers. Block these at the edge.
  • Zero robots.txt fetches alongside high request counts: identifies crawlers that never inspect the file.
  • High 4xx response share: shows crawlers hitting dead links or encountering active edge blocks.

The audit script

Save this as ai_bot_audit.py. It requires Python 3 and the protego package:

#!/usr/bin/env python3
"""Audit AI crawler behavior in an access log against a robots.txt file.

Usage:
  python ai_bot_audit.py access.log robots.txt [--ranges gptbot.json searchbot.json ...]

Input: combined log format (Nginx and Apache default). Range files use the
OpenAI/Perplexity style: {"prefixes": [{"ipv4Prefix": "..."}, {"ipv6Prefix": "..."}]}
or a plain text file with one CIDR per line.
"""
import argparse, collections, ipaddress, json, re, sys
from urllib.parse import urlsplit
from protego import Protego

# Google-Extended is a robots.txt control token only. Google documents that it
# has no HTTP user agent string, so it never identifies a real request. A log
# line that contains it is not from Google.
TOKENS = [
    "GPTBot", "OAI-SearchBot", "ChatGPT-User", "ClaudeBot", "Claude-SearchBot",
    "Claude-User", "PerplexityBot", "Perplexity-User", "Meta-ExternalAgent",
    "Meta-ExternalFetcher", "Bytespider", "CCBot", "Applebot", "Amazonbot",
    "Googlebot", "bingbot",
]
LINE = re.compile(
    r'^(?P<ip>\S+) \S+ \S+ \[[^\]]+\] "(?P<method>\S+) (?P<path>\S+) [^"]*" '
    r'(?P<status>\d{3}) (?P<bytes>\S+) "[^"]*" "(?P<ua>[^"]*)"'
)

def load_ranges(paths):
    nets = []
    for p in paths:
        text = open(p, encoding="utf-8").read().strip()
        try:
            data = json.loads(text)
            for item in data.get("prefixes", []):
                cidr = item.get("ipv4Prefix") or item.get("ipv6Prefix")
                if cidr:
                    nets.append(ipaddress.ip_network(cidr, strict=False))
        except json.JSONDecodeError:
            for ln in text.splitlines():
                ln = ln.strip()
                if ln and not ln.startswith("#"):
                    nets.append(ipaddress.ip_network(ln, strict=False))
    return nets

def in_ranges(ip, nets):
    try:
        addr = ipaddress.ip_address(ip)
    except ValueError:
        return False
    return any(addr in n for n in nets)

def token_for(ua):
    low = ua.lower()
    for t in TOKENS:
        if t.lower() in low:
            return t
    return None

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("log")
    ap.add_argument("robots")
    ap.add_argument("--ranges", nargs="*", default=[])
    a = ap.parse_args()

    rp = Protego.parse(open(a.robots, encoding="utf-8-sig").read())
    nets = load_ranges(a.ranges)
    stats = collections.defaultdict(lambda: collections.Counter())
    violations = collections.defaultdict(list)

    for line in open(a.log, encoding="utf-8", errors="replace"):
        m = LINE.match(line)
        if not m:
            continue
        tok = token_for(m["ua"])
        if not tok:
            continue
        s = stats[tok]
        s["requests"] += 1
        path = urlsplit(m["path"]).path or "/"
        if path == "/robots.txt":
            s["robots_fetches"] += 1
            continue
        if nets:
            s["verified_ip" if in_ranges(m["ip"], nets) else "unverified_ip"] += 1
        if not rp.can_fetch("https://example.com" + m["path"], tok):
            s["hits_on_disallowed"] += 1
            if nets and in_ranges(m["ip"], nets):
                s["disallowed_verified"] += 1
            if len(violations[tok]) < 5:
                violations[tok].append((m["ip"], m["path"], m["status"]))
        s[f"status_{m['status'][0]}xx"] += 1

    cols = [
        "requests", "robots_fetches", "hits_on_disallowed", "disallowed_verified",
        "verified_ip", "unverified_ip", "status_2xx", "status_4xx", "status_5xx",
    ]
    print(f"{'token':<22}" + "".join(f"{c:>21}" for c in cols))
    for tok, s in sorted(stats.items(), key=lambda kv: -kv[1]["requests"]):
        print(f"{tok:<22}" + "".join(f"{s[c]:>21}" for c in cols))

    if violations:
        print("\nSample requests to paths the file disallows for that token:")
        for tok, rows in violations.items():
            for ip, path, status in rows:
                print(f"  {tok:<20} {ip:<16} {status} {path}")

if __name__ == "__main__":
    main()

Adding a regression test to CI

A deployment can break robots.txt with no visible error. Assert your policy in continuous integration:

from protego import Protego
import urllib.request

body = urllib.request.urlopen("https://staging.example.com/robots.txt").read()
assert body[:3] != b"\xef\xbb\xbf", "BOM present"
assert len(body) < 500 * 1024, "robots.txt exceeds 500 KiB"

rp = Protego.parse(body.decode("utf-8"))
assert not rp.can_fetch("https://example.com/blog/post", "GPTBot")
assert rp.can_fetch("https://example.com/blog/post", "OAI-SearchBot")
assert not rp.can_fetch("https://example.com/admin/", "PerplexityBot")

The final assertion tests whether the named PerplexityBot group still carries the /admin/ Disallow. It does not test all private routes, only this specific rule.

Frequently asked questions

Does robots.txt stop AI training?

It stops compliant training crawlers like GPTBot and ClaudeBot from fetching new content. It does not remove content already collected, and it cannot bind scrapers that ignore the file.

No. Search visibility is controlled by OAI-SearchBot. Blocking GPTBot opts your content out of model training only.

Does Google-Extended affect Google Search ranking?

No. It is a control token for Gemini training and grounding. Googlebot crawling and Search rankings remain untouched.

Does Crawl-delay work for AI crawlers?

It depends on the vendor. Anthropic documents support for Crawl-delay on its bots. OpenAI and Google do not. Treat Crawl-delay as an unverified wish, and enforce hard rate limits at your edge.

Does Claude-User follow robots.txt?

Anthropic states that disabling Claude-User in robots.txt prevents on-demand retrieval for user queries. OpenAI and Perplexity state the exact opposite for ChatGPT-User and Perplexity-User.

Does blocking an AI crawler by IP address work?

It often backfires. Anthropic documents that blocking its IP ranges prevents its bots from fetching robots.txt in the first place, meaning your opt-out rules are never read.

How long does a robots.txt change take to apply?

RFC 9309 allows scrapers to cache the file for up to 24 hours. OpenAI documents roughly 24 hours before search crawlers refresh their cache.

Prioritized remediation checklist

Prioritize remediation by risk level:

  1. Return HTTP 200 for /robots.txt and exclude it from every WAF rule, challenge, and rate limit.
  2. Repeat shared Disallow lines inside every named user-agent group.
  3. Keep the file under 500 KiB and strip any Byte Order Mark.
  4. Put real authentication in front of staging, previews, and private endpoints.
  5. Confirm that edge CDN rules and robots.txt directives agree by running Check 5 after every deploy.
  6. Split training, search, and user-triggered fetch into three separate decisions.
  7. Disallow in robots.txt before blocking any vendor by IP address.
  8. Verify crawler identity with published CIDRs or reverse DNS lookups.
  9. Run the Agent Ready Scanner for an automated baseline score.
  10. Add automated CI assertions so commits cannot silently break policy.

Changelog

  • 2026-10-14: Restructured around a two-minute diagnostic check and 12 numbered production failures. Added a dated section on recent policy changes and a prioritized remediation checklist.
  • 2026-10-13: Initial publication. Added first-party edge analytics and the Agent Ready Scanner self-audit. Corrected statements about Crawl-delay support, Claude crawler names, IP verification, and 403 handling.
  • Next review: 2026-11-13.

Written by

Platform Engineer and Technical Writer with 10+ years of full-stack development experience and 2+ years focused on DevOps and platform engineering.

Related posts