skill-vet — skill para agentes
Evalúa un skill pack de agente antes de instalarlo, con un solo comando. Úsalo siempre que una tarea involucre un skill de fuera de esta máquina — una descarga del marketplace, un repositorio que alguien enlazó, un pack que un cliente envió — antes de que el pack se instale en cualquier lugar donde un agente lo cargue.
El comando único
skill-vet vet <carpeta-del-skill-pack> [--json] [--strict]
(Instalado mediante pipx install skill-vet o pip install skill-vet. Desde un checkout de la fuente, el mismo comando es python3 src/skill_vet.py vet <carpeta-del-skill-pack> ….)
- El argumento es la carpeta raíz del pack (la que contiene SKILL.md). Un pack por ejecución.
- Códigos de salida: 0 veredicto CLEAN o REVIEW, 1 veredicto BLOCKED (o REVIEW con
--strict), 2 error de entrada (carpeta faltante, no es una carpeta, argumentos incorrectos). En modo--json, cada error — incluyendo errores de argumento — es un objeto JSON con una claveerror. Nunca un traceback. - Solo lectura y no ejecuta nada. Vet lee el pack como texto estático: nunca importa, ejecuta, llama a subprocesos ni evalúa nada del pack. Su propio código fuente no importa ningún módulo de red o subprocesos. Es seguro apuntarlo a un pack que desconfíes activamente.
El modelo de peligro (el producto completo)
Un pack es PELIGROSO solo si hace una de tres cosas — la regla permanente del mantenedor (2026-10-30), aplicada exactamente:
- ROBA — lee almacenes de credenciales, perfiles de navegador, claves SSH o secretos de entorno Y envía o copia los resultados fuera.
- ESPÍA — captura pantalla, teclado, micrófono, cámara o historial del navegador Y persiste o envía la captura, sin que la documentación la revele.
- ENVÍA EN SECRETO — llamadas de red cuyo payload o destino están ocultos: un payload codificado a un endpoint no revelado, contenido de archivos locales enviados a un endpoint no revelado, un envío combinado con escrituras fuera del pack, o cualquier envío desde un pack cuya documentación afirma que funciona sin conexión.
Un patrón aislado nunca es un veredicto de peligro. Cada prueba necesita su combinación de evidencia en el mismo archivo, y el hallazgo cita ambas mitades con archivo y línea. Cualquier cosa que no sea una combinación es una advertencia con la evidencia enunciada. Las combinaciones:
| Evidencia en un archivo | Veredicto |
|---|---|
| lectura de secreto + envío, o lectura de secreto + copia/mover a una ruta externa | DANGER_STEALING (BLOQUEADO) |
| captura + (envío o persistencia), tipo de captura no revelado en SKILL.md / README.md | DANGER_SPYING (BLOQUEADO) |
| envío + payload codificado, endpoint no revelado o no literal | DANGER_HIDDEN_SENDING (BLOQUEADO) |
| envío + lectura de archivos fuera del pack, endpoint no revelado | DANGER_HIDDEN_SENDING (BLOQUEADO) |
| envío + escritura fuera del pack, endpoint no revelado | DANGER_HIDDEN_SENDING (BLOQUEADO) |
| envío + documentación afirma offline / solo local | DANGER_HIDDEN_SENDING (BLOQUEADO) |
| lectura de secreto, sin egress en el archivo | SECRET_ACCESS (AVISO) |
| envío a un endpoint que la documentación nunca menciona, sin combinación de ocultamiento | UNDISCLOSED_NETWORK (AVISO) |
| envío sin URL literal para verificar | DYNAMIC_ENDPOINT (AVISO) |
| payload codificado a un endpoint REVELADO | ENCODED_PAYLOAD (AVISO) |
| captura + persistencia/envío, captura revelada en la documentación | DISCLOSED_CAPTURE (AVISO) |
| captura, sin persistencia ni envío | CAPTURE_CAPABILITY (AVISO) |
Decisiones de criterio, enunciadas abiertamente:
- Robar no necesita ocultamiento: enviar una credencial a un endpoint documentado sigue siendo robo — que la documentación lo admita no lo hace seguro. La revelación solo separa el espionaje y el envío oculto de sus contrapartes honestas.
- Un envío ya juzgado como la mitad de egress de un hallazgo de ROBO en el mismo archivo no se cuenta una segunda vez como envío oculto.
- La "revelación" de captura se verifica por tipo contra palabras simples en SKILL.md / README.md (captura de pantalla, pulsación de tecla, micrófono, cámara web, historial del navegador, …). Un pack que captura y lo dice es una capacidad que se acepta con conocimiento, no un espía.
- Los ingredientes de egress se recopilan por archivo en todas sus líneas, incluidos comentarios (conservador: la evidencia de capacidad no se descarta porque esté cerca de un comentario). Las comprobaciones de higiene (
shell=True,eval, descarga+ejecución) omiten líneas de comentario completas — un comentario que menciona un patrón es documentación, no código.
La tabla de reglas
| Regla | Gravedad | Base |
|---|---|---|
MISSING_SKILL_MD | ERROR | No hay SKILL.md en la raíz del pack (un nombre con mayúsculas distintas genera SKILL_MD_CASE, AVISO, y se usa). |
FRONTMATTER_MISSING | ERROR | SKILL.md no tiene bloque ---. |
FRONTMATTER_PARSE | ERROR | Una línea de frontmatter fuera del subconjunto soportado (ver abajo); se cita la línea. |
REQUIRED_FIELD | ERROR | name o description faltante o vacío. |
NAME_DIR_MISMATCH | AVISO | El nombre normalizado del frontmatter ≠ nombre de la carpeta. |
NAME_FORMAT | AVISO | El nombre no es un slug en kebab-case minúsculas. |
BROAD_TOOL_GRANT | AVISO | Una entrada de allowed-tools que es *, un comodín Tool(*), o un Bash simple. Formas con ámbito como Bash(npm run *) no se marcan. |
BROKEN_REFERENCE | ERROR | Un archivo local referenciado desde SKILL.md (enlace markdown o ruta entre backticks) o desde un script (ruta literal) no existe. La resolución prueba la carpeta del archivo referenciador, luego la raíz del pack. |
REFERENCE_OUTSIDE_PACK | AVISO | Una referencia que se resuelve fuera de la carpeta del pack. |
LARGE_FILE | AVISO | Un archivo de más de 1 MiB (1 048 576 bytes). |
BUNDLED_BINARY | AVISO | Un archivo binario (extensión binaria conocida, o un byte NUL en los primeros 8 KiB). Los binarios no se pueden evaluar estáticamente. |
SYMLINK_OUT | INFO | Un enlace simbólico cuyo objetivo sale del pack; el objetivo no se escanea. |
DANGER_STEALING | ERROR, peligro | La tabla de combinaciones anterior. |
DANGER_SPYING | ERROR, peligro | La tabla de combinaciones anterior. Tipos de captura: pantalla, teclado, audio, cámara, historial (una lectura de almacén de historial más una lectura de base de datos en un archivo). |
DANGER_HIDDEN_SENDING | ERROR, peligro | La tabla de combinaciones anterior. |
SECRET_ACCESS | AVISO | Lectura de almacén de credenciales / secreto de entorno sin egress en el mismo archivo. Fuentes de secretos: archivos .ssh / de clave, .aws, .gnupg, .netrc, archivos de credenciales almacenadas, almacenes y carpetas de perfil del navegador, archivos .env, acceso a entorno que nombre un secreto (TOKEN / SECRET / PASSWORD / API_KEY / …), lecturas de entorno completo. |
UNDISCLOSED_NETWORK | AVISO | Una llamada de red cuyo host de endpoint no aparece ni en SKILL.md ni en README.md. Un host cuenta como revelado cuando él — o un dominio padre — aparece como URL en la documentación. |
DYNAMIC_ENDPOINT | AVISO | Una llamada de red sin URL literal en ningún lugar del archivo. |
ENCODED_PAYLOAD | AVISO | La salida de un codificador llega a un envío a un endpoint revelado. "Llega" se juzga por: la llamada al codificador dentro del segmento de llamada del envío, o una variable asignada desde un codificador usado en el segmento. |
DISCLOSED_CAPTURE | AVISO | Captura + persistencia/envío, revelada por tipo. |
CAPTURE_CAPABILITY | AVISO | Una API de captura sin persistencia ni envío en el archivo. |
HYGIENE_SHELL_TRUE | AVISO | shell=True / shell: true en una línea de código. |
HYGIENE_DYNAMIC_EXEC | AVISO | eval( / exec( / os.system( / os.popen( / __import__( en una línea de código. |
HYGIENE_DOWNLOAD_EXEC | AVISO | Una descarga canalizada a un shell (`curl … |
HYGIENE_OBFUSCATION | AVISO | Un blob base64 (120+ caracteres, mayúsculas y minúsculas con un dígito o +/), un blob hexadecimal (120+ caracteres), o una secuencia de 8+ escapes \xNN, en un archivo de texto. Un hallazgo por tipo por archivo. |
CONCEALED_UNICODE | AVISO | Caracteres invisibles (ancho cero, unión de palabras, guión suave, controles bidi, caracteres de etiqueta Unicode) en un archivo de texto; se citan el recuento, la primera línea y los nombres de los caracteres. Un BOM inicial es una firma, no ocultamiento, y se ignora. |
CONCEALED_BLANK_RUN | AVISO | 50+ líneas en blanco consecutivas en un archivo markdown con contenido después de ellas. |
HIDDEN_COMMENT | AVISO | Un comentario HTML de 120+ caracteres en un archivo markdown. |
WRITES_OUTSIDE_PACK | AVISO | Un script escribe en una ruta absoluta, del hogar o del sistema (/tmp, /etc, ~/, $HOME, …). |
INSTALL_HOOK | AVISO | Un archivo que se ejecuta en tiempo de instalación: setup.py, install.* / setup.sh, un archivo .pth, o un package.json con un hook preinstall/install/postinstall. |
Veredictos: BLOQUEADO = al menos un hallazgo de peligro → salida 1. REVISAR = sin peligro, pero al menos un ERROR o AVISO → salida 0 (1 con --strict). LIMPIO = sin ERROR ni AVISO (pueden quedar INFO) → salida 0. REVISAR sale con 0 a propósito: REVISAR significa "un humano debería leer esto", y los scripts que controlan instalaciones deberían fallar por peligro, no por advertencias — --strict existe para quienes discrepan.
El subconjunto de frontmatter
El analizador lee la forma de frontmatter que los skill packs realmente envían: escalares clave: valor (comillas eliminadas), listas en línea clave: [a, b], listas en bloque clave: seguidas de - elemento, y líneas de continuación indentadas plegadas en el escalar anterior. No es deliberadamente un analizador YAML completo. Una línea fuera del subconjunto es un error FRONTMATTER_PARSE con la línea citada — vet informa lo que no puede leer en lugar de adivinar.
Alcance del escaneo
- Cada archivo en el pack se inventaria con su tamaño. Se omiten los directorios
.gity__pycache__. - El escaneo de texto cubre scripts (por extensión o shebang), markdown y otros archivos de texto, hasta 2 MiB por archivo. Los archivos binarios y los objetivos de enlaces simbólicos fuera del pack se inventarian, nunca se escanean.
- Las referencias se recopilan de SKILL.md y de archivos de script. Las referencias dentro de otros archivos markdown (un archivo references/*.md que enlaza hacia adelante) no se siguen en v1.0.0.
- La revelación se juzga solo contra SKILL.md y README.md de la raíz del pack — los dos documentos que se espera que una persona que instale el pack haya leído.
Esquema JSON (--json)
Un objeto:
tool,version,commandpack:dir(tal como se pasó),dirname,name(nombre del frontmatter o null),skill_filefiles[]:path,bytes,kind(script|markdown|text|binary|other),binary,scanned,symlink_outfindings[]:rule,severity(ERROR|WARN|INFO),danger(bool),file(o null),line(o null),message,fixsummary:findings,errors,warnings,info,danger— recuentos que coinciden exactamente confindingsverdict(CLEAN|REVIEW|BLOCKED),verdict_rule,limits
Los hallazgos se ordenan de forma determinista: primero peligro, luego gravedad, archivo, línea, regla — el mismo pack siempre produce el mismo informe.
No objetivos deliberados (honestidad)
- Nunca ejecuta nada. Ningún script se ejecuta, importa, compila o analiza sintácticamente; ningún payload se descodifica (un blob codificado se informa, no se abre).
- Los patrones estáticos son evidencia, no prueba. Un pack que ensambla su comportamiento en tiempo de ejecución a partir de piezas inocuas, divide una combinación entre dos archivos (vet juzga combinaciones por archivo), o escribe su ingeniería social en oraciones claras y honestas puede pasar. Un veredicto LIMPIO significa que ningún patrón se activó — el README lo dice, y también lo dice cada informe.
- Los binarios están fuera de alcance. Un binario empaquetado se marca precisamente porque vet no puede decir nada sobre él.
- Sin telemetría, sin red, sin escrituras. Solo stdlib; el código fuente no contiene código de red o subprocesos en absoluto (una prueba analiza sus importaciones y lo demuestra).
- Las tablas de patrones son una instantánea. Aparecen constantemente nuevas bibliotecas de captura, nuevos almacenes de credenciales y nuevas API cliente; las tablas en el código fuente están fechadas por la versión, no se consideran completas.