Como validar seu arquivo llms.txt
Valide links, nomes e versões de pacotes e a estrutura Markdown do llms.txt com um script Node.js sem dependências e automatize as verificações no CI.
Para validar um arquivo llms.txt, verifique quatro pontos: ele começa com um H1, cada link leva à página que o texto do link descreve, cada nome e versão de pacote existe no registry, e nada nele contradiz a documentação atual.
Seu llms.txt provavelmente foi gerado por uma ferramenta de documentação durante a correria de um lançamento, e ninguém o abriu desde então. Nesse meio-tempo, páginas foram renomeadas e pacotes foram descontinuados, mas os agentes de código continuam lendo o arquivo.
Se precisar relembrar o formato, comece pelo nosso explicativo sobre llms.txt. Este artigo pula o básico. Ele mostra as verificações, oferece um script Node sem dependências e explica como executá-lo no CI para que os problemas sejam detectados antes que um agente aja com base neles.
Principais pontos
- A proposta original do llms.txt exige apenas um elemento: um H1 com o nome do projeto ou do site. O resumo em blockquote, os parágrafos de detalhes e as seções de links em H2 são todos opcionais.
- A proposta do llms.txt não publica um validador oficial, e alguns verificadores online apontam a ausência de blockquote ou de seção H2 como erro, mesmo que a proposta trate ambos como opcionais.
- Um nome de pacote no llms.txt que não existe mais no registry pode ser registrado por qualquer pessoa. Um agente com permissão para executar comandos de instalação pode então baixar o que essa pessoa publicar.
- Execute a verificação a cada deploy para capturar erros introduzidos por você, e semanalmente para capturar links e pacotes que quebram na origem sem nenhuma mudança da sua parte.
Por que um llms.txt desatualizado é um problema de supply chain?
Um llms.txt desatualizado é um problema de supply chain porque os agentes tratam o llms.txt como material de referência confiável e, muitas vezes, agem com base nele sem nenhum humano no meio do caminho. Isso transforma um link desatualizado ou um nome de pacote errado em uma instrução. Uma pessoa que encontra um link quebrado desiste. Um agente com acesso ao shell pode executar npm install em um nome que agora pertence a outra pessoa.
O risco de supply chain no llms.txt já foi demonstrado. O TechRadar noticiou a pesquisa de Alon Hertz, que varreu 6.214 domínios ativos e encontrou 8.265 arquivos llms.txt e llms-full.txt. Pelas contas dele, os arquivos de 120 sites diferentes apontavam para pacotes ou domínios que ninguém havia registrado. Em seguida, ele registrou alguns desses nomes livres e publicou pacotes que o avisavam sempre que alguém os instalava. Segundo o pesquisador, os sistemas de uma empresa da Fortune 500 começaram a se comunicar com ele em menos de uma hora. A reportagem cita Claude, Codex e Hermes como os agentes envolvidos.
Mantenha as proporções. Foi um experimento controlado, não um registro de malware em circulação. Ainda assim, é uma exposição real de supply chain, e um job de CI a elimina a baixo custo.
Controle de acesso é outra questão. Bloquear crawlers de IA com robots.txt define quais partes do seu site os crawlers podem ler. Validar o llms.txt garante que o que eles leem continua correto.
O que verificar ao validar o llms.txt
Um llms.txt válido tem um H1 e links formatados corretamente, e cada link e referência a pacote aponta para algo que existe e corresponde à sua descrição. A proposta do llms.txt não publica um validador oficial. As verificações abaixo cobrem o que os verificadores online testam, além de consultas ao registry para os pacotes citados no arquivo.
Os links resolvem
Cada URL do llms.txt deve retornar um status 2xx depois de seguidos os redirecionamentos. Trate 4xx, 5xx e falhas de DNS como erros. Um domínio que não resolve mais é a versão em domínio de um pacote não registrado: qualquer pessoa pode comprá-lo. Registre também os redirecionamentos em log. Um redirecionamento que cai na raiz do site geralmente significa que a página foi removida. Envie primeiro um HEAD e, como fallback, um GET, porque alguns servidores rejeitam ou tratam mal o HEAD.
Os links retornam o que prometem
Uma resposta 200 prova que a URL responde. Não prova que a página ainda diz o que o texto do link promete. Verifique se o content-type é HTML ou Markdown, conforme o esperado. Compare o <title> ou o primeiro heading da página com o texto do link e fique atento a soft 404s, em que uma página de erro é servida com status 200. Essa verificação é imprecisa, então execute-a como uma revisão periódica, e não como um bloqueio de build.
Pacotes e versões existem no registry
Confirme a existência no registry de todo pacote que seu llms.txt manda um agente instalar. Manualmente, npm view <name> mostra o que o registry tem para aquele pacote e retorna erro se o pacote não existir. Em um script, busque o documento do pacote em registry.npmjs.org. Um nome que o registry não conhece é um nome que outra pessoa pode registrar. Para versões fixadas, confirme que a versão exata aparece na lista de versões publicadas do pacote. PyPI, crates.io e outros ecossistemas oferecem consultas equivalentes.
A estrutura do Markdown é válida
A proposta original do llms.txt torna obrigatória apenas uma seção: um H1 com o nome do projeto ou do site. Fora um byte-order mark opcional, o arquivo precisa começar com # Name. A especificação atual do llms.txt diz que cada seção H2 contém uma lista em Markdown, e cada item dela deve começar com um link [name](url). Observações após dois-pontos são opcionais. Faça o build falhar se faltar o H1 ou se a sintaxe de algum link estiver quebrada. Não faça o build falhar por falta de blockquote ou de H2. Alguns verificadores online apontam os dois mesmo assim, então não trate esses avisos como violações da especificação.
O arquivo não contradiz a documentação atual
Compare o conjunto de URLs do llms.txt com o seu sitemap.xml ou com o manifesto de rotas do build da documentação. Considere suspeita qualquer URL que apareça no llms.txt, mas não no sitemap. Faça o mesmo com os pacotes: cada nome no arquivo deve corresponder ao campo name de um package.json que você publica, ou a uma dependência que você pretende recomendar.
Um script Node sem dependências para validar o llms.txt
Este script recebe uma URL ou um caminho local. Ele verifica o H1 e todos os links absolutos e, em seguida, consulta no registry do npm cada pacote citado em um comando de instalação. Ele usa o fetch global do Node, então roda sem nenhuma dependência em uma versão LTS atual, como o Node 24.
// scripts/check-llms.mjs — usage: node scripts/check-llms.mjs <url-or-path>
import { readFile } from 'node:fs/promises';
const src = process.argv[2];
if (!src) { console.error('usage: node check-llms.mjs <url-or-path>'); process.exit(2); }
const failures = [];
async function load(s) {
if (!/^https?:\/\//.test(s)) return readFile(s, 'utf8');
const res = await fetch(s);
if (!res.ok) { console.error(`✗ ${s} returned ${res.status}`); process.exit(1); }
return res.text();
}
async function checkLink(url) {
let res;
try {
res = await fetch(url, { method: 'HEAD', redirect: 'follow' });
} catch {
// some servers reset the connection on HEAD; fall through to GET
}
try {
if (!res?.ok) res = await fetch(url, { redirect: 'follow' }); // some servers refuse HEAD
if (res.status >= 400) failures.push(`link ${res.status}: ${url}`);
else if (res.redirected) console.warn(`↪ ${url} -> ${res.url}`);
} catch (err) {
failures.push(`link unreachable (${err.cause?.code ?? err.message}): ${url}`);
}
}
async function checkPackage(spec) {
const [, name, version] = spec.match(/^(@?[^@\s]+)(?:@(\S+))?$/) ?? [];
if (!name) return;
const res = await fetch(`https://registry.npmjs.org/${name.replace('/', '%2F')}`);
if (res.status === 404) return failures.push(`npm: "${name}" is not registered`);
if (!res.ok) return failures.push(`npm: registry returned ${res.status} for "${name}"`);
const doc = await res.json();
if (/^\d+\.\d+\.\d+/.test(version ?? '') && !doc.versions?.[version])
failures.push(`npm: ${name}@${version} does not exist`);
}
const text = await load(src);
if (!/^# \S/.test(text.trimStart())) failures.push('structure: file does not start with an H1');
const links = new Set([...text.matchAll(/\]\((https?:\/\/[^)\s]+)\)/g)].map((m) => m[1]));
const specs = new Set([...text.matchAll(/\b(npx|npm (?:install|i|add)|pnpm add|yarn add)\s+([^\n`]+)/g)]
.flatMap(([, cmd, args]) => {
const tokens = args.trim().split(/\s+/).filter((t) => !t.startsWith('-'));
return cmd === 'npx' ? tokens.slice(0, 1) : tokens;
}));
for (const url of links) await checkLink(url);
for (const spec of specs) await checkPackage(spec);
failures.forEach((f) => console.error(`✗ ${f}`));
console.log(`${links.size} links, ${specs.size} package refs, ${failures.length} failures`);
process.exit(failures.length ? 1 : 0);
Como funciona:
loadlê o arquivo do disco ou via HTTP. Se a própria URL do llms.txt falhar, o script para imediatamente.checkLinktenta primeiro umHEADe repete comGETse a requisiçãoHEADder erro ou retornar um status diferente de OK. Redirecionamentos são registrados como avisos, e falhas no nível de rede, como um domínio que não resolve, são registradas como falhas.checkPackageconsulta cada nome no registry do npm e aponta nomes inexistentes, além de versões exatas que nunca foram publicadas.- As requisições são executadas uma de cada vez de propósito, para manter baixa a carga sobre seus servidores e sobre o registry.
- Qualquer falha faz o script encerrar com código 1.
Ele tem limitações conhecidas. Links relativos são ignorados. Texto que vem depois de um comando de instalação na mesma linha pode gerar falsos positivos. Faixas de versão não são verificadas, apenas versões exatas.
Uma execução com falhas fica mais ou menos assim (saída ilustrativa com nomes fictícios):
↪ https://docs.example.com/guide -> https://docs.example.com/
✗ link unreachable (ENOTFOUND): https://docs.old.example.com/api
✗ npm: "example-sdk-legacy" is not registered
42 links, 3 package refs, 2 failures
Execute a verificação no CI a cada deploy e toda semana
Execute o validador do llms.txt de duas formas: uma vez a cada deploy, sobre o arquivo que você acabou de gerar, e outra de forma agendada, sobre o arquivo em produção. A execução no deploy captura os erros que você introduz. A execução agendada captura links quebrados e pacotes despublicados na origem enquanto seu repositório permanece intocado.
name: Validate llms.txt
on:
push:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Mondays 06:00: catches link rot and unpublished packages
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: node scripts/check-llms.mjs https://docs.example.com/llms.txt
O gatilho schedule aceita uma string cron padrão de cinco campos, e o GitHub a interpreta em UTC, a menos que você adicione uma chave timezone com o nome de um fuso horário IANA. O workflow_dispatch permite disparar uma execução manualmente. Se o deploy da sua documentação estiver no mesmo workflow, adicione antes da etapa de publicação uma etapa que aponte para a saída do build, por exemplo node scripts/check-llms.mjs dist/llms.txt, para que uma verificação com falha bloqueie o release. Mantenha o job agendado apontando para a URL em produção.
Mantenha o llms.txt sincronizado gerando-o automaticamente
Se você gerar o llms.txt a partir da mesma fonte da sua documentação, o arquivo não terá como listar uma página ou um pacote que a documentação não contém mais. Gere-o na mesma etapa que renderiza a documentação, a partir da mesma lista de páginas e do mesmo frontmatter:
// scripts/build-llms.mjs
import { writeFile } from 'node:fs/promises';
import pages from '../dist/pages.json' with { type: 'json' };
const lines = ['# Example SDK', '', '## Docs', ''];
for (const p of pages.filter((p) => !p.draft)) {
lines.push(`- [${p.title}](https://docs.example.com${p.path}): ${p.description}`);
}
await writeFile('dist/llms.txt', lines.join('\n') + '\n');
Nunca edite manualmente o arquivo gerado. Faça commit apenas do gerador ou, então, faça commit do gerador e da saída e configure o CI para falhar quando a regeneração produzir um diff. Com a geração automática implementada, o validador passa a funcionar como uma proteção para o gerador, e não como uma tarefa de limpeza que alguém precisa lembrar de fazer.
Conclusão
Um arquivo llms.txt só ajuda se tudo o que ele lista ainda existir e ainda corresponder à sua descrição, porque os agentes agem com base nele sem verificar. Adicione scripts/check-llms.mjs ao seu repositório, execute-o uma vez sobre o arquivo em produção, corrija o que ele apontar e, depois, faça commit do workflow para que o próximo link quebrado ou nome de pacote órfão faça um build falhar antes que um agente o encontre.
Perguntas frequentes
O llms-full.txt deve ser validado da mesma forma que o llms.txt?
Sim para links e pacotes, e com ainda mais rigor no caso dos pacotes. O llms-full.txt incorpora o conteúdo completo da documentação, então costuma ter muito mais comandos de instalação e exemplos de código do que o llms.txt, e cada um deles é um nome de pacote que um agente pode executar. As verificações de estrutura podem ser mais flexíveis, porque o arquivo é composto principalmente de conteúdo de páginas, e não de uma lista curta de links. Aponte o mesmo script para os dois arquivos.
Por que a verificação de links reporta erros 403 em páginas que carregam normalmente no navegador?
Provavelmente uma proteção contra bots ou um web application firewall está bloqueando a requisição automatizada. Algumas CDNs e camadas de segurança rejeitam requisições sem headers típicos de navegador ou vindas de faixas de IP de runners de CI. Coloque seu runner de CI ou o User-Agent do verificador em uma allowlist para o seu próprio domínio de documentação, ou envie um header User-Agent descritivo com o fetch. Não ignore simplesmente os erros 403, já que os agentes podem ser bloqueados da mesma forma.
Como verifico no PyPI os nomes de pacotes Python citados no llms.txt?
Consulte a API JSON do PyPI em pypi.org/pypi/NAME/json. Para um projeto existente, ela retorna os metadados e um objeto releases indexado pela string da versão, o que permite confirmar tanto o nome quanto qualquer versão fixada. Se a requisição retornar 404, trate o nome como não registrado. Extraia os nomes dos comandos pip install e uv add da mesma forma que o script faz com npm install.
Passar na verificação do registry significa que um pacote citado no llms.txt é seguro para instalar?
Não. A verificação confirma que o nome existe, não que ele pertence às pessoas certas. Alguém pode já ter registrado um nome depois que o pacote original foi abandonado, e um erro de digitação no llms.txt pode apontar para um pacote com nome parecido que de fato existe. Para cada pacote que você recomenda, verifique se o publisher e o link do repositório correspondem à sua organização e dê preferência a nomes com escopo que você controla.