Comment valider votre fichier llms.txt
Validez les liens, noms et versions de paquets et la structure Markdown de llms.txt avec un script Node.js sans dépendances, puis automatisez les contrôles dans CI.
Pour valider un fichier llms.txt, vérifiez quatre points : il commence par un titre H1, chaque lien mène bien à la page que décrit son texte, chaque nom de paquet et chaque version existent sur le registre, et rien dans le fichier ne contredit votre documentation actuelle.
Un outil de documentation a probablement généré votre llms.txt lors d’un sprint de lancement, et personne ne l’a rouvert depuis. Entre-temps, des pages ont été renommées et des paquets dépréciés, mais les agents de code continuent de lire ce fichier.
Si vous avez besoin d’un rappel sur le format, commencez par notre présentation de llms.txt. Cet article fait l’impasse sur les bases. Il passe en revue les vérifications à effectuer, propose un script Node sans aucune dépendance et montre comment l’exécuter en CI afin de détecter les problèmes avant qu’un agent n’agisse en conséquence.
Points clés
- La proposition initiale de llms.txt n’impose qu’un seul élément : un H1 indiquant le nom du projet ou du site. Le résumé en citation (blockquote), les paragraphes de détail et les sections de liens en H2 sont tous facultatifs.
- La proposition llms.txt ne publie aucun validateur officiel, et certains outils de vérification en ligne signalent l’absence de blockquote ou de section H2 alors que la proposition les considère comme facultatifs.
- Un nom de paquet présent dans llms.txt qui n’existe plus sur le registre peut être enregistré par n’importe qui. Un agent autorisé à exécuter des commandes d’installation risque alors de récupérer tout ce que cette personne publie.
- Exécutez la vérification à chaque déploiement pour détecter les erreurs que vous introduisez, et chaque semaine pour repérer les liens et les paquets qui cassent en amont sans aucune modification de votre part.
Pourquoi un llms.txt obsolète pose-t-il un problème de chaîne d’approvisionnement logicielle ?
Un llms.txt obsolète pose un problème de chaîne d’approvisionnement parce que les agents le considèrent comme une source de référence fiable et agissent souvent en fonction de son contenu sans intervention humaine. Un lien périmé ou un nom de paquet erroné devient alors une instruction. Un lecteur humain qui tombe sur un lien mort abandonne. Un agent disposant d’un accès au shell peut, lui, lancer npm install sur un nom désormais détenu par quelqu’un d’autre.
Ce risque lié à llms.txt a été démontré. TechRadar a rendu compte des travaux d’Alon Hertz, qui a analysé 6 214 domaines actifs et y a trouvé 8 265 fichiers llms.txt et llms-full.txt. Selon ses calculs, les fichiers de 120 sites distincts pointaient vers des paquets ou des domaines que personne n’avait enregistrés. Il a ensuite revendiqué certains de ces noms disponibles et publié des paquets qui le prévenaient à chaque installation. D’après le chercheur, les systèmes d’une entreprise du Fortune 500 ont commencé à le contacter en moins d’une heure. Le rapport cite Claude, Codex et Hermes parmi les agents concernés.
Gardez le sens de la mesure : il s’agissait d’une expérience contrôlée, et non d’un cas avéré de logiciel malveillant en circulation. Cela reste néanmoins une véritable exposition de la chaîne d’approvisionnement, qu’un job de CI permet de corriger à moindre coût.
Le contrôle d’accès est une question distincte. Bloquer les robots d’IA avec robots.txt indique aux robots quelles parties de votre site ils peuvent lire. Valider llms.txt permet de vérifier que ce qu’ils lisent est toujours exact.
Que vérifier lors de la validation de llms.txt
Un llms.txt valide comporte un H1 et des liens correctement formatés, et chaque lien et chaque référence de paquet pointent vers une ressource qui existe et correspond à sa description. La proposition llms.txt ne publie aucun validateur officiel. Les vérifications ci-dessous couvrent ce que testent les outils en ligne, avec en plus des contrôles sur le registre pour les paquets mentionnés dans le fichier.
Les liens répondent
Chaque URL de llms.txt doit renvoyer un code 2xx une fois les redirections suivies. Considérez les codes 4xx, 5xx et les échecs DNS comme des erreurs. Un domaine qui ne se résout plus est l’équivalent, côté domaines, d’un paquet non enregistré : n’importe qui peut l’acheter. Journalisez également les redirections. Une redirection qui aboutit à la racine du site signifie généralement que la page a été supprimée. Envoyez d’abord une requête HEAD, puis repliez-vous sur GET, car certains serveurs refusent ou gèrent mal HEAD.
Les liens renvoient bien ce qu’ils annoncent
Une réponse 200 prouve qu’une URL répond. Elle ne prouve pas que la page contient toujours ce que promet le texte du lien. Vérifiez que le content-type correspond bien à du HTML ou du Markdown, selon ce qui est attendu. Comparez le <title> ou le premier titre de la page avec le texte du lien, et méfiez-vous des « soft 404 », c’est-à-dire des pages d’erreur servies avec un statut 200. Cette vérification est approximative : exécutez-la lors d’une revue périodique plutôt que comme condition bloquante du build.
Les paquets et les versions existent sur le registre
Vérifiez sur le registre l’existence de chaque paquet que votre llms.txt demande à un agent d’installer. Manuellement, npm view <name> affiche ce que le registre contient pour ce paquet et renvoie une erreur si celui-ci n’existe pas. Dans un script, récupérez le document du paquet sur registry.npmjs.org. Un nom inconnu du registre est un nom que quelqu’un d’autre peut s’approprier. Pour les versions épinglées, confirmez que la version exacte figure dans la liste des versions publiées du paquet. PyPI, crates.io et les autres écosystèmes proposent des recherches équivalentes.
La structure Markdown est valide
La proposition initiale de llms.txt ne rend obligatoire qu’une seule section : un H1 indiquant le nom du projet ou du site. Hormis un éventuel BOM (byte-order mark), le fichier doit commencer par # Name. La spécification llms.txt actuelle précise que chaque section H2 contient une liste Markdown dont chaque élément doit commencer par un lien [name](url). Les notes placées après un deux-points sont facultatives. Faites échouer le build en cas d’absence de H1 ou de syntaxe de lien incorrecte. Ne le faites pas échouer pour une blockquote ou une section H2 manquante. Certains outils en ligne les signalent malgré tout : ne considérez pas ces avertissements comme des violations de la spécification.
Le fichier ne contredit pas la documentation actuelle
Comparez l’ensemble des URL de llms.txt avec votre sitemap.xml ou le manifeste des routes généré par votre build de documentation. Considérez comme suspecte toute URL présente dans llms.txt mais absente du sitemap. Procédez de même pour les paquets : chaque nom figurant dans le fichier doit correspondre au champ name d’un package.json que vous publiez, ou à une dépendance que vous souhaitez recommander.
Un script Node sans dépendance pour valider llms.txt
Ce script accepte une URL ou un chemin local. Il vérifie le H1 et chaque lien absolu, puis recherche sur le registre npm chaque paquet cité dans une commande d’installation. Il utilise le fetch global de Node et fonctionne donc sans aucune dépendance sur une version LTS actuelle comme 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);
Fonctionnement :
loadlit le fichier depuis le disque ou via HTTP. Si l’URL du llms.txt elle-même échoue, le script s’arrête immédiatement.checkLinktente d’abordHEAD, puis réessaie avecGETsi la requêteHEADéchoue ou renvoie un statut non OK. Les redirections sont journalisées sous forme d’avertissements, et les échecs au niveau réseau, comme un domaine impossible à résoudre, sont enregistrés comme des échecs.checkPackagerecherche chaque nom sur le registre npm et signale les noms inexistants, ainsi que les versions exactes qui n’ont jamais été publiées.- Les requêtes sont volontairement exécutées une par une, afin de limiter la charge sur vos serveurs et sur le registre.
- Le moindre échec fait sortir le script avec le code 1.
Le script présente des limites connues. Les liens relatifs sont ignorés. Du texte qui suit une commande d’installation sur la même ligne peut produire des faux positifs. Les plages de versions ne sont pas vérifiées, seules les versions exactes le sont.
Une exécution en échec ressemble à ceci (sortie fictive avec des noms inventés) :
↪ 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
Exécuter la vérification en CI à chaque déploiement et chaque semaine
Exécutez le validateur llms.txt à deux occasions : à chaque déploiement, sur le fichier que vous venez de générer, et de façon planifiée, sur le fichier en production. L’exécution au déploiement détecte les erreurs que vous introduisez. L’exécution planifiée détecte la dégradation des liens (link rot) et les paquets dépubliés en amont, alors même que votre dépôt n’a pas changé.
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
Le déclencheur schedule accepte une expression cron standard à cinq champs, que GitHub interprète en UTC, sauf si vous ajoutez une clé timezone avec un nom de fuseau IANA. workflow_dispatch vous permet de lancer une exécution manuellement. Si le déploiement de votre documentation se trouve dans le même workflow, ajoutez avant l’étape de publication une étape qui cible le résultat du build, par exemple node scripts/check-llms.mjs dist/llms.txt, afin qu’une vérification en échec bloque la mise en production. Laissez le job planifié pointer vers l’URL en production.
Maintenir llms.txt à jour en le générant
Si vous générez llms.txt à partir de la même source que votre documentation, le fichier ne peut pas référencer une page ou un paquet que la documentation ne contient plus. Générez-le lors de l’étape qui produit la documentation, à partir de la même liste de pages et du même 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');
Ne modifiez jamais le fichier généré à la main. Soit vous ne versionnez que le générateur, soit vous versionnez le générateur et sa sortie, et la CI échoue lorsque la régénération produit un diff. Une fois la génération en place, le validateur sert de garde-fou pour le générateur plutôt que de tâche de nettoyage dont quelqu’un doit se souvenir.
Conclusion
Un fichier llms.txt n’est utile que si tout ce qu’il référence existe encore et correspond toujours à sa description, car les agents agissent sur la base de son contenu sans vérification. Ajoutez scripts/check-llms.mjs à votre dépôt, exécutez-le une première fois sur votre fichier en production, corrigez tout ce qu’il signale, puis versionnez le workflow afin que le prochain lien cassé ou nom de paquet orphelin fasse échouer un build avant qu’un agent ne tombe dessus.
FAQ
Faut-il valider llms-full.txt de la même manière que llms.txt ?
Oui pour les liens et les paquets, et de façon encore plus stricte pour les paquets. llms-full.txt intègre l'intégralité du contenu de la documentation : il contient donc généralement bien plus de commandes d'installation et d'exemples de code que llms.txt, et chacun d'eux est un nom de paquet qu'un agent peut exécuter. Les vérifications de structure peuvent être plus souples, car le fichier est essentiellement constitué de contenu de pages plutôt que d'une courte liste de liens. Pointez le même script vers les deux fichiers.
Pourquoi la vérification des liens signale-t-elle des erreurs 403 pour des pages qui s'affichent correctement dans un navigateur ?
Une protection anti-bots ou un pare-feu applicatif web (WAF) bloque probablement la requête automatisée. Certains CDN et couches de sécurité rejettent les requêtes dépourvues d'en-têtes de type navigateur ou provenant des plages d'adresses IP des runners de CI. Ajoutez votre runner de CI ou le User-Agent du vérificateur à la liste d'autorisation de votre domaine de documentation, ou envoyez un en-tête User-Agent explicite avec fetch. N'ignorez pas simplement les erreurs 403 : les agents peuvent être bloqués de la même manière.
Comment vérifier sur PyPI les noms de paquets Python mentionnés dans llms.txt ?
Interrogez l'API JSON de PyPI à l'adresse pypi.org/pypi/NAME/json. Pour un projet existant, elle renvoie des métadonnées ainsi qu'un objet releases indexé par chaîne de version, ce qui vous permet de confirmer à la fois le nom et toute version épinglée. Si la requête renvoie une erreur 404, considérez le nom comme non enregistré. Extrayez les noms des commandes pip install et uv add de la même façon que le script traite npm install.
Un contrôle réussi sur le registre signifie-t-il qu'un paquet mentionné dans llms.txt peut être installé sans risque ?
Non. Le contrôle confirme que le nom existe, pas qu'il appartient aux bonnes personnes. Quelqu'un a pu s'approprier un nom après l'abandon du paquet d'origine, et une faute de frappe dans llms.txt peut pointer vers un paquet d'apparence similaire qui, lui, existe bel et bien. Pour chaque paquet que vous recommandez, vérifiez que l'éditeur et le lien vers le dépôt correspondent à votre organisation, et privilégiez les noms scopés que vous contrôlez.