Que fait skill-vet — Compétence d'agent ?
Analysez une compétence d'agent avant de l'installer, avec une seule commande. Utilisez-la chaque fois qu'une tâche implique une compétence provenant de l'extérieur de cette machine — un téléchargement depuis un marché, un dépôt lié par quelqu'un, un pack envoyé par un client — avant que le pack ne soit installé là où un agent le chargera.
La commande unique
skill-vet vet <dossier-du-pack> [--json] [--strict]
(Installé via pipx install skill-vet ou pip install skill-vet. Depuis une extraction source, la même commande est python3 src/skill_vet.py vet <dossier-du-pack> ….)
- L'argument est le dossier racine du pack (celui contenant SKILL.md). Un pack par exécution.
- Codes de sortie : 0 verdict PROPRE ou REVISION, 1 verdict BLOQUE (ou REVISION avec
--strict), 2 erreur d'entrée (dossier manquant, pas un dossier, mauvais arguments). En mode--json, chaque erreur — y compris les erreurs d'argument — est un objet JSON avec une cléerror. Jamais de traceback. - Lecture seule, et n'exécute rien. Vet lit le pack en tant que texte statique : il n'importe jamais, n'exécute jamais, ne lance jamais de shell ni n'évalue quoi que ce soit dans le pack. Son propre source n'importe aucun module réseau ou sous-processus. Sûr de pointer vers un pack que vous ne faites pas confiance.
Le modèle de danger (la totalité du produit)
Un pack est DANGEREUX seulement s'il fait une de ces trois choses — la règle permanente du propriétaire (30/10/2026), appliquée exactement :
- VOLE — lit les magasins de credentials, les profils de navigateur, les clés SSH ou les secrets d'environnement ET envoie ou copie les résultats à l'extérieur.
- ESPIONNE — capture écran, clavier, microphone, caméra ou historique de navigateur ET persiste ou envoie la capture, sans que les documents ne divulguent la capture.
- ENVOIE EN SECRET — appels réseau dont le contenu ou la destination est cachée : un payload encodé vers un endpoint non divulgué, le contenu d'un fichier local envoyé vers un endpoint non divulgué, un envoi combiné à des écritures hors du pack, ou tout envoi d'un pack dont les documents prétendent qu'il fonctionne hors ligne.
Un seul motif n'est jamais un verdict de danger. Chaque test a besoin de sa combinaison de preuves dans le même fichier, et la conclusion cite les deux moitiés avec fichier et ligne. Tout ce qui n'est pas une combinaison est un avertissement avec les preuves énoncées. Les combinaisons :
| Preuves dans un même fichier | Verdict |
|---|---|
| lecture de secret + envoi, ou lecture de secret + copie/déplacement vers un chemin extérieur | DANGER_STEALING (BLOQUE) |
| capture + (envoi ou persistance), type de capture non divulgué dans SKILL.md / README.md | DANGER_SPYING (BLOQUE) |
| envoi + payload encodé, endpoint non divulgué ou non littéral | DANGER_HIDDEN_SENDING (BLOQUE) |
| envoi + lecture de fichiers hors du pack, endpoint non divulgué | DANGER_HIDDEN_SENDING (BLOQUE) |
| envoi + écriture hors du pack, endpoint non divulgué | DANGER_HIDDEN_SENDING (BLOQUE) |
| envoi + documents prétendent hors ligne / local uniquement | DANGER_HIDDEN_SENDING (BLOQUE) |
| lecture de secret, pas d'envoi dans le fichier | SECRET_ACCESS (AVERTISSEMENT) |
| envoi vers un endpoint que les documents ne mentionnent jamais, sans combinaison de dissimulation | UNDISCLOSED_NETWORK (AVERTISSEMENT) |
| envoi sans URL littérale à vérifier | DYNAMIC_ENDPOINT (AVERTISSEMENT) |
| payload encodé vers un endpoint DIVULGUE | ENCODED_PAYLOAD (AVERTISSEMENT) |
| capture + persistance/envoi, capture divulguée dans les documents | DISCLOSED_CAPTURE (AVERTISSEMENT) |
| capture, sans persistance ni envoi | CAPTURE_CAPABILITY (AVERTISSEMENT) |
Jugements, énoncés ouvertement :
- Le vol n'a pas besoin de dissimulation : envoyer un credential vers un endpoint documenté reste du vol — le fait que les documents l'admettent ne le rend pas sûr. La divulgation ne sépare l'espionnage et l'envoi caché de leurs équivalents honnêtes.
- Un envoi déjà jugé comme la moitié de sortie d'une découverte VOL dans le même fichier n'est pas compté une deuxième fois comme envoi caché.
- La "divulgation" de capture est vérifiée par type par rapport à des mots simples dans SKILL.md / README.md (capture d'écran, frappe, microphone, webcam, historique de navigateur, …). Un pack qui capture et le dit est une capacité à accepter en connaissance de cause, pas un espion.
- Les éléments de sortie (egress) sont collectés par fichier sur toutes ses lignes, y compris les commentaires (conservateur : la preuve de capacité n'est pas écartée parce qu'elle se trouve près d'un commentaire). Les vérifications d'hygiène (shell=True, eval, téléchargement-exécution) ignorent les lignes de commentaire complet — un commentaire mentionnant un motif est de la documentation, pas du code.
La table des règles
| Règle | Sévérité | Base |
|---|---|---|
MISSING_SKILL_MD | ERREUR | Absence de SKILL.md à la racine du pack (un nom avec une casse différente génère SKILL_MD_CASE, AVERTISSEMENT, et est utilisé). |
FRONTMATTER_MISSING | ERREUR | SKILL.md n'a pas de bloc ---. |
FRONTMATTER_PARSE | ERREUR | Une ligne de frontmatter en dehors du sous-ensemble pris en charge (voir ci-dessous) ; la ligne est citée. |
REQUIRED_FIELD | ERREUR | name ou description manquant ou vide. |
NAME_DIR_MISMATCH | AVERTISSEMENT | Nom de frontmatter normalisé ≠ nom de dossier. |
NAME_FORMAT | AVERTISSEMENT | Le nom n'est pas une chaîne kebab-case en minuscules. |
BROAD_TOOL_GRANT | AVERTISSEMENT | Une entrée allowed-tools qui est *, un joker Tool(*) ou un simple Bash. Les formes restreintes comme Bash(npm run *) ne sont pas signalées. |
BROKEN_REFERENCE | ERREUR | Un fichier local référencé depuis SKILL.md (lien markdown ou chemin entouré de backticks) ou depuis un script (chemin littéral) n'existe pas. La résolution essaie le dossier du fichier référent, puis la racine du pack. |
REFERENCE_OUTSIDE_PACK | AVERTISSEMENT | Une référence qui se résout en dehors du dossier du pack. |
LARGE_FILE | AVERTISSEMENT | Un fichier de plus de 1 Mio (1 048 576 octets). |
BUNDLED_BINARY | AVERTISSEMENT | Un fichier binaire (extension binaire connue, ou un octet NUL dans les 8 premiers Kio). Les binaires ne peuvent pas être analysés statiquement. |
SYMLINK_OUT | INFO | Un lien symbolique dont la cible quitte le pack ; la cible n'est pas scannée. |
DANGER_STEALING | ERREUR, danger | Le tableau de combinaisons ci-dessus. |
DANGER_SPYING | ERREUR, danger | Le tableau de combinaisons ci-dessus. Types de capture : écran, clavier, audio, caméra, historique (une lecture de magasin d'historique plus une lecture de base de données dans un même fichier). |
DANGER_HIDDEN_SENDING | ERREUR, danger | Le tableau de combinaisons ci-dessus. |
SECRET_ACCESS | AVERTISSEMENT | Une lecture de magasin de credentials / secret d'environnement sans envoi dans le même fichier. Sources de secrets : .ssh / fichiers de clés, .aws, .gnupg, .netrc, fichiers de credentials stockés, magasins et dossiers de profil de navigateur, fichiers .env, accès à l'environnement nommant un secret (TOKEN / SECRET / PASSWORD / API_KEY / …), lectures de tout l'environnement. |
UNDISCLOSED_NETWORK | AVERTISSEMENT | Un appel réseau dont l'hôte de l'endpoint n'apparaît ni dans SKILL.md ni dans README.md. Un hôte est considéré comme divulgué quand lui — ou un domaine parent de lui — apparaît comme URL dans les documents. |
DYNAMIC_ENDPOINT | AVERTISSEMENT | Un appel réseau sans URL littérale nulle part dans le fichier. |
ENCODED_PAYLOAD | AVERTISSEMENT | La sortie d'un encodeur atteint un envoi vers un endpoint divulgué. "Atteint" est jugé par : l'appel à l'encodeur dans le segment d'appel de l'envoi, ou une variable assignée depuis un encodeur utilisée dans le segment. |
DISCLOSED_CAPTURE | AVERTISSEMENT | Capture + persistance/envoi, divulguée par type. |
CAPTURE_CAPABILITY | AVERTISSEMENT | Une API de capture sans persistance ni envoi dans le fichier. |
HYGIENE_SHELL_TRUE | AVERTISSEMENT | shell=True / shell: true sur une ligne de code. |
HYGIENE_DYNAMIC_EXEC | AVERTISSEMENT | eval( / exec( / os.system( / os.popen( / __import__( sur une ligne de code. |
HYGIENE_DOWNLOAD_EXEC | AVERTISSEMENT | Un téléchargement redirigé vers un shell (`curl … |
HYGIENE_OBFUSCATION | AVERTISSEMENT | Un blob base64 (120+ caractères, mélange de casse avec un chiffre ou +/), un blob hex (120+ caractères), ou une série de 8+ séquences d'échappement \xNN, dans un fichier texte. Une constatation par type par fichier. |
CONCEALED_UNICODE | AVERTISSEMENT | Caractères invisibles (taille nulle, jointure de mots, trait d'union souple, contrôles bidi, caractères de balise Unicode) dans un fichier texte ; nombre, première ligne et noms de caractères cités. Un BOM en tête de fichier est une signature, pas une dissimulation, et est ignoré. |
CONCEALED_BLANK_RUN | AVERTISSEMENT | 50+ lignes vides consécutives dans un fichier markdown avec du contenu après elles. |
HIDDEN_COMMENT | AVERTISSEMENT | Un commentaire HTML de 120+ caractères dans un fichier markdown. |
WRITES_OUTSIDE_PACK | AVERTISSEMENT | Un script écrit sur un chemin absolu, home ou système (/tmp, /etc, ~/, $HOME, …). |
INSTALL_HOOK | AVERTISSEMENT | Un fichier qui s'exécute au moment de l'installation : setup.py, install.* / setup.sh, un fichier .pth, ou un package.json avec un hook preinstall/install/postinstall. |
Verdicts : BLOQUE = au moins une constatation de danger → sortie 1. REVISION = pas de danger, mais au moins une ERREUR ou un AVERTISSEMENT → sortie 0 (1 avec --strict). PROPRE = aucune ERREUR ni AVERTISSEMENT (INFO peut subsister) → sortie 0. REVISION sort avec 0 intentionnellement : REVISION signifie "un humain devrait lire ceci", et les scripts qui bloquent les installations doivent échouer sur le danger, pas sur les avertissements — --strict existe pour ceux qui pensent le contraire.
Le sous-ensemble de frontmatter
L'analyseur lit la forme de frontmatter que les packs de compétences livrent réellement : cle: valeur scalaires (guillemets retirés), cle: [a, b] listes en ligne, cle: suivi de - item listes en bloc, et les lignes de continuation indentées repliées dans le scalaire précédent. Il n'est délibérément pas un analyseur YAML complet. Une ligne en dehors du sous-ensemble est une erreur FRONTMATTER_PARSE avec la ligne citée — vet signale ce qu'il ne peut pas lire plutôt que de deviner.
Portée de l'analyse
- Chaque fichier dans le pack est inventorié avec sa taille. Les répertoires
.gitet__pycache__sont ignorés. - L'analyse de texte couvre les scripts (par extension ou shebang), le markdown et autres fichiers texte, jusqu'à 2 Mio par fichier. Les fichiers binaires et les cibles de liens symboliques hors du pack sont inventoriés, jamais analysés.
- Les références sont collectées à partir de SKILL.md et des fichiers de script. Les références à l'intérieur d'autres fichiers markdown (un lien references/*.md pointant plus loin) ne sont pas suivies dans v1.0.0.
- La divulgation est jugée d'après le SKILL.md et le README.md de la racine du pack uniquement — les deux documents qu'une personne installant le pack est censée avoir lus.
Schéma JSON (--json)
Un objet :
tool,version,commandpack:dir(tel que passé),dirname,name(nom de frontmatter ou null),skill_filefiles[]:path,bytes,kind(script|markdown|text|binary|other),binary,scanned,symlink_outfindings[]:rule,severity(ERROR|WARN|INFO),danger(bool),file(ou null),line(ou null),message,fixsummary:findings,errors,warnings,info,danger— des comptes qui correspondent exactement àfindingsverdict(CLEAN|REVIEW|BLOCKED),verdict_rule,limits
Les constatations sont triées de manière déterministe : danger d'abord, puis sévérité, fichier, ligne, règle — le même pack produit toujours le même rapport.
Objectifs délibérément non visés (honnêteté)
- Il n'exécute jamais rien. Aucun script n'est exécuté, importé, compilé, ni analysé par le shell ; aucun payload n'est décodé (un blob encodé est signalé, pas ouvert).
- Les motifs statiques sont des preuves, pas une certitude. Un pack qui assemble son comportement à l'exécution à partir de pièces anodines, qui répartit une combinaison sur deux fichiers (vet juge les combinaisons par fichier), ou qui écrit son ingénierie sociale en phrases simples et honnêtes peut passer. Un verdict PROPRE signifie qu'aucun motif ne s'est déclenché — le README le dit, et chaque rapport aussi.
- Les binaires sont hors de portée. Un binaire intégré est signalé précisément parce que vet ne peut rien en dire.
- Pas de télémétrie, pas de réseau, pas d'écritures. Stdlib uniquement ; le source ne contient aucun code réseau ou sous-processus (un test analyse ses imports et le prouve).
- Les tables de motifs sont un instantané. De nouvelles bibliothèques de capture, de nouveaux magasins de credentials et de nouvelles API client apparaissent constamment ; les tables dans le source sont datées par la version, pas considérées comme complètes.