Cómo validar tu archivo llms.txt
Valida los enlaces, nombres y versiones de paquetes y la estructura Markdown de llms.txt con un script Node.js sin dependencias; automatiza las comprobaciones en CI.
Para validar un archivo llms.txt, comprueba cuatro cosas: que empiece con un H1, que cada enlace lleve a la página que describe su texto, que cada nombre y versión de paquete exista en el registro y que nada en él contradiga tu documentación actual.
Lo más probable es que una herramienta de documentación generara tu llms.txt durante un sprint de lanzamiento y que nadie lo haya vuelto a abrir desde entonces. Mientras tanto, se han renombrado páginas y se han deprecado paquetes, y los agentes de programación siguen leyendo el archivo de todos modos.
Si necesitas repasar el formato, empieza por nuestra explicación sobre llms.txt. Este artículo se salta lo básico: recorre las comprobaciones, te ofrece un script de Node sin dependencias y muestra cómo ejecutarlo en CI para detectar los problemas antes de que un agente actúe en función de ellos.
Puntos clave
- La propuesta original de llms.txt solo exige un elemento: un H1 con el nombre del proyecto o del sitio. El resumen en blockquote, los párrafos de detalle y las secciones de enlaces bajo H2 son opcionales.
- La propuesta de llms.txt no publica un validador oficial, y algunos validadores online marcan como error la ausencia del blockquote o de una sección H2, aunque la propuesta considera ambos opcionales.
- Cualquiera puede registrar un nombre de paquete que aparezca en llms.txt y que ya no exista en el registro. Un agente con permiso para ejecutar comandos de instalación podría entonces descargar lo que esa persona publique.
- Ejecuta la comprobación en cada despliegue para detectar los errores que introduces tú, y de forma semanal para detectar enlaces y paquetes que se rompen aguas arriba sin que hayas cambiado nada.
¿Por qué un llms.txt desactualizado es un problema de cadena de suministro?
Un llms.txt desactualizado es un problema de cadena de suministro porque los agentes tratan llms.txt como material de referencia fiable y, a menudo, actúan en función de él sin intervención humana. Eso convierte un enlace obsoleto o un nombre de paquete incorrecto en una instrucción. Un lector humano que se topa con un enlace roto se rinde. Un agente con acceso a la shell puede ejecutar npm install sobre un nombre que ahora pertenece a otra persona.
El riesgo de cadena de suministro de llms.txt ya se ha demostrado. TechRadar informó sobre la investigación de Alon Hertz, que analizó 6.214 dominios activos y encontró 8.265 archivos llms.txt y llms-full.txt. Según su recuento, los archivos de 120 sitios distintos apuntaban a paquetes o dominios que nadie había registrado. Después se hizo con algunos de esos nombres libres y publicó paquetes que le avisaban cada vez que alguien los instalaba. Según el investigador, los sistemas de una empresa del Fortune 500 empezaron a conectarse en menos de una hora. El informe menciona a Claude, Codex y Hermes entre los agentes implicados.
Conviene ponerlo en perspectiva: fue un experimento controlado, no un registro de malware en circulación real. Aun así, es una exposición real de la cadena de suministro, y un job de CI la cierra con muy poco coste.
El control de acceso es otra cuestión. Bloquear los rastreadores de IA con robots.txt indica a los rastreadores qué partes de tu sitio pueden leer. Validar llms.txt controla si lo que leen sigue siendo correcto.
Qué comprobar al validar llms.txt
Un llms.txt válido tiene un H1 y enlaces con el formato correcto, y cada enlace y referencia a paquetes apunta a algo que existe y coincide con su descripción. La propuesta de llms.txt no publica un validador oficial. Las comprobaciones siguientes cubren lo que verifican los validadores online, además de comprobaciones en el registro de los paquetes que menciona el archivo.
Los enlaces responden
Cada URL de llms.txt debería devolver un estado 2xx tras seguir las redirecciones. Trata los 4xx, los 5xx y los fallos de DNS como errores. Un dominio que ya no resuelve es el equivalente, en dominios, a un paquete sin registrar: cualquiera puede comprarlo. Registra también las redirecciones. Una redirección que acaba en la raíz del sitio suele indicar que la página se eliminó. Envía primero HEAD y recurre a GET como alternativa, porque algunos servidores rechazan HEAD o lo gestionan mal.
Los enlaces devuelven lo que prometen
Una respuesta 200 demuestra que la URL responde, pero no que la página siga diciendo lo que promete el texto del enlace. Comprueba que el content-type sea HTML o Markdown, según lo esperado. Compara el <title> o el primer encabezado de la página con el texto del enlace, y presta atención a los soft 404, en los que se sirve una página de error con estado 200. Esta comprobación es imprecisa, así que ejecútala como revisión periódica y no como condición para que pase el build.
Los paquetes y versiones existen en el registro
Comprueba que existe en el registro cada paquete que tu llms.txt indica a un agente que instale. A mano, npm view <name> muestra lo que el registro contiene sobre ese paquete y devuelve un error si no existe. En un script, obtén el documento del paquete desde registry.npmjs.org. Un nombre que el registro no conoce es un nombre que otra persona puede reclamar. En el caso de versiones fijadas, confirma que la versión exacta figura en la lista de versiones publicadas del paquete. PyPI, crates.io y otros ecosistemas ofrecen consultas equivalentes.
La estructura Markdown es válida
La propuesta original de llms.txt solo hace obligatoria una sección: un H1 con el nombre del proyecto o del sitio. Aparte de un BOM (byte-order mark) opcional, el archivo debe empezar con # Name. La especificación actual de llms.txt establece que cada sección H2 contiene una lista Markdown y que cada elemento debe empezar con un enlace [name](url). Las notas tras dos puntos son opcionales. Haz que falle el build si falta el H1 o si la sintaxis de un enlace está rota. No lo hagas fallar por la ausencia de un blockquote o de un H2. Algunos validadores online los marcan igualmente, así que no trates esas advertencias como incumplimientos de la especificación.
El archivo no contradice la documentación actual
Compara el conjunto de URLs de llms.txt con tu sitemap.xml o con el manifiesto de rutas del build de tu documentación. Considera sospechosa cualquier URL que aparezca en llms.txt pero no en el sitemap. Haz lo mismo con los paquetes: cada nombre del archivo debería coincidir con el campo name de algún package.json que publiques, o con una dependencia que tengas intención de recomendar.
Un script de Node sin dependencias para validar llms.txt
Este script recibe una URL o una ruta local. Comprueba el H1 y todos los enlaces absolutos y, después, busca en el registro de npm cada paquete mencionado en un comando de instalación. Usa el fetch global de Node, por lo que funciona sin dependencias en una versión LTS actual, como 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);
Cómo funciona:
loadlee el archivo desde disco o por HTTP. Si falla la propia URL de llms.txt, el script se detiene inmediatamente.checkLinkprueba primero conHEADy reintenta conGETsi la peticiónHEADda error o devuelve un estado distinto de OK. Las redirecciones se registran como advertencias, y los fallos a nivel de red, como un dominio que no resuelve, se registran como fallos.checkPackagebusca cada nombre en el registro de npm y marca los nombres que no existen, así como las versiones exactas que nunca se publicaron.- Las peticiones se ejecutan de una en una a propósito, para mantener baja la carga sobre tus servidores y sobre el registro.
- Cualquier fallo hace que el script termine con código 1.
Tiene limitaciones conocidas. Se omiten los enlaces relativos. El texto que sigue a un comando de instalación en la misma línea puede producir falsos positivos. No se comprueban rangos de versiones, solo versiones exactas.
Una ejecución fallida tiene un aspecto similar a este (salida ilustrativa con nombres inventados):
↪ 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
Ejecuta la comprobación en CI en cada despliegue y cada semana
Ejecuta el validador de llms.txt por partida doble: una vez en cada despliegue, sobre el archivo que acabas de generar, y otra de forma programada, sobre el archivo publicado. La ejecución en el despliegue detecta los errores que introduces tú. La ejecución programada detecta enlaces que se han ido rompiendo con el tiempo (link rot) y paquetes retirados aguas arriba, aunque tu repositorio no haya cambiado.
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
El trigger schedule acepta una cadena cron estándar de cinco campos, y GitHub la interpreta en UTC salvo que añadas una clave timezone con un nombre de zona IANA. workflow_dispatch te permite lanzar una ejecución manualmente. Si el despliegue de tu documentación está en el mismo workflow, añade un paso antes del de publicación que apunte a la salida del build, por ejemplo node scripts/check-llms.mjs dist/llms.txt, para que una comprobación fallida bloquee la release. Mantén el job programado apuntando a la URL publicada.
Mantén llms.txt sincronizado generándolo
Si generas llms.txt a partir de la misma fuente que tu documentación, el archivo no puede listar una página o un paquete que la documentación ya no contenga. Genéralo en el mismo paso que renderiza la documentación, a partir de la misma lista de páginas y del mismo 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');
No edites nunca a mano el archivo generado. O bien haz commit solo del generador, o bien haz commit tanto del generador como de su salida y haz que CI falle cuando la regeneración produzca un diff. Una vez implantada la generación, el validador actúa como salvaguarda del generador en lugar de ser una tarea de limpieza que alguien tiene que acordarse de hacer.
Conclusión
Un archivo llms.txt solo resulta útil si todo lo que enumera sigue existiendo y sigue coincidiendo con su descripción, porque los agentes actúan en función de él sin comprobarlo. Añade scripts/check-llms.mjs a tu repositorio, ejecútalo una vez sobre tu archivo publicado, corrige lo que te indique y, después, haz commit del workflow para que el próximo enlace roto o nombre de paquete huérfano haga fallar un build antes de que un agente lo encuentre.
Preguntas frecuentes
¿Debe validarse llms-full.txt del mismo modo que llms.txt?
Sí en cuanto a enlaces y paquetes, y con más rigor en el caso de los paquetes. llms-full.txt incluye el contenido completo de la documentación, por lo que suele contener muchos más comandos de instalación y ejemplos de código que llms.txt, y cada uno de ellos es un nombre de paquete que un agente puede ejecutar. Las comprobaciones de estructura pueden ser más flexibles, porque el archivo consiste principalmente en contenido de páginas y no en una lista breve de enlaces. Apunta el mismo script a ambos archivos.
¿Por qué la comprobación de enlaces informa de errores 403 en páginas que cargan bien en el navegador?
Probablemente una protección antibots o un firewall de aplicaciones web (WAF) está bloqueando la petición automatizada. Algunas CDN y capas de seguridad rechazan las peticiones que no llevan cabeceras propias de un navegador o que proceden de rangos de IP de runners de CI. Añade tu runner de CI o el User-Agent del validador a la lista de permitidos de tu propio dominio de documentación, o envía una cabecera User-Agent descriptiva con fetch. No te limites a ignorar los 403, ya que los agentes pueden estar bloqueados de la misma manera.
¿Cómo compruebo en PyPI los nombres de paquetes de Python que aparecen en llms.txt?
Consulta la API JSON de PyPI en pypi.org/pypi/NAME/json. Para un proyecto existente, devuelve metadatos y un objeto releases indexado por cadena de versión, de modo que puedes confirmar tanto el nombre como cualquier versión fijada. Si la petición devuelve un 404, trata el nombre como no registrado. Extrae los nombres de los comandos pip install y uv add del mismo modo que el script gestiona npm install.
¿Que la comprobación en el registro sea satisfactoria significa que un paquete de llms.txt es seguro de instalar?
No. La comprobación confirma que el nombre existe, no que lo controlen las personas adecuadas. Puede que alguien ya haya reclamado un nombre después de que se abandonara el paquete original, y una errata en llms.txt puede apuntar a un paquete con un nombre parecido que sí existe. Para cada paquete que recomiendes, comprueba que el publicador y el enlace al repositorio corresponden a tu organización, y da preferencia a nombres con scope que controles.