Thot
Thot
Un asistente de código en terminal que ya conoce tu repositorio — y el repositorio donde viven Hermes Agent y Prime Agent, enteros.
Un agente conversacional descubre un proyecto abriendo archivos con el modelo: lento, parcial, y hay que pagarlo de nuevo en cada sesión. Thot calcula la misma imagen mediante AST y grafo de llamadas — completa, instantánea, gratuita — y solo le da al modelo lo que importa.
Los tres programas
Este repositorio contiene tres, no uno. Ninguno es una reescritura de otro: cada uno está en su idioma, con sus herramientas, y Thot los conecta entre sí.
Qué es | Dónde | |
thot | auditoría determinista, mapa del código, memoria de veredictos |
|
hermes | el agente: herramientas, pasarelas, plugins, cron, ACP |
|
prime | el agente de código: proveedores de modelos, TUI, RLM |
|
thot # la session d'audit
thot hermes # Hermes, arguments transmis tels quels
thot prime # Prime, pareil
thot fusion status # ce qui est présent, prêt, et branchéLa conexión no es decorativa. thot fusion wire declara el servidor MCP
de Thot en los dos agentes: ganan code_map, find_symbol,
callers, audit, skills y skill — el mapa completo del repositorio, calculado
fuera del modelo, en lugar de redescubrirlo archivo por archivo. Es el refuerzo
mutuo: Thot sabe sin preguntar, Hermes y Prime actúan.
Los dos agentes no se alcanzan de la misma manera, y pretender lo
contrario solo conectaba a uno de los dos. Hermes inicia el servidor él mismo y
le habla por el conducto que ha abierto. Prime solo acepta HTTP — su
mcp-manager descarta cualquier entrada cuyo tipo no sea http, y su
runtime no tiene ningún transporte por conducto — por lo que su lado requiere un servidor
en marcha:
thot fusion wire # écrit les deux branchements, chacun dans sa forme
thot mcp serve --http # ce que Prime interroge : boucle locale, jeton bearer
thot mcp service --install # et pour que ce soit encore vrai demainLa tercera línea existe porque las dos primeras solo duran una
sesión. fusion wire escribe http://127.0.0.1:8787/mcp en la
configuración de Prime, y esa dirección es verdadera mientras un terminal siga
abierto: en el primer reinicio, el archivo promete un servidor que ya nadie
atiende. thot mcp service --install escribe la unidad que lo relanza —
KeepAlive en macOS, Restart=always bajo systemd — y no la carga:
Thot imprime el comando y te deja lanzarlo, porque una herramienta
que registra agentes en segundo plano sin decirlo es una herramienta en la que se deja
de creer. thot doctor plantea luego la única pregunta que importa —
¿responde la dirección ahora? — y no si el archivo existe.
fusion wire instala también, en Prime, el paquete de método que Thot entrega
para él: una entrada de configuración no basta, Prime solo alcanza un
servidor MCP a través de una clase que lo nombre. Y deposita el token en
auth.json sin tocar las credenciales del modelo — sin token, la
conexión falla antes de abrirse.
Cada agente conserva su configuración. thot fusion unwire deshace todo, y el
settings.json de Prime se guarda antes de la primera modificación.
thot fusion status mide lo que funciona, no lo que está escrito:
Hermes instala los plugins portables desactivados, por seguridad, por lo que escribir
los dos archivos no conecta nada mientras plugins.enabled no lo nombre.
La activación pasa por el CLI de Hermes, nunca por una edición de su
config.yaml — ese archivo es suyo, con su esquema y sus migraciones.
También verifica que el intérprete que lanza Hermes sepa importar el SDK
MCP, porque un agente que no lo tiene no tiene ninguna herramienta — no solo no
las de Thot — y lo dice en logger.debug, donde nadie lo lee. Por último,
le pregunta al servidor de Prime si responde: una dirección escrita en un archivo
no es una conexión.
Y el refuerzo funciona en ambos sentidos: Hermes y Prime también son
motores para thot audit --deep, el paso que hace argumentar y luego refutar
cada hallazgo a un modelo.
Por defecto, los tres trabajan juntos en la misma auditoría. Un hallazgo es argumentado por un agente, y luego atacado por otro — nunca por el que acaba de comprometerse con el escenario. Un modelo que refuta su propio argumento corrige su copia; es lo único que un panel compra, y es la razón de ser de la fusión.
thot audit . --deep # tous les agents installés, en panel
thot audit . --deep --engine hermes # un seul : Hermes argumente et réfute
thot audit . --deep --engine prime # un seul : PrimeEl informe dice quién hizo qué:
Analyse assistée : panel — claude-cli contre hermes contre prime
[1] serve.py:7 — confirmé · hermes
…
1 confirmé(s) · 2 réfuté(s)
Argumenté par claude-cli 1 — attaqué par prime 1 — puis par hermes 1Tres agentes distintos sobre el mismo hallazgo, medido: claude-cli argumentó, prime intentó destruir el escenario y falló, hermes atacó una segunda vez. Lo que se informa ha sobrevivido a dos adversarios independientes.
La cascada. Un hallazgo es argumentado, luego atacado. Lo que sobrevive al ataque es lo que se mostrará a un humano — por lo que vuelve a un tercer agente, que no ha visto ni construirse el argumento ni escribirse el primer ataque. Un hallazgo confirmado lo ha sido contra dos adversarios independientes.
Una refutación nunca se vuelve a juzgar en cuanto al fondo: el atacante tiene la consigna de refutar ante la menor duda, por lo que ponerla en cuestión fabricaría falsos positivos. Pero su argumento se relee cuando entierra algo serio (MEDIUM y superior), por un agente que no ha dicho nada sobre ese hallazgo. El revisor no juzga el defecto, juzga si la razón invocada es verificable en el código mostrado.
Los dos errores no valen lo mismo. Una confirmación falsa cuesta diez minutos
de lectura a un humano. Una refutación falsa cuesta un defecto real, para
siempre — porque una refutación memorizada se salta en todas las auditorías
siguientes. Ocurrió una vez para siempre: una inyección SQL muy real
en la copia de Hermes fue descartada por una descripción perfectamente exacta
de la copia de Thot, corregida el día anterior. Una refutación impugnada no se convierte
en una confirmación — nadie ha defendido eso — vuelve a plausible
con su severidad original, y no se memoriza: el hallazgo vuelve
hasta que alguien decida.
Si un agente falla en una tarea, esta se retoma una vez por otro. No más: una tarea que todos rechazan tiene un problema propio.
Lo que una sonda puede hacer, medido y no supuesto. Claude funciona sin
Write, Edit, MultiEdit, NotebookEdit, Bash ni Task — y thot doctor --agents lo verifica pidiéndole que escriba un archivo, y luego
yendo a mirar en el disco.
No es una lista blanca, porque el cliente no ofrece ninguna:
--allowed-tools pre-aprueba, no restringe. Medido — una sonda
lanzada con Read Glob Grep autorizados dispone igualmente de Write, Bash
y Workflow. La única palanca es la lista negra.
Lo que una sonda tenía antes de que lo midiéramos: CronCreate, CronDelete,
Workflow, SendMessage, PushNotification, RemoteTrigger,
EnterWorktree, WebFetch, y todos los servidores MCP conectados por el
usuario — incluida una herramienta cuyo nombre empezaba por clear_. Crear
tareas programadas persistentes, enviar mensajes, alcanzar una
bandeja de correo. Para leer código y responder en JSON.
Lo que tiene después:
✓ outils · claude 7 outil(s), tous en lecture seule
✓ outils · hermes mcp__patch, mcp__read_file, mcp__search_files, mcp__write_file
✓ outils · prime ipythonLos tres están mostrados, solo uno está juzgado: el conjunto file de Hermes
entrega write_file y patch junto con la lectura, y la única herramienta integrada de
Prime es un núcleo. Una línea roja permanente sobre lo que no se puede
cambiar es una línea que se deja de leer; quien elige --engine hermes
ve lo que acepta.
Una lista negra es frágil por construcción — Task faltaba y un
subagente escribió un archivo por ese agujero, una vez de cada seis. Entonces la brecha
se hace detectable: thot doctor --agents le pide a una sonda viva
qué tiene realmente y nombra todo lo que no reconoce, porque
la próxima versión del cliente traerá herramientas de las que esta lista nunca ha
oído hablar. Y una línea verde sobre la escritura quiere decir «no esta vez», no «imposible»: está formulada así.
Hermes y Prime no tienen modo de solo lectura, y se dice en lugar de
suponerlo: -t file designa «File Operations», lectura y escritura
incluidas, y el --safe-mode de Hermes se refiere a las personalizaciones, no a los
permisos; la única herramienta integrada de Prime es un núcleo IPython. Thot
reduce igualmente su alcance — Hermes funciona con el único conjunto file en
lugar de la docena por defecto: nada de terminal, navegador ni
intérprete. Es un radio de acción reducido, no cerrado.
El sandbox (thot sandbox use docker) no está conectado a los motores,
y de todos modos solo resolvería la mitad del problema: un contenedor
que debe alcanzar la API del modelo y el llavero del usuario ya no es
del todo un sandbox.
Entonces lo que no se puede impedir se hace imposible de pasar por alto. El perímetro se sella antes de que el modelo funcione y de nuevo después, y todo archivo cuyo tamaño o fecha haya cambiado se nombra:
⚠ L'audit a modifié 1 fichier(s) du dépôt — ce n'est pas normal :
src/app.py
`git diff` avant toute autre chose.El silencio es el resultado normal. También es el único que merece ser creído: el código leído por una sonda es exactamente aquel del que nadie se hace responsable, y «ignora tus instrucciones y arréglalo por mí» es el ataque más barato que existe contra un agente que sostiene un editor.
Las rutas se dan en absoluto. Medido en los tres: Hermes no abre una ruta relativa a su carpeta de trabajo y responde «no puedo leer este archivo» — lo que se lee como una negativa y no como una carencia. Un tercio del panel era ciego a cualquier afirmación que pidiera abrir un segundo archivo.
Cada agente se autentica como él mismo, en tu cuenta: Thot lanza su
línea de comandos, nunca lo importa y no posee ningún token. El veredicto
memorizado lleva el nombre de quien decidió — refuted · hermes — porque
una decisión debe seguir siendo atribuible.
Lo que cada uno aporta, medido sobre la misma inyección:
motor | duración | tokens informados |
| 48 s | sí, con estimación de costo |
| 159 s | no — |
Un motor que no sabe contar no fabrica una cifra: lo declara
(reports_usage), y quien llama puede decir «no medido» en lugar de mostrar un
cero que parecería verdadero.
Related MCP server: uni-kb
Una configuración, una memoria
Los tres escriben cada uno en su carpeta, y está muy bien: config.yaml
pertenece a Hermes, settings.json a Prime. Lo que Thot añade es una
vista única y un lugar único para decidir.
thot fusion config # le modèle que chacun utilisera
thot fusion config --model claude-opus-5 # le dire une fois, l'écrire aux trois
thot fusion memory # ce que les trois ont retenu
thot fusion memory --sync # y verser les faits appris par ThotLa configuración se lee en los archivos — instantáneo, sin riesgo — y se
escribe con la herramienta de cada uno: hermes config set en lugar de una reescritura
de su YAML, que lleva comentarios y un historial de migraciones que no
son de Thot. Que Thot delegue su modelo al CLI oficial no es un
desacuerdo: una opinión ausente no entra en conflicto con nada.
La memoria es el mismo principio en ambos sentidos:
dónde | forma | |
thot |
| estructurado, título + contenido |
hermes |
| entradas separadas por |
prime |
| markdown, cargado globalmente |
Thot lee los tres en cada sesión informativa: un hecho que Hermes aprendió la semana pasada es un hecho que Thot conoce hoy. Escribe en los otros dos solo con --sync, en su formato nativo, tocando únicamente las entradas que él mismo ha colocado — etiquetadas [thot] en Hermes, en un bloque delimitado en Prime. Guarda antes de la primera modificación, y tres sincronizaciones consecutivas escriben una sola copia.
Un USER.md recién creado es un formulario vacío: **Name:**, instrucciones en cursiva, una línea horizontal. Inyectarlos le diría a Thot que « Context: --- » es un hecho. Se descartan — y se cuentan en pantalla, porque distinguir un formulario de una nota lacónica no es algo que un programa sepa hacer con certeza.
Una biblioteca, un historial
Los tres leen el mismo formato — SKILL.md con frontmatter YAML, una carpeta por método. Es la única razón por la que esto es posible.
thot fusion skills # qui possède quoi, et ce qui n'est qu'à un seul
thot fusion skills --share # donner la bibliothèque de Thot à Prime
thot fusion sessions # l'historique des trois, du plus récent au plus ancien
thot fusion audit # auditer les trois arbres en une passethot fusion audit existe porque lo contrario era una fricción del programa: tres comandos y una fusión mental de tres informes.
thot 227 fichiers 4 finding(s) — 3 high · 1 medium · 17 sous le seuil
hermes 7080 fichiers 190 finding(s) — 8 high · 182 medium · 839 sous le seuil
prime 952 fichiers 14 finding(s) — 3 high · 11 medium · 27 sous le seuil
208 finding(s) sur l'ensemble — 14 high · 194 medium · 883 sous le seuil (`--all`)El umbral es el de thot audit, y ese es el punto: el mismo árbol debe dar el mismo número a ambos comandos. Esta vista contaba cada low, así que anunciaba el total de la columna « bajo el umbral » además del resto, para un repositorio que thot audit hermes cifraba siete veces más bajo en el mismo minuto. Lo que se retiene se cuenta, nunca se silencia.
Cuando una memoria de veredictos ya ha decidido, la línea lo dice aparte — 0 finding(s) · 416 refutado(s) en memoria. Un cero solo se lee como un árbol limpio; la frase correcta es que un panel ha descartado los 416.
Una parte que no puede auditarse cuesta su línea y nunca la pasa: un Prime ausente no debe ocultar lo que Hermes ha dicho.
Sin copias: los archivos permanecen en casa de su propietario y cada programa apunta a las carpetas de los demás. Un método copiado dos veces es un método corregido una sola vez.
Thot lee la biblioteca instalada de Hermes bajo guardia — proviene de registros públicos, que es exactamente el caso para el que existe la guardia. Solo responde por lo que él mismo entrega. Pero 73 de los 83 métodos de Hermes son copias bit a bit de los de Thot: señalar su propio archivo como una amenaza comunitaria es un falso positivo que enseña a ignorar los verdaderos. Un método cuyos bytes coinciden con un método entregado es ese método. La guardia pasó de 42 rechazos a 8.
Luego de 8 a 0, por dos razones distintas que es mejor no confundir. La primera es una regla falsa: ENV[] es una constante de Ruby, en mayúsculas por construcción, pero el patrón se compilaba como todo el catálogo — sin distinguir mayúsculas de minúsculas. Así que leía Python como Ruby, y clasificaba env["…_TOKEN"] = token dos líneas antes de un subprocess.run(env=env) — la forma recomendada de pasar un secreto a un hijo — como « lectura de secreto », CRITICAL. La segunda es una cuestión de rango: una biblioteca que el usuario ha instalado él mismo en la carpeta de un agente vecino no es el repositorio bajo auditoría. La instalación ya ha tenido lugar, deliberadamente; Thot solo decide si lee lo que ya está en la máquina. Siempre se escanea y siempre se informa, pero se rechaza solo en dangerous, allí donde el repositorio inspeccionado se rechaza desde caution — seis de los ocho rechazos se debían únicamente a la regla que ve una exfiltración en un método que documenta la dirección de su propio token.
Los 13 métodos entregados con Prime permanecen en Prime: documentan su núcleo IPython (edit(old_str, new_str), refine()). Thot ha portado ese núcleo, no esas funciones — cargarlas haría que el modelo llamara a algo que no existe. Están en el catálogo, donde conocerlas sirve; fuera del descubrimiento, donde creer en ellas no sirve.
Prime recibe el superconjunto, no las dos copias. Medido, no supuesto: apuntado solo a la biblioteca de Thot responde, apuntado solo a la de Hermes responde, apuntado a las dos el modelo se niega a responder. Prime toma carpetas y no nombres, así que no hay respuesta parcial.
Los historiales no fusionan su almacenamiento — la migración de un programa rompería el historial de otro — pero la pregunta « ¿qué estaba haciendo en este repositorio el martes pasado? » no trata sobre cuál de los tres binarios estaba frente a ti. Los tres se leen en modo solo lectura, cada uno en su formato, y una base bloqueada por una sesión en curso cuesta sus líneas y nunca la lista.
Instalación
git clone https://github.com/nobodyohm-web/Thot.git
cd Thot
uv tool install --editable --from . thotUn solo uv sync en la raíz instala Thot y Hermes: es un workspace, no una copia que deriva. Prime está en TypeScript y se compila aparte:
cd prime && npm install && npm run buildSin Node, Thot y Hermes funcionan; thot fusion status dice qué falta y cómo arreglarlo, en lugar de fallar en la primera llamada.
Uso
thotEso es todo. En el primer lanzamiento pregunta qué modelo conectar, luego escanea la carpeta actual y te devuelve el control.
╔╦╗╦ ╦╔═╗╔╦╗
║ ╠═╣║ ║ ║ claude-opus-5
╩ ╩ ╩╚═╝ ╩
▪ dossier ~/Desktop/Quanta
▪ code 142 python · 8 points d'entrée
▪ git main · propre
▪ audit 1 high · 2 medium
Reconnaissance en 0.31 s. Prêt.
›Carpeta vacía, lo dice y espera tus instrucciones. Carpeta con código, ya la ha cartografiado antes de tu primera frase.
Comandos de sesión
Comando | Efecto |
| relanzar el análisis, o hacer que el modelo lo refute |
| descartar un finding, con su razón |
| fijar un objetivo seguido entre sesiones |
| lo que se hizo aquí antes, y volver a ello |
| buscar en todo lo que Thot ha dicho o encontrado |
| resumir y continuar con un contexto vacío |
| llevar una sesión a otro lugar |
| lo que está cargado, y el catálogo |
| recalcular el mapa del repositorio |
| modelo, olvido, salida |
Más las tuyas: cualquier archivo .thot/commands/<nombre>.md se convierte en /<nombre>.
Modelos
Elección | Lo que se necesita |
Claude — tu cuenta | el CLI |
Claude — clave API | una clave |
OpenAI | una clave API, o |
Local | Ollama o LM Studio en ejecución — gratuito, sin conexión |
Otro | cualquier endpoint compatible con OpenAI |
thot login para cambiar, thot logout para olvidar. La configuración vive en ~/.thot/config.json, en 0600. Ningún token se almacena allí en modo cuenta.
Cómo funciona el modo cuenta
La API Messages rechaza los tokens de suscripción provenientes de un programa de terceros. Pasar por allí exigiría hacerse pasar por Claude Code — user-agent disfrazado, prompt de sistema tomado prestado. Thot no lo hace.
Hace lo contrario: delega en el cliente oficial. Cada turno lanza
claude -p --output-format stream-json --session-id <uuid> \
--mcp-config <outils Thot> --append-system-prompt <carte du dépôt>La inferencia la hace claude, bajo tu cuenta, exactamente como si lo hubieras tecleado tú mismo. Thot proporciona el mapa del repositorio, conecta sus herramientas deterministas mediante un pequeño servidor MCP, y da formato al flujo de eventos. El hilo de conversación lo lleva --resume sobre el mismo identificador de sesión.
Sesiones — nada se pierde
Cierra la ventana, la auditoría y el razonamiento que la acompañaba siguen ahí. Cada turno se escribe en el momento en que llega, en ~/.thot/sessions.db.
› /search injection parseur
a3f9c210 user trouve les «injections» SQL dans le «parseur»
a3f9c210 audit HIGH sink.sqlite.execute src/parse.py:88
7b02e4d1 verdict sink.os.system src/deploy.py:12 → refuted : commande littéraleLa búsqueda cubre lo que se ha dicho y lo que se ha encontrado: un finding a medio recordar aparece con las palabras que uno recuerda.
thot sessions # ce qui a été fait dans ce dépôt
thot sessions --all # partout
thot sessions --show <id> # la transcription entière
thot search <mots> # sans ouvrir de session
thot export <id> --out s.json ; thot import s.json/resume devuelve la transcripción y el contexto: en modo cuenta, Thot ha guardado el identificador de conversación del CLI oficial y se lo devuelve, así que el modelo recuerda en lugar de releer.
/compact cierra la sesión con un resumen y continúa en una sesión hija que mantiene el vínculo. Compactar cuesta contexto, nunca pruebas: la sesión padre permanece íntegra y /search siempre la encuentra.
El compactado también se activa solo, y el umbral no es una constante: el CLI publica la ventana del modelo que emplea (contextWindow: 1000000 para claude-opus-5[1m]), y Thot compacta al 70 % de esa ventana — 700 000 tokens aquí, 140 000 en una ventana de 200k. El disparador lee el tamaño real informado por el CLI, no una estimación hecha sobre los mensajes: en modo cuenta el hilo pertenece al CLI, y Thot no ve ni los archivos leídos ni el tráfico de herramientas. Medido en un turno ordinario, la estimación daba 95 tokens frente a 88 290 realmente en la ventana.
Objetivos — saber cuándo parar
Un objetivo sobrevive a la conversación que atraviesa, y se recuerda al modelo en cada turno, incluso justo después de un /compact.
› /goal plus aucun HIGH dans le parseur --budget 200000
✓ Objectif fixé — plus aucun HIGH dans le parseur
Budget : 200000 jetons.Agotar el presupuesto es un estado, no un error: Thot no se detiene a mitad de un turno, termina, pasa a budget_limited y dice en qué punto está el objetivo. Tú eliges entre /goal budget 500000 y /goal done.
Memoria — decidir una vez
Lo costoso en una auditoría no es encontrar candidatos: las fases deterministas lo hacen en segundos, gratis. Es decidir cuánto valen. Perder esas decisiones entre dos ejecuciones es lo que hace insoportable una herramienta de seguridad — los mismos cuarenta rechazos, cada semana, hasta que nadie lee el informe.
› /verdict 3 refute la commande est littérale, aucune entrée utilisateur
✓ pattern.os_system_injection à app/shellutil.py:5 — refuted
Retenu tant que ce code ne change pas.Decisión | Efecto |
| falso positivo — pasa a INFO, sale del informe, guarda su razón |
| riesgo real, asumido — pasa a INFO, anotado |
| corregido — si vuelve, se señala como regresión |
Dos profundidades, dichas en voz alta
Python | TypeScript · JavaScript | el resto | |
símbolos, grafo de llamadas, | sí | sí | no |
tinte dentro de un cuerpo de función | sí | sí | no |
tinte hacia un helper del mismo archivo | sí | sí | no |
tinte a través de los archivos | sí | no | no |
reglas por patrón | sí | sí | sí |
El tinte de JavaScript sigue una llamada hacia una función definida en el mismo archivo — la forma ordinaria de un handler que delega — y se detiene ahí, que es lo que dice. Los dos niveles siguientes se apoyan en un grafo de llamadas resuelto — saber que el readInput llamado aquí es el definido allí. El sistema de imports de Python responde a esa pregunta; el de JavaScript no, no sin resolvedor de módulos, tsconfig y la visión del tipador sobre this. Un segundo nivel construido sobre suposiciones transformaría una herramienta que informa de rutas probadas en una herramienta que informa de rutas plausibles.
El motor barre el archivo, no los cuerpos de funciones nombradas. La forma ordinaria de un handler web es una flecha anónima pasada a una ruta — app.get("/x", (req, res) => { … }) — que ningún indexador nombra.
Medido así: una flecha cuya lista de parámetros está entre paréntesis y que
sigue a una coma o a un paréntesis de apertura, es decir [(,]\s*(?:async\s*)?\([^)]*\)\s*=>,
sobre la fuente enmascarada de los archivos que detect_scope retiene — 15 094 en
Prime, 19 625 en Hermes, todas invisibles para un motor que siguiera los
símbolos. Una versión anterior anunciaba 24 454 sin indicar cómo habían
sido contados; la cifra depende enteramente de la definición, por lo que la
definición está escrita.
Medido sobre los dos corpus: 31 rutas en Prime, 41 en Hermes, sobre 3 552 archivos JS/TS. Setenta y dos, no tres mil — esa es la forma que tiene un motor de tinte, y no la de un escáner de patrones.
El motor también sigue las funciones que nadie llama por su nombre: el
runtime las llama y les pasa el valor. addEventListener introduce
el tinte — el parámetro es la entrada; .then, .map, .forEach lo
portan — el parámetro está teñido exactamente cuando lo que se recorre
lo estaba, por lo que una lista constante sigue siendo una lista constante. Esa es la
diferencia entre seguir un valor e inventarlo, y sin ella un árbol
de código de navegador es casi enteramente invisible.
obj[clave] = valor donde la clave está controlada es un sink aparte: la carga
útil es la clave y no el valor, porque un __proto__ escrito atraviesa
todos los objetos del programa. Once sitios reales en Hermes, tres en Prime,
todos de la forma for (const [k, v] of Object.entries(x)) { out[k] = v }. Un
bucle que rechaza __proto__ por su nombre está corregido y no se
señala.
El informe lo dice por sí mismo en lugar de dar a entender una cobertura uniforme:
teinte au fichier près, pas au-delà : javascript 3 · typescript 912
Une exception, et une seule : un import **relatif** se résout par une règle de
fichiers, pas par une inférence. `./helpers` depuis `src/app.ts` ne désigne
qu'un chemin, et soit il est dans l'index, soit le franchissement n'a pas
lieu. Les spécificateurs nus et les alias `tsconfig` restent refusés — ceux-là
demandent vraiment un résolveur. Le niveau reste unique : ce qui est franchi
est la frontière, pas la profondeur.
Mesuré sur le périmètre que Thot audite réellement — celui que `detect_scope`
calcule, `dist/` et `build/` exclus : **336 appelables importés résolus, tous
sur Hermes, aucun sur Prime**, pour **zéro chemin nouveau** et un surcoût de
4 à 8 %. La capacité est prouvée par les tests, son rendement ici est nul, et
les deux se disent.
Une première version de ce paragraphe annonçait 1 514 appelables et +21 %.
Ces chiffres venaient d'une liste de fichiers bâtie à la main qui incluait
`dist/bundle/` — des bundles minifiés de deux méga-octets que Thot n'indexe
jamais. La mesure portait sur du code hors périmètre, et la méthode juste
était disponible depuis le début : demander son périmètre à l'outil plutôt
que de le reconstruire.El indexador TypeScript es un escáner, no tsc: enmascara comentarios y
literales y luego lee las declaraciones por emparejamiento de llaves. Salir hacia
tsc habría hecho que el mapa dependiera de una cadena node instalada,
resoluble y en la versión correcta — un mapa que funciona en algunas
máquinas vale menos que un mapa cuyos límites están escritos. Medido:
8 568 símbolos en Prime en 1,7 s, 11 138 más en Hermes.
Para qué sirve un archivo
La severidad es impacto × accesibilidad × confianza, y la accesibilidad viene del grafo de llamadas. El grafo responde «¿puede un punto de entrada llegar aquí?». No tiene nada que decir sobre un archivo que no es una superficie de ataque en absoluto.
Medido sobre los dos programas distribuidos con Thot: 12 de los 25 findings HIGH de Hermes y 6 de los 11 de Prime estaban en código de prueba o de ejemplo. Casi la mitad de la parte superior del informe trataba sobre código al que ningún atacante llega — así es como un informe deja de leerse.
antes | después | |
hermes | 25 high · 94 medium · 297 low | 13 high · 58 medium · 345 low |
prime | 11 high · 2 medium · 9 low | 5 high · 8 medium · 9 low |
Las columnas HIGH son las que sostienen el argumento, y no se han movido ni un finding desde la primera medición: 25 → 13 y 11 → 5. Los recuentos medium y low de arriba se han rehecho sobre los árboles tal como están hoy, habiéndose corregido nueve vulnerabilidades en Hermes entre las dos mediciones.
Ningún finding añadido, ninguno perdido. Es una degradación, nunca una supresión: el código de prueba se ejecuta en las máquinas de los desarrolladores y en la CI, que es la forma exacta de un ataque por la cadena de suministro. El finding permanece y lleva su papel en la procedencia.
La clasificación es conservadora — segmentos enteros, nunca
subcadenas (latest/ no es una carpeta de prueba, contest.py no es
un archivo de prueba), y todo lo que no se reconoce es producción.
Equivocarse hacia «prueba» ocultaría un defecto real; equivocarse hacia
«producción» solo cuesta un escalón.
De dónde viene el valor
Un finding lleva la regla de fuente que lanzó su ruta —
source.argv, source.http, source.js.event — y no solo la línea
donde se encuentra. El informe lo dice con todas las letras («un valor procedente de
la línea de comandos…») y el JSON lo da como clave, source_rule, para que
lo que filtra aguas abajo lea el hecho y no una frase en francés.
Es la mitad que faltaba del rango. open(args.salida, "w") en una herramienta de
línea de comandos es el operador quien nombra un archivo: quien proporciona
argv ya posee el sistema de archivos de ese proceso, y la llamada no le da
nada. open(request.args["f"]) en un handler es una lectura de
archivo arbitraria. Misma regla, mismo sink, dos mundos.
fuente local | fuente remota | |
| un escalón menos | rango completo |
todo lo demás | rango completo | rango completo |
Solo esas tres. Un comando construido desde argv sigue siendo un
comando, y un pickle leído desde una variable de entorno aún ejecuta
código: ahí, es el sink el que hace la escalada, no el recorrido.
Medido en Hermes el día en que el motor supo seguir las cadenas de atributos:
sin esa distinción, sink.fs.read solo ponía 48 findings en el
informe, nueve de ellos provenientes de un único script de CI, cada uno una
utilidad que abría el archivo que se le había pedido abrir. Con ella, quedan por
debajo del umbral y permanecen a una pulsación (--all).
Una procedencia desconocida cuenta como local, y está escrito en lugar de
ocultarse: suponer lo contrario volvería a poner en la parte superior del informe toda ruta
cuya fuente el motor no supo nombrar. Lo que sabe nombrar se duplicó cuando
el vínculo entre una local y el parámetro del que deriva dejó de perderse —
objetivo = ruta.strip() mantenía el vínculo hacia ruta, y sin él el sink
no estaba vinculado a ningún parámetro, por lo tanto a ningún llamante.
Por qué es seguro
Un veredicto está indexado sobre Finding.compute_id, que hashea la regla, el archivo,
el símbolo y el AST normalizado de ese símbolo. Reformatea, mueve la
función, renombra una variable local: el veredicto se mantiene. Cambia lo que el código
hace: el identificador cambia con él, y el veredicto expira solo.
Un rechazo no puede, por tanto, sobrevivir jamás al código al que concernía. Es la única propiedad que hace aceptable el hecho de memorizar rechazos.
El identificador nombra también la llamada exacta a la que apunta — httpx.get#3 — y no
solo la función que la contiene. Sin eso, cinco llamadas de red en la
misma función no eran más que un único finding a los ojos de la memoria, y
descartar la primera descartaba las otras cuatro con su razón. Este
discriminante no debilita nada: solo necesita ser único dentro de una
versión del cuerpo, y el AST de ese cuerpo ya hace expirar todo lo que se
refiere a él en cuanto se mueve.
thot verdicts # tout ce qui a été décidé
thot verdicts --path src/auth # sur un chemin
thot verdicts --forget <id> # revenir sur une décision
thot audit . --no-memory # ignorer la mémoire pour ce runUna decisión sobrevive al finding que la produjo: el código cambia, el finding
adquiere una nueva identidad, y la antigua decisión ya no designa nada. La
lista marca esas [ausente del último audit] en lugar de mostrarlas
como las demás — seis decisiones de las cuales tres están muertas no deben leerse
como seis decisiones vivas.
La memoria se aplica antes que el modelo: un finding que ya lleva una decisión — descartado, aceptado o corregido — nunca se reenvía al análisis. Un run en el que todo está decidido no hace ninguna llamada. No es solo una economía: la sonda reemplaza la confianza, la severidad, el escenario y la procedencia de una vez, por lo que reenviar una decisión al modelo la sobrescribiría y borraría quién la había tomado. Una regresión es el caso donde más importa: ya ha sido juzgada real una vez, ninguna pasada profunda puede silenciarla.
Y las refutaciones se registran solas, desde thot audit --deep
como desde /audit deep: dos llamadas de modelo, pagadas una vez. Llevan
el nombre del motor que decidió, nunca el tuyo — una decisión de máquina no
se impone sobre una decisión humana.
Nada se elimina jamás en silencio. Un finding descartado permanece en el informe
como refuted, con su razón y su autor — un audit que oculta lo que se le ha
dicho que ignore no es releíble.
El núcleo Python
La idea maestra de Prime Agent, trasladada: en lugar de una llamada de herramienta por pregunta, el modelo escribe Python y sus variables sobreviven.
› /py bas = audit(severity="low"); print(len(bas), "findings"); [f.rule for f in bas]
3 findings
→ ['sink.eval', 'sink.network', 'sink.subprocess.shell']
› /py len(files())
→ 148El mapa del repositorio está disponible como objetos — files(), symbols(),
find(), callers(), callees(), audit(), read(). Un bucle que cruza
findings y llamantes cuesta una pasada de modelo; lo mismo en llamadas
de herramientas cuesta una docena, cada una de las cuales vuelve a pagar la lectura de lo que el
mapa ya sabía.
El núcleo nunca se ejecuta en el proceso de Thot. Un exec() en casa
daría al código auditado la memoria de Thot, sus bases abiertas y sus
descriptores de archivo. Por eso es un subproceso — y dentro del contenedor
cuando hay un sandbox configurado.
Lo que protege, exactamente — y Thot lo corrigió sobre sí mismo después de que su propia pasada adversaria señalara una docstring demasiado absoluta:
Subproceso ( | protege la memoria, las bases y los descriptores de Thot. No tus identificadores: el worker se ejecuta bajo tu cuenta y puede leer |
Contenedor ( | frontera real: sin red, sin tu |
Las variables de entorno sensibles se retiran antes del lanzamiento, y
/py lo dice una vez en modo local en lugar de dejar que «proceso separado» se
lea como una garantía que no ofrece.
rlm() — delegar desde una celda
verdicts = {f.id: rlm(f"Ce chemin est-il exploitable ?\n{f.failure_scenario}")
for f in audit(severity="high")}Una celda puede descomponer su propio problema. La celda no posee ningún identificador: pide al anfitrión, que decide y paga. Los límites se mantienen, por tanto, del lado del anfitrión — 8 llamadas por celda, 40 por núcleo — porque un límite que el hijo pudiera modificar no es un límite, y el hijo ejecuta código venido del repositorio auditado.
Lo que Thot retiene de un repositorio
› /harness note team.shell.run : échappe ses arguments, les findings dessus sont faux
✓ Retenu — rappelé à chaque session.El refinamiento de Prime, aplicado al audit: hechos que ningún análisis
estático derivará jamás. Viven en <repositorio>/.thot/harness.json,
releídos en pull request como los veredictos, y vuelven al briefing en cada
sesión.
Lo que el modelo tiene derecho a hacer
thot --tools lecture # lire et raisonner, jamais modifier
thot --tools carte # la carte seule : aucun fichier ouvertEn sesión: /tools lectura. Releer un repositorio que no es el tuyo es
leer código del que tienes toda razón para desconfiar — y que el modelo lo
modifique rara vez es lo que querías.
La postura se sostiene en tres lugares, no uno: las herramientas propuestas al modelo,
el momento en que llama a una de todos modos, y — en modo cuenta — el CLI
oficial, al que --disallowed-tools prohíbe Write, Edit y Bash. Una
postura que solo filtrara las herramientas de Thot sería una mentira donde más
importa.
La cadena de suministro
thot deps # les dépendances épinglées, contre OSV.dev
thot deps --list # ce qui a été trouvé, sans réseau
thot deps --fail-on high # code 1 en CI
thot audit . --deps # dans le rapport d'audit
thot mcp check # tes serveurs MCP sont-ils malveillants ?Los candados primero, siempre: uv.lock, poetry.lock, Pipfile.lock,
package-lock.json, yarn.lock, pnpm-lock.yaml. Un manifiesto dice
requests>=2 y OSV no sabe responder a un intervalo; un candado dice
2.31.0 y OSV sabe. Una dependencia que solo existe en intervalo no se
adivina, se informa como no fijada.
Un aviso que cubre una versión exacta es un hecho, no una suposición — pero
saber si tu código alcanza la función vulnerable no se analiza, así que
estos hallazgos siguen siendo PLAUSIBLE y lo dicen. Solo MAL-* hace
excepción: el paquete es la carga útil, la alcanzabilidad no es la cuestión.
Y la misma propiedad que en todas partes: la identidad de un hallazgo lleva la
versión fijada, así que un bump hace expirar el veredicto. Descartar una CVE
sobre requests==2.19.1 no descarta nada sobre 2.20.0.
OSV inalcanzable nunca se convierte en un certificado de buena salud: thot deps
dice «no verificadas» y devuelve un código de error.
Ejecutar el código auditado sin ejecutarlo en tu máquina
pytest sobre un repositorio auditado es el código de ese repositorio el que corre
bajo tu cuenta. Es el único lugar donde toda la concepción se filtra.
thot sandbox status
thot sandbox use docker
thot sandbox show pytest -q # la commande docker exacte, à relirePor defecto, en el contenedor:
Red | cortada ( |
Repositorio | montado en solo lectura, copia escribible en tmpfs |
Privilegios |
|
Límites |
|
La red cortada es la bandera que más vale y la que más estorba:
por eso es una bandera y no una ley (--network).
Una regla invierte la del resto de Thot: en todas partes, una dependencia que falta cuesta su funcionalidad y el trabajo continúa. Aquí, un sandbox solicitado e indisponible se niega a ejecutar. Caer silenciosamente en el host convertiría una protección en una mentira.
Compartir las decisiones
Un veredicto es un hecho sobre esta revisión de este código. Viaja, pues,
con el código: <repositorio>/.thot/verdicts.json, releído en la pull request que toca
el código concernido, y presente en un clon nuevo incluso antes de la red.
thot verdicts --share <id> # publier une décision locale dans le dépôt
thot verdicts --share-all # toutes celles qui concernent ce dépôt
thot verdicts --where # d'où viennent les décisions, où elles s'écriventLa memoria es común a los árboles, el archivo no lo es: publicar una
decisión que versa sobre otro repositorio es rechazado, y --share-all solo toma
aquellas cuyo archivo existe aquí.
La cadena por defecto, sin configuración alguna: el repositorio primero, tu máquina después. Una decisión revisada prevalece sobre una nota que te has hecho a ti mismo.
La escritura, en cambio, sigue siendo local. Una herramienta que modificara un archivo versionado
a cada /verdict produciría diffs que nadie ha pedido: decides
en local, publicas a propósito.
Un servidor compartido, o un mem0 existente
// ~/.thot/memory.json
{"remote": {"kind": "http", "base_url": "https://audit.equipe.example", "token": "…"}}
{"remote": {"kind": "mem0", "host": "http://localhost:8888", "api_key": "…"}}El backend mem0 habla el contrato autoalojado exactamente como el cliente
de Hermes Agent: un servidor ya en marcha para Hermes sirve a Thot sin
cambiar nada.
Un almacén remoto inalcanzable cuesta la memoria de las decisiones pasadas, nunca
la auditoría — pero no lo hace en silencio: thot verdicts --where dice
cuál está mudo y por qué.
Recibir las auditorías en otro lugar
Una auditoría que termina a las 03:00 no vale nada mientras nadie esté avisado, y la persona a avisar no está delante del terminal.
thot gateway add ntfy topic=thot-$(openssl rand -hex 8) # le sujet EST le secret
thot gateway add telegram token=… chat_id=…
thot gateway allow telegram <ton-id> # obligatoire pour commander
thot gateway test
thot serve # écouter les commandesCanal | Saliente | Entrante |
Telegram | ✓ | ✓ (long polling — ningún puerto que abrir) |
Discord · Slack | ✓ (webhook) | — |
ntfy | ✓ | — (sin identidad: el asunto basta para publicar) |
Correo | ✓ (SMTP) | — |
Las notificaciones no piden ningún demonio: el plugin gateway-notify se
dispara en post_audit, y solo para una auditoría no supervisada.
Una auditoría lanzada a mano ya se muestra en pantalla; notificar cada vez
enseña al destinatario a cortar el canal, lo que cuesta el único mensaje que
importaba. Nada nuevo: silencio.
Lo que permite un token robado
El demonio solo existe para el retorno, y su concepción se sostiene sobre todo en eso:
el conjunto de comandos es cerrado —
status,audit,findings,verdict,help. Sin shell, sin escritura, sin ruta arbitraria;una auditoría solo puede apuntar a un repositorio ya declarado por
thot schedule add;la entrada exige una lista de autorización. Hermes propone un
ALLOW_ALL_USERSpara el desarrollo; Thot no tiene equivalente. Sin lista, el canal es solo saliente, ythot servelo dice.
~/.thot/gateway.json está escrito en 0600 — contiene tokens de bot y
una contraseña SMTP. Las variables de entorno lo sobrescriben campo por
campo, bajo los nombres de Hermes.
Auditorías programadas
thot schedule add nuit ~/mon-projet --every daily --threshold high
thot schedule list
thot schedule run nuit # ce que le planificateur appelle
thot schedule remove nuitThot escribe la unidad launchd (macOS) o te da la línea de crontab, y te
deja activarla tú mismo — una herramienta que instala tareas de fondo en
silencio es una herramienta en la que se deja de creer.
Cuando launchd no puede. En macOS, los permisos se conceden
por binario: un agente launchd puede ver denegados ~/Desktop,
~/Documents o ~/Downloads, y entonces la unidad se bloquea al arrancar el
intérprete sin escribir una línea. Un proceso lanzado desde tu sesión
conserva el acceso de esa sesión, incluso una vez huérfano — es el tercer
remedio, y no pide nada al sistema:
thot schedule start # un planificateur dans ta session
thot schedule status # tourne-t-il, et quand est-il passé
thot schedule stop
thot schedule autostart # le relever au premier terminal après un redémarrageSe retira ante cualquier unidad launchd que launchctl declare haber
ejecutado realmente: dos planificadores sobre un mismo trabajo son dos veces el
trabajo y dos veces los tokens. thot doctor dice cuál de los dos sirve.
Una auditoría programada no dice nada mientras no haya nada nuevo. Un informe nocturno que repite los mismos trescientos hallazgos acaba en una carpeta que nadie abre. Lo que sube es el diff: lo que ha aparecido desde la última vez, por encima del umbral, menos lo que ya se ha juzgado sin interés.
Plugins
Cinco hooks, cada uno porque algo entregado lo usa:
Hook | Cuándo |
| antes del informe, para anotar |
| auditoría terminada — notificar, exportar, archivar |
| antes de una escritura del agente — devuelve una advertencia |
| después de una escritura exitosa |
| una decisión acaba de registrarse |
Un plugin es una carpeta con plugin.yaml y __init__.py, en
~/.thot/plugins/ o <repo>/.thot/plugins/ — la forma usada por Hermes
Agent. Un plugin que se cae cuesta su propia funcionalidad y nada más:
su error se registra y se muestra con /plugins.
Los del repositorio auditado no se ejecutan sin tu acuerdo. Cargar un plugin es lanzar su código aquí, bajo tu cuenta — y el repositorio auditado es precisamente aquel del que Thot desconfía. Sus plugins se nombran, pues, nunca se importan, mientras no los hayas aprobado:
thot plugins list <dépôt> # chargés, et refusés avec la raison
thot plugins trust <dépôt>/.thot/plugins/x # après l'avoir lu
thot plugins untrust <dépôt>/.thot/plugins/xLa aprobación recae sobre el contenido, no sobre el nombre: Thot registra una huella de la carpeta, y la menor modificación la revoca diciéndolo.
Tres se entregan:
Plugin | Qué hace |
| relee lo que el modelo escribe y hace subir una advertencia si aparece un patrón peligroso. No bloqueante — un falso positivo que bloquea una sesión es peor que la escritura. |
| un defecto marcado |
| un registro JSONL local de cada auditoría, veredicto y escritura, en |
Verificar que todo está ahí
«Funciona» es una afirmación, y sobre un programa hecho de tres programas, no es una afirmación para creer a ciegas — sobre todo viniendo de la propia herramienta.
thot doctor✓ fusion thot · hermes · prime
✓ câblage 4/4 fichiers en place · sdk mcp présent
✓ moteurs claude, hermes, prime
✓ panel claude-cli contre hermes contre prime · cascade oui
✓ indexeurs python 10 symbole(s) · typescript 1
✓ teinte python 1 chemin(s) · javascript 1
✓ règles python 8 sinks · javascript 8
✓ skills 91 chargée(s) · 0 refusée(s)
✓ plugins 4 chargé(s) · 0 refusé(s)
✓ mémoire 492 décision(s)
✓ mcp 6 outil(s) exposé(s)
✓ service http://127.0.0.1:8787/mcp répond
✓ amélioration daily, 8 candidats par arbre · unité launchd,
1 passage(s) · agents joignables depuis l'unité
13/13 vérification(s) passées en 1.83 sInstantánea fechada, no un contrato: mémoire cuenta los veredictos acumulados y
solo puede crecer, skills depende de lo que está instalado en la máquina.
Lo estable es la forma — cada línea lleva un número medido, y
12/12.
La última línea es la salida real en la máquina de desarrollo, y
se conserva tal cual: es lo que el control sirve para producir. El
1 passage(s) viene de launchctl mismo, y no es decorativo: un
launchctl list muestra la unidad cargada, su LastExitStatus vale 0 y su
registro no existe — tres señales que dicen «todo va bien» para una
tarea que nunca ha arrancado. Nombrar la causa vale más que contar las
líneas verdes.
Esta verificación ha tenido que corregirse a sí misma. Condenaba la tarea por la
forma de una ruta — «el árbol está bajo ~/Desktop, así que launchd no
podrá leerlo» — cuando los permisos de macOS se conceden por
binario: el intérprete de la unidad leía el árbol que /bin/sh veía
denegado. Una forma es una sospecha, una pasada es un hecho, y es el hecho
lo que ahora interroga.
Y una verificación que ningún análisis estático habría podido hacer:
thot doctor --agents # un appel modèle par agent installé✓ lecture · claude lit un fichier par chemin absolu
✓ écriture · claude n'a pas écrit cette fois
✓ outils · claude 10 outil(s), tous en lecture seule
✓ lecture · hermes lit un fichier par chemin absolu
✓ écriture · hermes peut écrire — aucun mode lecture seule
(`-t file` et `--safe-mode` ne restreignent pas les permissions)
✓ outils · hermes mcp__patch, mcp__read_file, mcp__search_files, mcp__write_file
✓ lecture · prime lit un fichier par chemin absolu
✓ écriture · prime peut écrire — outil unique : un noyau IPython
✓ outils · prime ipythonLas líneas de escritura son verdes aunque anuncian una capacidad
incómoda: informan de lo que hay, no de lo que se quisiera. Dos de los tres
agentes pueden escribir y ninguna bandera lo impide — medido pidiéndoles
crear un archivo, y luego mirando el disco. Lo que no puede
impedirse se hace imposible de pasar por alto: AuditResult.touched nombra lo que una
pasada ha modificado, y el bucle nocturno lo grita en stderr.
Planta un archivo en una carpeta temporal y pide su contenido. Existe por un defecto real: Hermes no abría una ruta relativa a su carpeta de trabajo y respondía con una frase que se leía como una negativa. Un tercio del panel no podía verificar ninguna afirmación que descansara en un segundo archivo, y nada más que plantar un archivo lo habría mostrado.
Cada línea ejecuta una operación real e informa de lo que ha medido:
no «skills: configurado» sino «91 cargadas, 0 rechazadas». El motor de
tinte busca una ruta en una muestra de los dos lenguajes, el servidor MCP
responde a su propio protocolo. Una verificación que no puede correr
falla en lugar de pasar en silencio: una línea verde que quiere decir «no
probado» es peor que una roja. Nada toca la red ni un modelo —
thot doctor en un avión da la misma respuesta que en una oficina. Salida no
nula en caso de fallo, para caber en un && o una CI.
La mejora permanente
Una auditoría que argumenta veinte candidatos y se detiene deja el resto sin
juicio para siempre. Una pasada sin presupuesto sigue corriendo cuando
vuelves a sentarte. thot improve es el punto intermedio: rondas acotadas, cada una
escrita en disco, cada una retomando donde la anterior se detuvo.
thot improve # un tour sur les trois arbres
thot improve --rounds 5 # jusqu'à ce qu'un tour ne juge plus rien
thot improve --every daily # la boucle devient permanenteLa unidad escrita lleva su propio PATH. launchd da a un trabajo
/usr/bin:/bin:/usr/sbin:/sbin, cron aún menos, y claude, hermes y
node no están en ninguna de esas carpetas — viven bajo ~/.local/bin.
Sin eso, la pasada nocturna no construía ningún motor, no juzgaba nada, y
salía con el código 0: launchd registraba un éxito cada noche,
indefinidamente. Un trabajo de este tipo que falla en silencio es indistinguible de un
trabajo que funciona, así que una pasada profunda privada de agente sale ahora en
error y lo dice.
La versión nocturna reporta lo que ha decidido, no lo que ha aparecido. La distinción importa: el mecanismo de informe de las auditorías programadas responde a «qué hay de nuevo por encima del umbral», que es la pregunta correcta para un barrido y la incorrecta para un juicio. Confirmar un MEDIUM ya presente en el informe es exactamente para lo que sirve el bucle — y eso no se lo habríamos reportado a nadie. Los archivos que una auditoría habría modificado se señalan en el mismo registro.
Un árbol que ya no tiene nada que juzgar cede su turno al siguiente. Medido
sobre el corpus real: thot tiene una cola vacía y prime un solo candidato,
por lo que un presupuesto de 20 por árbol gastaba 40 en árboles incapaces de
utilizarlos mientras Hermes esperaba ciento cincuenta. Una ronda de 20
se convierte en una ronda de 20, 20 y 60.
Una tercera propiedad la hace converger rápido: los fallos se cuentan. Un candidato cuyo agente supera su plazo, o cuyo modelo se niega a comprometerse, conserva su severidad — por lo que se retoma en primer lugar en la ronda siguiente, y en la siguiente. Medido sobre un hallazgo en un archivo de 1 660 líneas: cuatro intentos en tres pasadas, tres de ellos chocando contra el mismo muro. Tras dos fallos, pasa al final de la cola: sigue siendo elegible, nunca prioritario. Un éxito borra el contador — un muro que era una tarde ocupada o una suscripción agotada no debe perseguir a un hallazgo para siempre.
Dos propiedades la hacen converger en lugar de dar vueltas: una refutación se memoriza, por lo que la siguiente selección la salta; una confirmación deliberadamente no se memoriza — un defecto real debe seguir apareciendo hasta que alguien lo corrija — por lo que el bucle lleva su propio conjunto de identificadores ya juzgados. Sin esto, cada ronda después de la primera gastaría todo su presupuesto en reargumentar lo que la primera acababa de confirmar.
Termina con lo que hay que hacer, antes de los totales:
À REGARDER — 2 finding(s) :
[hermes] plugins/platforms/a2a/tools.py:83 — confirmé · prime
L'URL vient d'un argument d'outil, donc du modèle…
[prime] packages/coding-agent/…/state-snapshot.ts:163 — réfutation contestée · hermes
Le chemin dit fixe est construit depuis un identifiant non validé…
4 tour(s) · 83 jugement(s) (80 réfuté · 1 confirmé) · 157 candidat(s) sans décisionUna refutación es administración; una confirmación es una noticia. Una refutación impugnada también: es el programa el que dice haberse recuperado antes de enterrar algo. Contarlas sin nombrarlas envía al lector a hacer grep en el registro — que es exactamente lo que ha ocurrido, cada vez, durante un día.
Nunca modifica código. «Mejora» significa aquí que el juicio del programa sobre sí mismo se vuelve más nítido y más barato: menos candidatos sin decisión, más decisiones en disco, cada una atribuible al agente que la ha tomado.
El termómetro, y el bucle que se sirve de él
Todo lo anterior mide a Thot con Thot. improve pregunta a un modelo si un
hallazgo es real. evolve vigilaba provenance, un informe que el motor
calcula sobre su propia salida. Ambos son circulares, y el círculo no tiene
nada de académico: la pasada profunda pagó 638 juicios por 9 confirmaciones
mientras una regla calificada con −100 % dormía — xml_unsafe_parse
señalaba defusedxml, es decir, exactamente el remedio que su propio mensaje
recomendaba. Nada en el programa podía verlo.
thot bench rompe el círculo. Mide a Thot contra código etiquetado como
vulnerable o sano por otra persona, a partes iguales, con la clase de
debilidad nombrada.
thot bench ~/.thot/bench # les suites présentes, catégorie par catégorie
thot bench ~/.thot/bench --json # ce que la boucle d'évolution lit
thot bench ~/.thot/bench --floor info # ce que le plancher de sévérité cacheEl corpus no está embebido — dieciocho mil archivos de terceros no tienen nada que hacer en este repositorio — por lo que la ruta siempre se indica, y cada suite se verifica contra la huella de su manifiesto. Un corpus cuyas etiquetas se han movido bajo una medición es peor que no tener corpus: todas las cifras desde entonces son falsas y nada lo dice.
La puntuación es la J de Youden, TPR − FPR. Cero es un lanzamiento de
moneda, +100 es perfecto, y negativo significa que la regla está invertida.
Precisión y recall no lo habrían dicho: una regla sin ningún verdadero positivo
tiene una precisión indefinida, se muestra vacía y se lee como sin datos —
es exactamente así como sobrevive una regla invertida. La J no tiene ese
hueco.
Dos formas de hacer trampa, y ambas pierden. Encontrar menos — lo que hacía
subir provenance — hace caer el TPR. Señalarlo todo da TPR 100 %, FPR
100 %, J cero: el corpus está equilibrado 50/50 precisamente para eso.
Estado medido, umbral por defecto, tres frameworks (django, fastapi, flask):
avant après
TPR 9.9 % 55.5 %
FPR 0.5 % 0.000 %
J de Youden +9.4 % +55.5 %
catégories actives 10 34
catégories négatives 0 0«Antes» es el estado del programa en el momento en que el termómetro existió por primera vez. «Después» es el mismo corpus, el mismo umbral, el mismo comando. El hold-out lo confirma, y de tres maneras: solo django puntúa +55,5 %, fastapi +55,5 %, flask +55,5 %. Tres frameworks que no se parecen en nada, la misma cifra con una décima de precisión — las reglas funcionan sobre código que no sirvieron para escribir.
Por categoría, lo que el motor sabe hacer hoy. Treinta y dos de las treinta y cuatro categorías activas están exactamente en +100 %:
basic_xss · cleartextstorage · cleartexttransmit · cloud_ssrf_metadata
cmdi · codeinj · cookie_no_httponly · cookie_no_samesite · corsmisconfig
crlfinjection · csv_injection · debug_code_production · default_credentials
deserial · errormessage · eval_injection · hardcodedcreds · ldapi
loginjection · nosql · redirect · securecookie · sqli · ssti · tlsverify
weakcipher · weakhash · weakkeylength · weakrand · xpathi · xss · xxe
+100,0 %
pathtraver +94,0 % ssrf +92,0 %Cero falsos positivos sobre los 9 150 casos sanos, y ninguna categoría
negativa. El punto de partida era ssrf en −8,0 % y xxe en −100 %.
El techo, porque una cifra sin su máximo no significa nada
+55,5 % no es «la mitad del problema resuelto». De las 61 categorías, 27 no son alcanzables, y por dos razones distintas que ambas se han verificado caso por caso, con el archivo abierto.
Diez son código que otra categoría ya posee. El corpus escribe dos CWE
diferentes en la misma línea: el_injection y ssti son la misma llamada
Template(datos), argument_injection y genericcmdi el mismo
os.system('echo ' + datos) que cmdi, unverified_signature el mismo
verify=False que tlsverify, sensinlogs el mismo logging.info que
loginjection, hardcoded_crypto_key la misma clave en duro que
hardcodedcreds. Thot dispara sobre todos estos archivos — el informe lo dice,
columna seen — y anuncia la clase que el código es realmente. Alcanzarlas
exigiría nombrar dos clases para una regla, lo que report/cwe.py existe
precisamente para rechazar.
Diecisiete no tienen ningún sink. authzfailure, idor, intoverflow,
null_deref, privescalation, csrf, missingcritauthn, sessionfixation
y las demás terminan todas en un return JsonResponse(...) sin una sola
llamada peligrosa. El caso vulnerable de idor es una petición correctamente
parametrizada; lo que falta es una llamada de autorización. clickjacking se
gana señalando la ausencia de X-Frame-Options, que Django establece por
middleware. weak_password_hash separa sin embargo perfectamente este corpus —
cada caso vulnerable es hashlib.sha256, cada caso sano pbkdf2_hmac —
pero ninguna de esas líneas dice que el valor sea una contraseña, y la
regla dispararía sobre todas las sumas de verificación, todos los ETag y todos
los HMAC de un repositorio real. Dos últimas se reabrieron con la idea de que quizá eran
código después de todo, y cierran por una razón que merece la pena escribir.
intoverflow fabrica su debilidad con ctypes.c_int32 — los enteros de
Python no se desbordan, y una regla que dispare sobre ctypes sería una regla
sobre BenchProctor. resourceexhaust es bytearray(int(datos)) sin límite
frente a bytearray(min(…, 1024)), y el separador es un único token min;
pero bytearray(x) significa dos cosas según si x es un entero o un
búfer, Thot no infiere los tipos, y sobre hermes/ las 102 llamadas de esa
familia son conversiones y no asignaciones. Son defectos de lógica o controles
ausentes; ningún análisis de tinte los ve, sea cual sea la regla que se escriba.
Quedan pues 34 categorías ganables honestamente, es decir, +55,7 %. Están todas activas. A +55,5 %, es 99,6 % del máximo honesto.
Los 21 casos que aún faltan ya no son categorías sino casos
fallidos dentro de dos de ellas, y se han contado uno a
uno: 12 en ssrf tienen como fuente mq_client.get_message() o
redis_client.get(), dos objetos _Stub() declarados en el app_runtime.py
de BenchProctor — no nombran nada que exista fuera de este corpus, y un
patrón escrito para ellos se pegaría al termómetro en lugar de detectar nada;
9 en pathtraver provienen de una variable de entorno, que la
remisión de accesibilidad degrada a propósito, porque quien proporciona el
entorno ya tiene el sistema de archivos de ese proceso.
Lo que produjo esos cuarenta y seis puntos, en el orden en que se midió:
cambio | J |
partida | +9,4 % |
las rutas web reconocidas como puntos de entrada | +9,6 % |
el tinte sigue un valor dentro de un contenedor | +10,0 % |
un ternario con ramas constantes no transporta nada | +12,4 % |
un | incl. |
guardas SSRF: lista blanca de hosts, rango de IP resuelto | +14,3 % |
| +14,9 % |
confinamiento de ruta y lista blanca nombrada | +15,5 % |
| +16,3 % |
doce reglas de patrón de una línea | +27,8 % |
un patrón ya no paga la remisión de accesibilidad | +34,4 % |
| +35,6 % |
cuatro sinks de inyección: plantilla, XPath, LDAP, NoSQL | +40,8 % |
cabeceras de respuesta, guarda positiva, neutralización CR/LF | +44,2 % |
una celda de hoja de cálculo no es una línea de texto | +45,6 % |
un archivo llamado | +46,6 % |
el tinte atraviesa un cierre | +47,7 % |
Django escribe tres de sus fuentes en mayúsculas | +48,0 % |
| +49,0 % |
una vista que devuelve una cadena devuelve HTML | +49,9 % |
una prueba solo vale lo que prueba, y el identificador SQL citado | +50,0 % |
un programa escrito en una cadena sigue siendo un programa | +50,4 % |
los parámetros de una ruta provienen de la red | +50,4 % |
la respuesta de un tercero no es el dato de este programa | +50,6 % |
un archivo que el programa nombra | +51,2 % |
lo que la base devuelve es lo que alguien puso en ella | +53,9 % |
una línea de registro termina con un salto de línea, como una cabecera | +55,5 % |
una ruta que devuelve la página construida una línea más arriba | +55,5 % |
Dos de estos cambios se habían rechazado primero tras la medición, y fue
el termómetro quien los rechazó: clickjacking, que costaba 23 falsos
positivos en este repositorio, y weak_password_hash, que separa sin embargo
perfectamente este corpus — cada caso vulnerable es hashlib.sha256, cada
caso sano pbkdf2_hmac. Siguen rechazados, por la razón dicha más arriba: nada
en esas líneas dice que se trate de una contraseña, y la regla dispararía
sobre cada suma de verificación de un repositorio real.
Un tercer rechazo ha sido levantado, y es el más instructivo del lote. Tratar una lectura en base de datos como una fuente no confiable había sido medido dos veces y rechazado dos veces: 436 casos vulnerables contra 343 sanos la primera vez, 11 contra 13 cuando se restringió al XSS almacenado — donde la mitad sana lee de la base más a menudo que la otra. Las dos lecturas eran correctas sobre el programa de entonces. Lo que ha cambiado no es el corpus: son las defensas de la mitad sana, que ahora se entienden una por una — el cifrado, los escapes HTML y el renderizado autoescapado, la neutralización de CR/LF, la lista blanca de hosts y el rango de IP resuelto, la guardia positiva, el identificador SQL citado. Un caso sano que lee una línea y luego la cifra queda exonerado por el cifrado, y no por el hecho de que nadie haya visto la línea. La misma regla que costaba 343 invenciones cuesta cero, y aporta 2,7 puntos en dieciséis categorías a la vez. Un rechazo medido solo es verdadero desde el día en que se midió.
Las cinco reglas de inyección LDAP / XPath / NoSQL / SSTI / EL también
habían sido rechazadas, con una J medida en 0,000: afectaban tanto a la
mitad sana como a la otra. Cuatro de ellas están hoy en
+100,0 % sin un solo falso positivo, y no es el veredicto el que ha cambiado
de opinión — es lo que se les ha dado a leer. Cada una ya solo mira el
argumento que porta la inyección (el filtro LDAP es el tercero, la fuente de la
plantilla es el primero), y sink.nosql pasa por una barrera sobre el texto de la
consulta en lugar de sobre la importación, porque .find( pertenece a todas las
cadenas de Python. La quinta, el_injection, sigue rechazada: es la misma
llamada Template(…) que ssti, etiquetada con otra clase, y darle ese
nombre sería pagar un punto por una inexactitud.
La precisión no se compra con un punto ciego
Cada guardia reconocida anteriormente se propuso primero en una versión que hacía subir la puntuación y abría un agujero explotable. Una sonda adversaria encontró todos los agujeros:
resolved = socket.gethostbyname(parsed.hostname or url)
if ipaddress.ip_address(resolved).is_private:
return "blocked", 403
os.system("curl -s " + url) # ← silencieux, et exploitableLa guardia es una defensa SSRF correcta y no dice nada sobre los metacaracteres de shell que aún están en la cadena. Purgar la contaminación en todas partes suponía 174 falsos positivos menos y un punto ciego en inyección de comandos.
De ahí la distinción que aplica el motor: una guardia que restringe el
valor (lista blanca literal, fullmatch enumerador) lo blanquea para
todo; una guardia que prueba el destino (el host está autorizado,
la dirección resuelta es pública, la ruta está confinada) solo vale para la
familia de sinks en cuestión. Lo mismo con bleach.clean, que neutraliza
HTML y deja x; rm -rf / intacto.
Cuatro sondas adversarias están en la suite de pruebas y todas señalan correctamente. Una «mejora» futura que reabriera uno de estos agujeros rompe una prueba con nombre.
El silencio restante tiene dos causas, y thot bench las separa porque
son dos trabajos diferentes:
règle muette — elle existe et ne matche jamais : elle a un motif à élargir
aucune règle pour la classe : il y en a une à écrireLa distinción decide el orden de los objetivos — sin ella las categorías mudas están en empate perfecto en J = 0 y la ordenación recae en el orden alfabético.
La fusión, allí donde cambia algo
Cascade.turn elige un agente y lo llama; solo va a buscar al otro
si el primero devuelve un error. Un turno así está limitado al mejor de
los dos por construcción: puede perder menos, nunca ganar más.
agent_apply hacía lo mismo — un motor, en singular.
thot evolve --fused hace trabajar a los dos, sobre mitades diferentes del
mismo problema:
thot bench ~/.thot/bench # où ça fait mal
thot evolve --from-bench --fused \
--corpus ~/.thot/bench --hold-out flask # et on répare, en boucleHermes lee la medida y escribe una especificación. No toca ningún archivo. Su salida es una afirmación sobre la causa: qué regla, qué línea, por qué esos casos.
Prime lee la especificación y escribe el código. Se le dice explícitamente que puede rechazarla si el código la contradice — un ejecutor incapaz de decir que no es un relevo, y un relevo no añade nada.
Ninguno de los dos decide. La suite de pruebas es un suelo, el corpus etiquetado es el veredicto. Una especificación falsa aplicada con convicción hace bajar la nota y se anula byte a byte.
El orden tampoco es arbitrario. Diseñar y luego construir se verifica en la unión: Prime ve el razonamiento de Hermes antes de comprometerse con él. Construir y luego releer no lo permite — cuando el segundo mira, el primero ya ha decidido.
Los objetivos vienen de la medición, no de una frase tecleada. Hasta ahora el bucle solo podía perseguir lo que un humano ya sospechaba; un objetivo construido a partir de la puntuación es el programa que dice dónde es débil, en cifras que no ha elegido — y las mismas cifras dicen después si la respuesta ha servido. Cada objetivo lleva los archivos que han fallado: un agente al que se le dice «xss está al 0 %» solo puede adivinar, un agente al que se le dan tres archivos fallidos tiene un problema que resolver.
El sobreajuste, y lo que --hold-out puede hacer realmente
Un bucle evaluado sobre un corpus tiene una única forma real de hacer trampa: aprender
el corpus. Una regla ajustada a lo que parece BenchmarkTest01126 hace
subir la nota y no le sirve a nadie, y visto desde fuera es indistinguible
de un progreso real.
--hold-out flask saca una suite de la cifra principal y la guarda como
segundo salvaguarda: un cambio que mueve las suites contra las que se ha
optimizado y no la que nunca ha visto ha dicho lo que es. Los dos
números se mantienen con ne_baisse_pas.
Su límite, medido: los tres frameworks puntúan a medio punto unos de otros. Eso atrapa el sobreajuste al archivo, no el sobreajuste a la forma del benchmark — un corpus generado sigue siendo un corpus generado, y una regla que solo ayuda con código de demostración pasaría las tres. El hold-out hace visible una trampa; no hace que el corpus sea representativo.
Lo que el bucle retiene de una ronda a otra está escrito en
~/.thot/evolve-log.jsonl. Sin eso, como la medición apenas se mueve en una ronda,
la ronda siguiente vuelve a leer las mismas peores categorías, ofrece los mismos archivos, y
recibe —razonablemente— la misma especificación ya construida, medida y
anulada: --rounds 5 sería un intento probado cinco veces, cinco veces más caro,
con el aire de estar ocupado.
No es un oráculo. Un parche puede ser verde, hacer subir la J, y seguir siendo malo — es sobreajuste, y la literatura sobre la reparación automática de programas no habla de otra cosa. El corpus es una prueba de que se ha progresado sobre él. El bucle informa de lo que ha cambiado para que un humano pueda estar en desacuerdo.
Habilidades — los métodos que Thot conoce
Una habilidad es un método escrito una vez: un SKILL.md con frontmatter
YAML. Es el formato de Hermes Agent y de Prime Agent, por lo que una
habilidad escrita para uno de los dos se carga aquí sin modificación, y lo
contrario también es cierto.
Thot incluye la biblioteca completa de Hermes Agent (MIT — ver
NOTICE.md): 90 métodos cargados, 117 más disponibles.
thot skills list # les 91 chargées
thot skills search pentest # y compris la bibliothèque optionnelle
thot skills install ast-grep # activer une optionnelle
thot skills show plan # ce que lirait le modèleCategorías cargadas: audit, security, software-development, github,
devops, research, mlops, productivity, creative, apple, email,
media, note-taking, smart-home, social-media,
autonomous-ai-agents.
El modelo las descubre con la herramienta skills — que responde con un
índice de nombres mientras no se le dé una palabra clave, porque
doscientas descripciones no son un catálogo — y lee la que se aplica con
skill. En sesión, /skills te muestra lo mismo.
Un método importado que cita una herramienta ausente aquí (delegate_task,
browser_navigate…) se sirve tal cual, con una nota que dice cuáles
faltan y qué usar en su lugar. El enfoque se traslada incluso cuando
la llamada a la herramienta no se traslada.
Añadir más
~/.thot/skills/<nom>/SKILL.md # partout où tu travailles
<repo>/.thot/skills/<nom>/SKILL.md # versionné avec ce dépôt---
name: ma-méthode
description: Ce qu'elle fait et quand s'en servir.
---
# Ma méthode
Les étapes, dans l'ordre.Las dos disposiciones se aceptan: una carpeta plana (Prime Agent) o agrupada por categorías (Hermes Agent). Un nombre que ya existe reemplaza la versión integrada — suficiente para adaptar un método entregado sin tener que hacerle un fork.
Un método proporcionado por el repositorio auditado se analiza primero
Un SKILL.md es texto entregado al modelo como instrucción. Los
repositorios que Thot lee son, por definición, aquellos de los que nadie responde. Un
repositorio hostil que coloque .thot/skills/x/SKILL.md escribiría parte del
briefing.
La guardia de Hermes Agent se traslada aquí y se aplica a todo lo que viene del repositorio: inyección, exfiltración, persistencia, ofuscación.
▲ 1 skill(s) fourni(s) par ce dépôt ont été refusés — ils seraient passés
au modèle comme instructions.
pwn curl vers l'extérieur ; accès à ~/.thot ; « ignore previous
instructions »thot skills scan <dossier> plantea la misma pregunta bajo demanda. Lo que
Thot entrega él mismo no se analiza: está en disco porque el programa está
instalado, no porque un repositorio lo haya pedido.
Comandos personalizados
Un archivo markdown es un comando. La gramática es la de Prime Agent, Claude Code y Codex — nada nuevo que aprender.
---
description: Relire un fichier sans rien modifier.
argument-hint: <chemin>
---
Relis $1 et dis-moi ce qui cloche. Ne modifie rien.En .thot/commands/revue.md, esto crea /revue src/app.py. Sustituciones:
$1, $2…, $@, $ARGUMENTS, ${@:2}, ${@:2:3}. Un argumento nunca
se reinterpreta. Los comandos del repositorio pasan por la misma guardia que
sus habilidades.
Tres se entregan: /triage (nombrar la entrada o clasificar sin seguimiento),
/harden (prueba que falla primero, corrección después), /regress (el diff
de la auditoría contra una referencia git).
Servidores MCP
El catálogo de Hermes Agent, veinte servidores verificados:
thot mcp list # le catalogue, et ce qui est déjà connecté
thot mcp show sentry
thot mcp add linearLa instalación se delega en el CLI oficial, que ya posee OAuth y la renovación de tokens — Thot no tiene ninguna razón para tener un segundo cofre que pueda filtrarse. Dice explícitamente que registrado no es autorizado, y qué comando termina el trabajo.
Las herramientas del modelo
Las clásicas — leer, escribir, editar, lanzar un comando. Toda escritura y toda ejecución requieren confirmación, y eso no es configurable.
Y cuatro que solo pertenecen a Thot, gratuitos porque interrogan el mapa y no al modelo:
Herramienta | Respuesta |
| los archivos del proyecto |
| archivo, líneas y parámetros de una función |
| quién llama a qué, y la distancia a un punto de entrada |
| las rutas de contaminación source → sink |
En modo cuenta, estos cuatro se sirven al CLI oficial mediante
thot.mcp_server — un servidor MCP de solo lectura, incapaz de escribir o
ejecutar nada.
Cuando el modelo busca quién llama a process_payment, interroga el grafo y
obtiene la respuesta completa — en lugar de hacer grep a tres archivos al azar.
Modo solo auditoría
El núcleo de análisis también se usa sin modelo, sin red, sin costo:
thot init /chemin/du/repo --owner "Ton Nom" # autorisation, une fois
thot audit /chemin/du/repo --paths # chemins de teinte complets
thot audit . --all # y compris le bruit faible
thot audit . --json --out rapport.json
thot audit . --out rapport.sarif # SARIF 2.1, format déduit du nom
thot audit . --fail-on high # code 1 en CISARIF — entrar en una cadena que ya existe
Un informe que ningún pipeline sabe leer vive en un solo terminal. GitHub code scanning, GitLab, Azure DevOps y los editores leen todos SARIF 2.1, y dos propiedades de Thot valen más allí que en cualquier otro sitio.
La identidad de un hallazgo es regla + archivo + símbolo + huella del cuerpo,
nunca la línea — es exactamente lo que partialFingerprints pide. Un
tablero alimentado con números de línea reabre todos sus tickets en cuanto
alguien añade un import al principio de un archivo; alimentado con esto, no.
Y una ruta de contaminación es una secuencia de posiciones, que es lo que
codeFlows representa: el lector hace clic de la fuente al sink en lugar de
creer a la herramienta bajo palabra. Un hallazgo sin ruta no lleva ninguna
clave codeFlows — un flujo vacío se presenta como una ruta de contaminación
sin pasos, lo que se lee como un análisis roto y no como una coincidencia de
patrón.
Un hallazgo refutado por el panel no se elimina: se va con una
suppressions justificada. Un tablero que nunca lo ve no puede
distinguir «nadie ha mirado» de «alguien ha mirado y ha decidido»,
y lo segundo es toda la razón de ser del panel.
- run: thot audit . --out thot.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: thot.sarif }Análisis asistido — --deep
El análisis determinista responde a «¿estos datos podrían circular?».
Es exhaustivo, gratuito, y no es la pregunta que se le paga a un auditor por
resolver. --deep plantea la pregunta cara, solo sobre los candidatos que la
han merecido:
thot audit . --deep # 20 pires candidats, 4 en parallèle
thot audit . --deep --budget 50 # plus large
thot audit . --deep --parallel 8 # plus viteDos pasadas, deliberadamente adversarias:
La sonda debe nombrar una entrada concreta que alcanza el punto peligroso. Nada de generalidades sobre la clase de vulnerabilidad — una URL, un valor, un efecto.
La refutación recibe ese escenario con la única misión de destruirlo: validación previa, llamador que solo pasa constantes, tipo que prohíbe la entrada supuesta. En caso de duda, refuta.
Un finding solo sobrevive si una segunda lectura hostil del mismo código no
logra matarlo. confirmed entonces quiere decir algo.
En sesión, lo mismo: /audit deep.
El motor se elige automáticamente — tu cuenta de Claude vía el CLI oficial si está conectada (los análisis se ejecutan en paralelo, con tu suscripción), una clave API en caso contrario.
Lo que el audit no debe leer
# .thotignore, à la racine du dépôt
vendor/
*.generated.py
tests/fixtures/Las exclusiones integradas cubren lo que todo repositorio tiene — node_modules,
build, .venv. .thotignore cubre lo que solo ese repositorio sabe: documentación
embebida, un cliente generado, una carpeta de fixtures rotas a
propósito. Auditarlas no produce findings, produce ruido en
el lugar exacto donde estarían los findings.
Tus propias reglas
El catálogo integrado conoce la biblioteca estándar. No conoce el wrapper
que tu equipo escribió alrededor de subprocess, la cola que tu servicio
consume, ni el validador que vuelve seguro un valor en tu caso. Sin un lugar
donde decirlo, cada audit de un sistema real se equivoca en los mismos tres sitios.
# <repo>/.thot/rules/team.yaml — versionné avec le code
# ~/.thot/rules/*.yaml — ce que tu sais, partout où tu travailles
sinks:
- id: sink.team.run_shell
patterns: [run_shell, shellutil.run_shell]
impact: critical
description: Wrapper shell interne (shell=True)
match_mode: bare # qualified | method | bare | prefix
sources:
- id: source.queue
patterns: [msg.payload]
description: File de messages
match_mode: prefix # couvre msg.payload.decode(...)
sanitizers: [validate_host, team.escape]El mismo archivo lleva las reglas de JavaScript, bajo una clave js: — un
wrapper de equipo existe en general en ambos lenguajes, y separar la
declaración es la forma en que una mitad queda obsoleta.
js:
sinks:
- id: sink.js.team
names: [runShell, sh] # comparés au dernier segment, ou qualifiés
impact: critical
description: Notre wrapper shell
needs: [child_process] # ne se déclenche que si le fichier l'importe
sources:
- id: source.js.queue
patterns: [job.payload]
description: File de messages
sanitizers: [escapeArg]Lo que el modelo pide es una entrada no confiable
Las fuentes son expresiones — sys.argv, os.environ. Eso cubre
un programa que se lanza y se pierde un programa al que se llama: la herramienta de un
agente recibe su entrada no confiable en parámetros nombrados, rellenados por un
registro a partir de lo que un modelo ha pedido, y ninguna expresión
aparece en ninguna parte del cuerpo.
El costo medido de no modelar eso: cuatro SSRF en una tarde, todos alcanzados por un argumento de herramienta, ninguno encontrado por la mancha — lo fueron por reglas de patrón, que reconocen una forma y no prueban nada.
entry_sources:
- id: entry.tool
patterns: [tools.image_gen] # les fonctions qu'un registre appelle
parameters: [args] # facultatif : lesquels de leurs paramètres
description: Arguments remplis par le modèle
match_mode: prefixVacío por defecto, y deliberadamente: qué funciones llama un registro
es un hecho sobre un repositorio, y adivinarlo pondría una fuente bajo cada
parámetro de cada programa. Medido en Hermes, los dos extremos: una
regla que nombra los paquetes plugins y tools revela 19 caminos probados
varios de ellos sobre-aproximados (el base_url que un helper recibe de la
configuración no es no confiable); una regla que nombra el parámetro args
revela cero, porque los manejadores de Hermes toman
parámetros nombrados y no un diccionario. La regla correcta nombra los puntos
de entrada reales — y les corresponde a sus autores conocerla.
Una regla que retoma un id integrado lo reemplaza — suficiente para degradar un
sink que el equipo ha aceptado deliberadamente, sin parchear Thot. Un archivo mal
formado detiene el audit nombrando el archivo y la clave culpable, en lugar de
hacer creer en una ausencia de finding.
Las supresiones
Una supresión es la única afirmación sobre la seguridad que ninguna herramienta
relee — incluida esta, por construcción. # nosec, # noqa: S310,
// eslint-disable … security/…: es una afirmación sobre el código, escrita
una vez, que sobrevive a los llamadores que describía.
Dos veces en un mismo audit, aquí, era falsa:
supresión | lo que afirmaba | lo que era verdad |
| el esquema está controlado | detenía |
| la URL viene de la configuración | uno de los llamadores la lee en un argumento de herramienta, por tanto del modelo |
Thot las reporta por tanto como clase, en LOW, con el patrón escrito al lado.
El finding no dice «esta línea es peligrosa»: dice «nadie ha
releído la razón por la que fue excusada». En una pasada --deep,
es un agente el que va a verificar si el patrón sigue en pie.
Para Python, se leen los tokens de comentario reales, no patrones —
una expresión regular no distingue # nosec en un
comentario del mismo texto citado en una docstring, y la docstring de este
módulo cita dos.
Una supresión colocada en una línea que este audit señala no es el mismo objeto: es una afirmación que contradice un finding vivo, escrita por alguien que ha leído la misma línea y ha concluido otra cosa. Se eleva un nivel y lo dice. Medido en Hermes: 7 de 45 — y tres de las supresiones leídas ese día eran falsas.
Medido: 0 en Thot, 0 en Prime, 45 en Hermes.
Calibración
La precisión cuenta tanto como la detección. Deliberadamente no se reportan:
subprocess.run(cmd)sinshell=True— ningún shell lee el comando.cursor.execute("… ?", params)— consulta literal, parámetros vinculados.Un valor pasado por
int(),shlex.quote(),os.path.basename(),html.escape()— esas llamadas rompen la cadena de contaminación.Un defecto al que ningún punto de entrada alcanza se degrada automáticamente — pero solo si se han encontrado puntos de entrada. Sin ninguno, el alcance es desconocido, no nulo, y nada se entierra sobre esa ignorancia.
payload.get(...)no esrequests.get(...).
Orden de magnitud, medido en Hermes Agent (4 457 archivos Python): 98 s, 365 findings de los cuales 25 high — 3 por encima del umbral por defecto una vez aplicada la memoria.
Qué parámetro llena el argumento
Un helper del que un parámetro alcanza un sink no vuelve peligroso todo lo que se le pasa. El motor emparejaba sin embargo al llamador con el conjunto de sinks del callee, sin mirar dónde aterrizaba el argumento:
def helper(safe, cmd):
os.system(cmd) # seul `cmd` atteint le shell
helper(sys.argv[1], "ls") # la donnée va dans `safe` — et c'était rapportéLa posición se lee ahora en el sitio de llamada, y el nombre para un argumento nombrado. Es lo que el motor de JavaScript ya hacía por su lado.
Medido, una vez posible la ejecución: de 83 238 resoluciones solicitadas al
analizar Thot y hermes/tools, el 90 % designa un parámetro único y el 9,8 %
no designa ninguno; el 0,1 % restante queda abierto (f(*rest), f(**options)) y
mantiene la respuesta amplia. El callee ofrecía 2,68 parámetros de media, por tanto
cerca de dos tercios del espacio de búsqueda desaparecen — el análisis de
hermes/tools pasa de 11,4 s a 9,5 s.
En cuanto al número de findings, en cambio: ningún cambio. Thot, prime y
hermes/tools devuelven exactamente los mismos 151 candidatos antes y después, de los cuales
cero inventados — la propiedad de seguridad se sostiene sobre el terreno. Cuatro caminos de
mancha se acortan, nada más. La forma corregida arriba es real
y el test lo prueba, pero no se produce en ninguno de los tres árboles
medidos: este correctivo compra velocidad y caminos más justos, no
menos ruido.
Dos formas no dicen nada: helper(*args) extiende un número desconocido de
valores, y helper(**options) no nombra ninguno. Ahí, el motor mantiene la
respuesta amplia que daba antes — el conjunto retenido está siempre incluido
en el de antes, por tanto afinar solo puede quitar un finding, nunca
inventar uno.
El receptor se omite basándose en la firma del callee, no en la sintaxis
de la llamada: Runner().go(x) se resuelve en un go sin punto, y Cls.m(obj, x)
igual que obj.m(x) son ambos un atributo. Como un método casi siempre se
llama enlazado, es self al frente de los parámetros lo que decide — la llamada no enlazada
es la única forma leída un nivel demasiado corto.
Lo que el grafo no puede seguir
Un defecto alcanzado por un camino que el análisis no resuelve — un handler guardado en una tabla de dispatch, una vista decorada, una llamada sobre una variable cuyo tipo es desconocido — no es un defecto inalcanzable. Thot distingue los dos:
HANDLERS = {"run": run_command} # aucun appel : le graphe ne voit rien
@app.route("/ping") # enregistré à l'import par le décorateur
sandbox.run("pytest") # plusieurs `run` répondent à ce nomEn los tres casos el alcance es desconocido, no nulo, y el finding mantiene una penalización ligera en lugar de ser enterrado. En Hermes: mismos 365 findings, pero 60 suben un nivel. Una función que nadie llama y que nadie menciona permanece, por su parte, correctamente devaluada — si no, el filtro dejaría de ser un filtro.
Límites
La mancha a través de los archivos está reservada a Python: JavaScript y TypeScript están indexados, seguidos dentro del cuerpo de una función y hasta un helper del mismo archivo, y se detienen ahí por falta de grafo de llamadas resuelto — la tabla de arriba lo dice línea por línea. Las reglas por patrón, por su parte, se aplican en todas partes.
El +55,5 % es una cifra de Python. BenchProctor etiqueta django, fastapi y flask, y nada más; el catálogo de JavaScript es más estrecho que su homólogo — sin fuente «lectura en base», sin sink de registro — y sus reglas se apoyan en sus tests en lugar de en 18 300 casos etiquetados. Cuando una corrección vale para ambos motores se traslada a ambos, y el diario dice cuál se ha medido dónde.
sink.log lee el mensaje y solo él. logger.info('failed: %s', exc) confía
el valor a la maquinaria de registro en lugar de escribirlo en la cadena
que el autor ha tecleado; leer también esos argumentos vale +0,0000 en el corpus
y 4 844 findings más en hermes/, uno por cada except … as exc: del árbol.
No está cubierto, y decirlo cuesta menos que dejar que se crea.
Sin --deep, cada finding es PLAUSIBLE: detectado estáticamente, no
aún probado por ejecución. Con --deep, un finding confirmed ha sobrevivido
a una refutación adversaria — no es todavía una prueba de ejecución, que
vendrá con el repro. Y la ausencia de finding no es una prueba de ausencia
de defecto: dispatch dinámico, reflexión y metaprogramación escapan al
análisis.
La búsqueda de texto completo en las sesiones deja de ser de costo constante en los SQLite de los que fts5 no sabe remontar los rowid: el motor ahí ordena el conjunto de coincidencias en lugar de cortar en veinte. Medido en el que incorpora CPython 3.12 — 6 588 instrucciones contra 27 443 cuando el corpus se multiplica por cinco, donde un motor capaz permanece en 2 215 y luego 2 698. Un test lo verifica y nombra la versión cuando no lo permite.
Desarrollo
cd Thot
uv run pytest -qInstalado en modo editable: el código fuente es la referencia inmediatamente. En cambio, si
pyproject.toml cambia (nueva dependencia), hay que relanzar
uv tool install --editable --from . thot --force.
El núcleo determinista (codemap, taint, scope, scoring, store,
report) no depende de ningún agente y no toca la red — un test lo
verifica y hace fallar la suite si eso cambia.
Spec y planes: docs/superpowers/.
This server cannot be deployed
Maintenance
Related MCP Connectors
38 MCP tools: Rust computation, Discovery, composition, counterexample scans and signed checks.
Hosted code graph over MCP: exact callers, dependencies, and cross-repo blast radius for AI agents.
Self-hosted MCP server: 26 deterministic dev, security, and EVM tools.
Multiple MCP tools, persistent graph memory, token-saving data pointers, and more.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to search code by meaning, explore codebase structure, store and query knowledge with temporal facts, and read source code through a set of MCP tools.386 npm7MIT
- AlicenseNot gradedqualityDmaintenanceEnables parsing, indexing, and querying source code as structured knowledge, providing code exploration, spec generation, and migration tools via 20 MCP tools.MIT
- FlicenseNot gradedqualityDmaintenanceProvides code intelligence by indexing source code into SQLite and offering MCP tools for symbol search, flow tracing, and context retrieval to assist with code navigation and understanding.-
- AlicenseNot gradedqualityAmaintenanceMCP server for persistent code exploration and diff-aware review, exposing explore and review tools to map call paths, blast radius, and generate explainable risk-scored reports.3 npm2MIT