Overview
Check everything a script can check deterministically, then spend model calls only on what needs judgment: whether each description is a useful routing hint. llms.txt drifts from the real site as pages are added, renamed, or deleted. This guide diffs it against the sitemap, then asks Claude to grade descriptions. The format itself is defined in llms-txt; building the file is in ship-llms-txt.
Prerequisites
- A live
/llms.txtand/sitemap.xml. If the file is missing, start with ship-llms-txt. curl, plus an Anthropic API key (ANTHROPIC_API_KEY) or Claude Code open in the project.
Steps
1. Diff against the sitemap
SITE=https://example.com
curl -s $SITE/llms.txt | grep -oE 'https?://[^) ]+' | sed 's:/$::' | sort -u > listed.txt
curl -s $SITE/sitemap.xml | grep -oP '(?<=<loc>)[^<]+' | sed 's:/$::' | sort -u > sitemap.txt
comm -13 listed.txt sitemap.txt # pages missing from llms.txt
comm -23 listed.txt sitemap.txt # listed URLs that are not in the sitemap (renamed or deleted)
curl -s $SITE/llms.txt | grep -oE 'https?://[^) ]+' | sort | uniq -d # duplicatesAlso fail on relative links and non-200 responses. These need no model.
2. Grade descriptions with a structured response
Constrain the output with a JSON schema so the result parses every time.
import json, anthropic
client = anthropic.Anthropic()
llms = open("llms-current.txt").read()
schema = {
"type": "object",
"properties": {
"weak_descriptions": {"type": "array", "items": {
"type": "object",
"properties": {"url": {"type": "string"}, "problem": {"type": "string"}, "suggested": {"type": "string"}},
"required": ["url", "problem", "suggested"], "additionalProperties": False}},
},
"required": ["weak_descriptions"], "additionalProperties": False,
}
resp = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
system="You review llms.txt entries. Flag descriptions that are empty, vague, tautological, "
"or longer than one sentence. Quote URLs exactly as given.",
messages=[{"role": "user", "content": f"<llms_txt>\n{llms}\n</llms_txt>"}],
output_config={"format": {"type": "json_schema", "schema": schema}},
)
print(json.loads(next(b.text for b in resp.content if b.type == "text")))Do not prefill the assistant turn to force JSON; current Claude models reject prefill, and the schema makes it unnecessary. Check every returned url against listed.txt in code before applying a suggestion.
3. Apply fixes and re-run
Fix the generator inputs (page summary frontmatter), not the generated file, then rebuild and repeat steps 1 and 2 until both are empty.
4. Catch drift in CI
PAGES=$(grep -c '<loc>' public/sitemap.xml)
ENTRIES=$(grep -c '^- \[' public/llms.txt)
[ "$ENTRIES" -ge "$PAGES" ] || { echo "llms.txt lists $ENTRIES of $PAGES pages"; exit 1; }Adjust for pages you list on purpose or exclude, such as index pages and drafts.
Verify it worked
Step 1 prints nothing, step 2 returns an empty weak_descriptions array, and curl -sI $SITE/llms.txt returns 200.
Common errors
- Every page shows as missing. The sitemap uses a trailing slash or
wwwform thatllms.txtdoes not; normalize both beforecomm. - The model invents URLs. Treat model output as suggestions and validate against the real URL list.
- The CI count fails after adding a page. The generator was not re-run before the check.
FAQ
Do I need an LLM to audit llms.txt?
Only for description quality. Missing pages, stale URLs, duplicates, relative links, and non-200 responses come from shell diffs against the sitemap and need no model.
How do I get reliable JSON back from Claude for the audit?
Constrain the output with a JSON schema through output_config. Do not prefill the assistant turn, because current Claude models reject prefill.
Should I edit llms.txt by hand when Claude flags a weak description?
No. Fix the generator inputs, usually the page summary frontmatter, then rebuild and repeat the diff and the grading until both come back empty.
Why does every page show as missing from llms.txt?
The sitemap uses a trailing slash or www form that llms.txt does not; normalize both before running comm.