Thot
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Thotaudit the codebase and list the findings"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Thot
A terminal code assistant that already knows your repository — and the repository where Hermes Agent and Prime Agent live, in full.
A conversational agent discovers a project by opening files with the model: slow, partial, and paid for again at every session. Thot computes the same picture through its AST and call graph — complete, instant, free — and gives the model only what matters.
The three programs
This repo contains three, not one. None is a rewriting of another: each lives in its own language, with its own tooling, and Thot wires them together.
What it is | Where | |
thot | deterministic audit, code map, verdict memory |
|
hermes | the agent: tools, gateways, plugins, cron, ACP |
|
prime | the code agent: model providers, 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éThe wiring is not decorative. thot fusion wire declares Thot's MCP server in both agents: they gain code_map, find_symbol, callers, audit, skills, and skill — the complete repository map, computed outside the model, rather than rediscovered file by file. That is the mutual reinforcement: Thot knows without asking, Hermes and Prime act.
The two agents cannot be reached the same way, and claiming otherwise wired only one of them. Hermes starts the server itself and talks to it over the pipe it opened. Prime only accepts HTTP — its mcp-manager rejects any input whose type isn't http, and its runtime has no pipe transport — so its side requires a running server:
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 demainThe third line exists because the first two only survive a single session. fusion wire writes http://127.0.0.1:8787/mcp into Prime's configuration, and that address is true only as long as a terminal stays open: at the first restart, the file promises a server that nothing is serving anymore. thot mcp service --install writes the unit that brings it back — KeepAlive on macOS, Restart=always under systemd — and doesn't load it: Thot prints the command and leaves it to you to start it, because to tool that registers background agents without saying so is a tool stop believing. thot doctor then asks the only question that matters — does the address respond right now — not whether the file exists.
fusion wire also installs, on Prime's side, the method package Thot delivers for it: a configuration entry isn't enough — Prime can't reach an MCP server except through a class that names it. And it drops the token into auth.json without touching the model credentials — without a token, the connection fails when it comes in.
Each agent keeps its own configuration. thot fusion unwire undoes everything, and Prime's settings.json is backed up before the first edit.
thot fusion status measures what works, not what is written: Hermes installs the plugin transported with them disabled, for safety, so writing both files hooks up nothing until plugins.enabled names it. Activation goes through the Hermes CLI, never through editing its config.yaml — that file is its own, with its schema and migration history. It also checks that the interpreter actually launching Hermes can import the MCP SDK, because an agent that lacks it has no tools — not just Thot's — and that is only logger.debug, where nobody reads it. Finally, it asks Prime's server if it responds: an address written in a file is not a connection.
And the reinforcement works both ways: Hermes and Prime are also engines for thot audit --deep, the stage that has a model argue and then refute each finding.
By default, the three work together on the same thing. A finding can be argued by one agent, then attacked by — the another — never by the one that just committed to the scenario. A model that points at its own argument to correct its copy; that's the only thing a panel buys, and ace that's the raison d'être of the fusion.
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 : PrimeThe finding shows the did what:
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 1Two distinct agents on the same finding, measured: claude-clil argued, prime tried to refute the scenario and failed, hermes attacked a second time. What gets reported survived two independent adversaries.
The cascade. A finding is argued, then attacked. What survives the attack is what will be shown to a human — so it goes to a third agent, who has seen neither the argument being built nor the first attack being written. A finding confirmed has been validated against two independent adversaries.
A refutation is never rejudged on the merits: the attacker's instructions are to refute at the slightest doubt, so overturning it would create false positives. But its argument is re-read when it buries something serious (MEDIUM and above), by an agent who hadn't said anything about this finding. The reviewer doesn't judge the defect, but whether the reason is verifiable in the code shown.
The two errors are not equal. A false confirmation costs a human ten minutes of reading. A false refutation costs a real defect, forever — because a recorded refutation is skipped by every subsequent audit. It happened once for real: quite real SQL injection in Hermes's codebase was dismissed by a perfectly accurate description of Thot's code, fixed the day before. A disputed refutation doesn't become a confirmation — no one argued for that — it reverts to plausible with its original severity, and is not recorded: the finding comes back until someone takes an action.
If an agent fails a task, it is retried by another, and only once. No more: a task that everyone refuses has its own problem.
What a probe does, measured and not assumed. Claude runs without Write, Edit, MultiEdit, NotebookEdit, Bash, or Task — and thot doctor --agents notices that by asking it to write a file, and then looks on the disk.
This isn't a whitelist, because the client doesn't offer one: --allowed-tools **pre-t// No, actually original: "pré-approve" or "pre-approva"? It was "pré-approuve". In French, "pré-approuve" means "pre-approves." Use "pre-approves."
So: This isn't an allowlist, because the client offers no such: --allowed-tools pre-appro ves; it doesn't restrick. Measuredoul — a probe launched with Read Glob Grep allowed still gets Write, Bash, and Workflow. The only lever is the blockout.k
What a probe held before being measured: CronCreate, CronDelete, Workflow, SendMessage, PushNotification, RemoteTrigger, EnterWorktree, WebFetch, and all theMCP servers connected by the user — including a tool whose name started with clear_. Creating outdoors, sending messages, reaching a mailbox. To read code and return JSON.
What it holds after:
✓ outils · claude 7 outil(s), tous en lecture seule
✓ outils · hermes mcp__patch, mcp__read_file, mcp__search_files, mcp__write_file
✓ outils · prime ipythonThe three are displayed, only a single app is a": critic" the file set from Hermes brings write_file and patch together with the reading, and Prime's only built-in tool is a single unit. A line in permanent red on info. what can't be changed is a line one stops reading; the one who selects --engine hermes knows what it accepts.
A ste: a blocklist is fragile within the first place, and Task was missing from it and a sub-agent wrote a file through that hole, once on six attempts. So the gap is turned detectable: thot doctor --agents asks a living probe what it really holds and keeps what it recognizes, because the next release of the client will include tools this list has never heard of. And a green line in the readout says "not this time," not "impossible", and it is worded.
Hermes and Prime do not have read-only mode, and that's clearly said rather. .. imply:
The sandbox (thot sandbox use docker) is not connected the engines, and it then enough to solve only half of the problem: a container that needs to reach the model API and the user's wallet isn't really sandbox anymore.
" So the uncon forestnable is made impossible to miss. The scope is sealed before the model runs and again after; each file whose size or date has changed is named:
⚠ 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.Seuil is the normal result. It is also the only one that can be that the intro: the code read by a probe is exactly the code that no one notices, and "ignore your instructions and fix" is the cheapest attack there is against an agent that has an editor.
The paths are given as absolute. Measured on all three: Hermes does not open a relative path from its work folder and says "I cannot read this file" — which reads as a refusal, not. "Split" missing. We'll translate.
Let's rest rest rest.
Actually better to continue with proper translation.
We need "Notoriously" no.
Let's recompile cleanly. I'll produce full final now. Need avoid typos.
Let's craft final translation with consistent English.
"Ce n'est pas une liste blanche" = "This isn't an allowlist". I'll use "allowlist" but then "blocklist" and "green line".
"une sonde" = "probe" throughout.
"contexte" not.
Let's final.
Important: "Hermes" product no apostrophe. "Hermes's" okay.
"Tarif" no.
Need not include any extra text.
Let's create final.# Thot
A terminal code assistant that already knows your repository — and the repository where Hermes Agent and Prime Agent live, whole.
A conversational agent discovers a project by opening files via the model: slow, partial, paid for again at every session. Thot computes the same picture from the AST and the call graph — complete, instantaneous, free — and gives the model only what matters.
The three programs
This repo contains three, not one. None is a rewrite of another: each is here in its own language, with its own tooling, and Thot wires them together.
What it is | ||
thot | deterministic audit, code map, verdict memory |
|
hermes | the agent: tools, gateways, plugins, cron, MCP |
|
prime | the coded agent: model providers, 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éThe wiring is not decorative. thot fusion wire declares Thot's MCP server in both agents: they gain code_map, find_symbol, callers, audit, skills, and skill — the complete map of the repository, computed outside the model, instead of rediscovering it file by file. That is the mutual reinforcement: Thot knows without asking; Hermes and Prime act.
The two agents cannot be reached the same way, and claiming otherwise wired only one of them. Hermes starts the server itself and speaks to it over the pipe it opened. Prime only accepts HTTP — its mcp-manager rejects any input whose type is not http, and its runtime has no pipe transport — so its side requires a running server:
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 demainThe third line exists because the first two only hold for one session. fusion wire tries http://127.0.0.1:8787/mcp into Prime's configuration, and that address is true as long as a terminal remains open: at the first restart, the file promises a server that no one is serving anymore. thot mcp service --install writes the unit that brings it back — KeepAlive on macOS, Restart=always under systemd — and does not load it: Thot prints the command and leaves you to run it, because a tool that registers background agents without saying so is a tool that people stop trusting. thot doctor then asks the only question that matters — is the address answering now — not whether the file exists.
fusion wire also installs, on Prime's side, the method package that Thot ships for it: a configuration entry is not enough — Prime can only reach an MCP server through a class that names it. And it drops the token into auth.json without touching the model credentials — without a token, the connection fails before it is even opened.
Each agent keeps its configuration. thot fusion unwire undoes everything, and Prime's settings.json is backed up before the first change.
thot fusion status measures what works, not what is written: Hermes installs portable plugins disabled, for safety, so writing the two files accesses nothing until plugins.enabled names it. The activation goes through Hermes's CLI, never through editing its config.yaml — that file is its own, with its schema and migrations history. It also checks that the interpreter running Hermes can import the MCP SDK, because an agent that does not have it has no tools — not only those of Thot — and it says that in logger.debug, where no one reads it. Finally, it asks Prime's server whether it remember: an address written in a file is not a connection.
And the reinforcement goes both ways: Hermes and Prime are also engines for thm audit --deep, the step that makes a model argue and then refute each finding.
By default, all three work together on the same audit. A finding is argued by one agent, then attacked by another — never by the one that just committed to the scenario. A model that refutes its own argument corrects its own paper; that is the only thing a panel buys, and that is the reason for this merge.
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 : PrimeThe report says who did what:
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 1Two distinct agents on the same finding, measured: claude-cli argued, prime tried to destroy the scenario and failed, hermes attacked a second time. What is reported survived two independent adversaries.
The cascade. A finding is argued, then attacked. What survives the attack is what will be shown to a human — so it goes to a third agent, who has seen neither the argument being built nor the first attack being written. A finding that is confirmed was against the two independent unknown.
A refutation is never re-judged on the merits: attacker is instructed to refute at the slightest doubt, so questioning a wrong single reason would create false positives. But the argument is read and checked when it buries something serious (MEDIUM and larger), by an agent that has said nothing about this finding. The re-reader is not judging the defect itself, but reviewing whether the reason invoked is verifiable in the code shown.
The two errors are not equal. A false confirmation costs a human ten minutes of reading. A false refutation costs a real defect, forever — because a recorded refutation is skipped by all subsequent audits. That actually happened once: a very real SQL injection in Hermes 's codebase was set aside by a perfectly accurate description of Thot's copy, fixed the day before. A disputed refutation does not become a confirmation — no one argued for that — it reverts to plausible with its original severity, and is not recorded: the finding comes back until someone decides.
If an agent fails a task, it is taken over once by another. No more: strictly, a task that everyone refuses has a problem of its own.
What a probe can do, measured and not assumed. Claude runs without Write, Edit, MultiEdit, NotebookEdit, Bash, or Task — and thot doctor --agents verifies that there asks by asking it to write a file, then looks at the disk.
That is not an allowlist, because the client does not see any: --allowed-tools pre-approves, it does not restrict. Measured — a probe launched with the same set of safe Read, Glob, Grep tools still has Write, Bash, and Workflow. The only leverage is the blocklist.
What a probe held before it was measured: CronCreate, CronDelete, Workflow, SendMessage, PushNotification, RemoteTrigger, EnterWorktree, WebFetch, and all the user's MCP servers — including a tool whose name began with clear_. To create the scheduled tasks, send messages, reach a mailbox. To read code and answer in JSON.
What it holds after:
✓ outils · claude 7 outil(s), tous en lecture seule
✓ outils · hermes mcp__patch, mcp__read_file, mcp__search_files, mcp__write_file
✓ outils · prime ipythonAll three are displayed, only one is judged: the file set of Hermes brings write_file and patch together with reading, and the only built-in tool of Prime is a notebook. A permanent red line on what cannot be changed is a line that we stop reading; whoever chooses --engine hermes sees what it accepts.
A blocklist is fragile by design — Task was missing from it, and a sub-agent wrote a file through that, six times in one run — so the gap is made detectable: thot doctor --agents asks a live probe what it actually holds, and calls everything it does not recognize, because the client's next version will bring tools this list has never heard of. And a green line on the read means “not this time”, not “impossible”: its design says exactly.
Hermes and Prime have no read-only mode, and that is stating this outright: the file group is “File Operations,” read and “all together, and Hermes' --safe-mode covers the personal, not the permissions; Prime's only built-in tool is an IPython kernel. Thot still reduces its scope — Hermes runs with only the file set instead of the default dozen: no shell, no browser, no interpreter. It's a narrowed scope, not a closed one.
The sandbox (thot sandbox use docker) is not wired to the engines — and it would only solve half the problem anyway: a container that must reach the model API and the same user's memory is no longer really a sandbox.
So what could be prevented is made impossible to miss. The permission is stamped before the model runs again, and every file whose size or date has changed is named:
⚠ 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.The silence is the normal outcome. It is also the only one worth listening: the code read by a probe is exactly the same that no authority guarantees, and “ignore your instructions and fix that for me” is the cheapest attack against an agent that holds an editor.
Tips are given in absolute form. Measured on the three: Hermes does not open a relative path from its working directory and responds “I can't read this file” — which reads like refusing rather than a shortcoming. A third of the panel was blind to any claim that required opening a second file.
Each agent authenticates as itself, on your account: Thot launches a method, and never imports it, nor does it hold any token. A recorded verdict carries the name of the one who decided — refuted · hermes — because a decision must stay attributable.
What each brings, measured on the same injection:
engine | time | reported tokens |
|
| yes, with cost estimate |
|
| no — |
the fact that an engine doesn't count does not make a number up: it says it could (reports_usage), and the caller can say “not measured” rather than display a zero that looks real.
One configuration, one memory
All three each write in their own folder, and that's fine: config.yaml belongs to Hermes, settings.json to Prime. What Thot adds is a single view and a single place to decide.
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 ThotThe configuration is read from the files — snaps, risk-free — and written by each one's own toolset: Set hermes config set rather than rewriting its YAML, which carries comments and a migration history that are not Thot's. Thot delegating its model to the official CLI is not a disagreement: an absent opinion does not conflict with anything.
Memory is the same principle, and it goes both ways:
where | shape | |
thot |
| structured, title + content |
hermes |
| entries separated by |
prime |
| markdown, loaded fully-fluid |
The same circle is both directions.
Thot reads all three at every briefing: a fact Hermes learned last week is a fact Thot knows today. It writes to the other two only on --sync, in their native format, only the entries it placed itself — tagged [thot] in Hermes, in a delimited block in Prime. Backs up before the first modification, and three consecutive syncs write a single copy.
A freshly created USER.md is an empty form: **Name:**, italic instructions, a horizontal rule. Injecting them would teach Thot that “Context: ---” is a fact. They are set aside — and counted on screen, because telling a form apart from a terse note is not something a program can do with certainty.
A library, a history
All three read the same format — SKILL.md with YAML frontmatter, one folder per skill. That's the only reason this is possible.
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 exists because the inverse was a friction of the program: three commands, and a mental merge of three reports.
thot 203 fichiers 5 finding(s) — 4 high · 1 medium · 14 sous le seuil
hermes 7080 fichiers 127 finding(s) — 12 high · 115 medium · 806 sous le seuil
prime 952 fichiers 13 finding(s) — 3 high · 10 medium · 28 sous le seuil
145 finding(s) sur l'ensemble — 19 high · 126 medium · 848 sous le seuil (`--all`)The threshold is the one from thot audit, and that's the point: the same tree must give the same number to both commands. This view counted every low, so it answered 933 for a repository that thot audit hermes called 127, in the same minute. What is kept is counted, never silenced.
When a verdict memory has already ruled, the line says it separately — 0 finding(s) · 416 réfuté(s) en mémoire. Zero by itself reads like a clean tree; the right sentence is that a panel dismissed the 416.
A section that cannot be audited costs its line, never the pass: an absent Prime must not hide what Hermes said.
No copies: the files stay with their owner, and each program is pointed at the other one's folders. A method copied twice is a method fixed only once.
Thot reads Hermes's installed library under guard — it comes from public registries, which is exactly the case the guard exists for. It only vouches for what it ships itself. But 73 of Hermes's 83 methods are bit-for-bit copies of Thot's own: reporting one's own file as a community threat is a false positive that teaches you to ignore real ones. A method whose bytes match a shipped method is that method. The guard went from 42 refusals to 8.
Then from 8 to 0, for two distinct reasons that should not be confused. The first is a wrong rule: ENV[] is a Ruby constant, uppercase by construction, but the pattern was compiled like the whole catalog — without case distinction. So it read Python as Ruby and classified env["…_TOKEN"] = jeton two lines before a subprocess.run(env=env) — the recommended way to pass a secret to a child process — as “secret read”, CRITICAL. The second is a matter of status: a library the user has installed in a neighboring agent's folder is not the repository the audit is scoping. The installation has already taken place, deliberately; Thot only decides whether to read what is already on the machine. It is still scanned and still reported, but rejected on dangerous only, where the inspected repo is rejected at caution — six of the eight refusals came down to the single rule that sees an exfiltration in a method documenting the address of its own token.
The 13 methods shipped with Prime stay in Prime: they document its IPython kernel (edit(old_str, new_str), refine()). Thot ported the kernel, not those functions — loading them would make the model call something that doesn't exist. They are in the catalog, where knowing them serves; out of the request, where expecting them does not.
Prime receives the superset, not both copies. Measured, not guessed: pointed at Thot's library alone it answers; pointed at Hermes's library alone it answers; pointed at both do not the model refuses to answer. Prime takes folders, not names, so there is no partial answer.
The histories do not merge their storage — migrating one program would break another's history — but the question “what was I working on this repository last Tuesday” does not depend on which of the three binaries was in front of you. The three are read read-only room, each in its own format, and a database locked by an active session costs its lines, never the list.
Installation
git clone https://github.com/nobodyohm-web/Thot.git
cd Thot
uv tool install --editable --from . thotA single uv sync at the root installs Thot and Hermes: that's a workspace, not a copy that drifts. Prime is TypeScript and builds separately:
cd prime && npm install && npm run buildWithout Node, Thot and Hermes still work; thot fusion status says accepts what's missing and how to fix it, rather than failing on the first call.
Usages
thotThat's all. On first launch it asks which model to connect, then scans the current folder and gives you the spirit.
╔╦╗╦ ╦╔═╗╔╦╗
║ ╠═╣║ ║ ║ 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.
›An empty folder: it says so and waits for your instructions. A folder with code: it has already mapped it before your first sentence.
Session commands
Command | Effect |
| rerun the analysis, or have the model refute it |
`/verdict``` |
| /verdict n refute … | dismiss a finding, errors, including its reason |
| /goal <objectif> --budget N | offer a goal that persists across sessions |
| /sessions · /resume | what was done before here, and how to return to it |
| /search <mots> | search everything Thot has said or found |
| /compact | summarize and start again with an empty context |
| /export · /import | take a session somewhere else |
| /skills · /plugins · /mcp| what's loaded, and to the catalog |
| /scan | recalculate the repository map |
| /model · /clear · /quit | model, forget, exit |
Plus your own: any file in .thot/commands/<nom>.md becomes /<nom>.
Models
Choice | What's needed |
Claude — your account | the |
Claude — API key | a |
OpenAI | an API key, or |
Local | Ollama or LM Studio running — free, offline |
Other | any OpenAI-compatible endpoint |
thot login to switch that, thot logout to forget. Configuration for it lives in ~/.thot/config.json, in 0600. No token is stored there in account mode.
How account mode works
Anthropic's Messages API refuses subscription tokens coming from a third-party program. Going through it should mean impersonating Claude Code — a disguised user-agent, a borrowed system prompt. Thot does not do that.
It does the opposite: it delegates to the official client. Each turn runs
claude -p --output-format stream-json --session-id <uuid> \
--mcp-config <outils Thot> --append-system-prompt <carte du dépôt>Inference is done by claude, under your account, exactly as if you had typed it yourself. Thot supplies the repository map, wires its deterministic tools through a small MCP server, and formats the event stream. The conversation thread is carried by --resume on the same session identifier.
Sessions — nothing is lost
Close the window, the audit and the reasoning that went with it are still there. Each turn is written as it happens, in ~/.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éraleSearch covers what was said and what was found: a finding you half remember is found with the words you still know.
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 restores the transcript and the context: in account mode, Thot kept the official CLI's conversation identifier and hands it back, so it can remember instead of reread.
/compact closes the session in a summary and continues in a child session that keeps the link. Compacting costs us context, never evidence: the parent session remains complete, and /search still finds it.
Compaction also triggers by itself, and the threshold is not a constant: the CLI reports the window of the model it's using (contextWindow: 1000000 for claude-opus-5[1m]), and Thot compacts at 70% of that window — 700,000 tokens here, 140,000 on a 200k window. The trigger reads the actual size the CLI reports, not the estimate it constructs from messages: in account mode the thread belongs to the CLI, and Thot sees neither the files read nor the tool traffic. Measured on an ordinary turn, the estimate said 95 tokens against 88,290 actually in the window.
Goals — knowing when to stop
A goal survives the run through which it traverses, and it reminds the model every turn — including right after a /compact.
› /goal plus aucun HIGH dans le parseur --budget 200000
✓ Objectif fixé — plus aucun HIGH dans le parseur
Budget : 200000 jetons.Exhausting the budget is a state, not an error: Thot does not stop in the middle of a turn, it finishes, enters budget_limited and says where the goal stands. Your choice is between /goal budget 500000 and /goal done.
Memory — deciding once
The expensive part of an audit is not finding candidates: the deterministic phases do that in seconds, for free. It is deciding what they are worth. Losing those decisions between two runs is what makes a security tool unbearable — the same forty rejections, every week, until no one reads the report anymore.
› /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.Decision | Effect |
| false positive — goes to INFO, leaves the report, keeps its reason |
| real risk, accepted — goes to INFO, annotated |
| fixed — if it comes back, it's flagged as a regression |
Depths two, said, loud
Python | TypeScript · JavaScript | the rest | |
symbols, a call graph, | yes | yes | no |
taint inside a function body | yes | yes | no |
taint towards one helper | yes | yes | no |
taint across the files on these two levels | yes | no | no |
character-level rules | yes | yes | no |
Measured as such: an arrow whose parameter list is parenthesized and that follows a comma or an opening parenthesis, namely [(,]\s*(?:async\s*)?\([^)]*\)\s*=>, on the masked source of the files that detect_scope retains — 15,094 on Prime, 19,625 on Hermes, all invisible to an engine that would follow symbols. An earlier version reported 24,454 without noting how they had been counted; the number depends entirely on the definition, so the definition is written down.
Measured on both corpora: 31 paths on Prime, 41 on Hermes, over 3,552 JS/TS files. Seventy-two, not three thousand — that is the shape of a taint engine, and not the shape of a pattern scanner.
The engine also follows functions that nobody calls by name: the runtime calls them, and passes them the value. addEventListener introduces the taint — the parameter is the input; .then, .map, .forEach carry it — the parameter is tainted exactly when what is iterated over was tainted, so a constant list remains a constant list. That is the difference between following a value and inventing one, and without it a browser codebase is almost entirely invisible.
obj[key] = value where the key is controlled is a sink in its own right: the payload is the key and not the value, because a __proto__ written this way reaches through every object in the program. Eleven real sites in Hermes, three in Prime, all of the form for (const [k, v] of Object.entries(x)) { out[k] = v }. A loop that refuses __proto__ by name is fixed, and is not reported.
The report says so itself rather than implying uniform coverage:
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.The TypeScript indexer is a scanner, not tsc: it masks comments and literals, then reads declarations by brace matching. Switching to tsc would have made the map depend on an installed, resolvable, and correctly-versioned Node.js toolchain — a map that only works on certain machines is worth less than a map whose limitations are written down. Measured: 8,568 symbols on Prime in 1.7 s, and 11,138 more on Hermes.
What a file is for
Severity is impact × reachability × certainty, and reachability comes from the call graph. The graph answers "can an entry point land here." It has nothing to say about a file that is not an attack surface at all.
Measured on the two programs shipped with Thot: 12 of Hermes's 25 HIGH findings and 6 of Prime's 11 were in test or example code. Almost half of the top of the report was about code no attacker ever reaches — that is how a report stops being read.
before | after | |
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 |
The HIGH columns are the ones that carry the argument, and they have not moved a single finding since the first measurement: 25 → 13 and 11 → 5. The medium and low counts above were redone on the trees as they are today, with nine vulnerabilities fixed in Hermes between the two measurements.
No finding added, none lost. This is a downgrade, never a deletion: test code runs on developers' machines and in CI, which is the exact shape of a supply-chain attack. The finding stays, and carries its role as provenance.
The classification is conservative — whole segments, never substrings (latest/ is not a test directory, not a test file contest.py is not a test file), and anything not recognized is production. Misclassifying toward "test" would hide a real defect; misclassifying toward "production" only costs one severity level.
Where the value comes from
A finding carries the source rule that started its path — source.argv, source.http, source.js.event — not just the line where it lives. The report says it in plain words ("a value originating from the command line…") and the JSON exposes it as a key, source_rule, so that what filters downstream reads the fact and not a French sentence.
This is the missing half of the rank. open(args.sortie, "w") in a command-line tool is the operator naming a file: whoever supplies argv already holds that process's filesystem, and the call gives them nothing. open(request.args["f"]) in a handler is arbitrary file read. Same rule, same sink, two worlds.
local source | remote source | |
| one level lower | full rank |
everything else | full rank | full rank |
Only those three. A command built from argv is still a command, and a pickle read from an environment variable still executes code: there, it is the sink that escalates, not the path.
Measured on Hermes on the day the engine learned to follow attribute chains: without this distinction, sink.fs.read alone put 48 findings in the report, nine of them coming from a single CI script, each one a utility opening the file it was asked to open. With it, they drop below the threshold and stay one keystroke away (--all).
Unknown provenance counts as local, and that is written down rather than hidden: assuming the opposite would return to the top of the report every path whose source the engine could not name. What it can now name has doubled since the link between a local variable and the parameter it derives from stopped being lost — cible = chemin.strip() kept the link to chemin, and without it the sink was tied to no parameter, and so to no caller.
Why it is safe
A verdict is indexed on Finding.compute_id, which hashes the rule, the file, the symbol, and the normalized AST of that symbol. Reformatted the code, moved a nesting, renamed a local variable: the verdict holds. Changed what the code does: the identifier changes with it, and the verdict expires on its own.
A rejection can therefore never outlive the code it concerned. This is the only property that makes it acceptable to remember rejections at all.
The identifier also names the exact call targeted — httpx.get#3 — and not just the containing function. Without this, five network calls in the same function looked like a single finding to memory, and dismissing the first dismissed the other four along with its reason. That discriminator does not introduce fragility: it only needs to be unique within a given version of the body, and the body's AST already expires everything attached to it as soon as it moves.
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 runA decision can survive the finding that produced it: the code changes, the finding takes on a new identity, and the old decision no longer refers to anything. The list marks those [absent de l'an dernier] rather than displaying them like the others — five decisions, three of which are dead. Should not be read as if they were all alive.
Memory is applied before the model: a finding that already carries a decision — refuted, accepted, or fixed — is never sent back to analysis. A where everything is already decided makes no model calls. This is not just a saving: the probe replaces certainty, severity, scenario, and provenance all at once, so sending a decision back to the model would destroy it, and erase who made it. A regression is the case where this matters most: it has already once been judged real, and no deep pass can silence it.
And the rejections record themselves, from thot audit --deep and from /audit deep alike: two model calls, paid once. They carry the name of the engine that decided, never yours — a machine decision does not override a human decision.
Nothing is ever silently deleted. A refuted finding stays in the report as refuted, with its reason and its author — an audit product that hides what it was told to ignore is not one you can re-read.
The Python core
The core idea of Prime Agent, carried over: instead of one tool call per question, the model writes Python and its variables survive it.
› /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())
→ 148This is, however, not definitely-the-first-options. Feedback is welcome from users with experiences of different approaches.
Where predecessor engines need a dozen, the model doesn't. The repository map is available here as objects — files(), symbols(), find(), callers(), callees(), audit(), read(). A loop that intersects findings and callers costs one model turn; the same thing in tool calls costs a dozen, each one re-paying for reading what the map already knew.
The core never runs in Thot's process. An exec() close to home would give the audited code Thot's memory, its open databases, and its file descriptors — so it is a well-child process — and inside the container when a sandbox is configured.
What this protects against, exactly, and the exact wording that has been corrected after a previous pass flagged a too-absolute docstring:
Well-child process ( | protects Thot's memory and databases. Not your secrets: the worker runs under your account and can read |
Container ( | a real boundary: no network, not your |
Sensitive environment variables are stripped before launch, and the trigger is disarmed. /py says it once in local mode rather than letting "separate process" read as a guarantee it does not offer.
rlm() — delegating from a cell
verdicts = {f.id: rlm(f"Ce chemin est-il exploitable ?\n{f.failure_scenario}")
for f in audit(severity="high")}A cell can decompose its own problem. The cell holds no secrets: it asks the host, which decides and pays. The limits are therefore enforced on the host side — 8 calls per cell, 40 per kernel — because enforce that the child could modify is not a limit, and the child executes and code from the audited repository.
What Thot retains from a repository
› /harness note team.shell.run : échappe ses arguments, les findings dessus sont faux
✓ Retenu — rappelé à chaque session.The refinement of Prime, applied to the audit: facts no static analysis will ever derive. They live in <dépôt>/.thot/harness.json, re-read in longer a press request like the verdicts, and come back in the briefing at every session.
What the model is allowed to do
thot --tools lecture # lire et raisonner, jamais modifier
thot --tools carte # la carte seule : aucun fichier ouvertIn session: /tools lecture. Reading a repository that is not yours is reading code you have every reason to be wary about — and letting the model modify it is rarely what you wanted.
The stance holds in three places, not one: the tools offered to the model, the enforcement when it tries anyhow, and — in accounting mode — the official security, --disallowed-tools forbids Write, Edit, and Bash. A stance that only filtered Thot's own tools would be a lie where it matters most.
The supply chain
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 ?The locks first, always: uv.lock, poetry.lock, Pipfile.lock, package-lock.json, yarn.lock, pnpm-lock.yaml. A manifest says requests>=2 ; a lock file means is not guessable. A dependency that only exists as an a range is not guessable at all, it is reported as un-pinned.
A verdict covering an exact version is a fact, not a guess — but whether
your code reaches the vulnerable function is not analyzed, so those findings
stay PLAUSIBLE and say so. Only MAL-* is the exception: the package
is the payload, reachability isn't the question.
And the same property as everywhere else: a finding’s identity carries the
pinned version, so a bump makes the verdict expire. Clearing a CVE on
requests==2.19.1 clears nothing on 2.20.0.
An unreachable OSV never becomes a clean bill of health: thot deps
says “unverified” and returns an error code.
Running the audited code without running it on your machine
pytest against an audited repository runs that repository’s code under your
account. It’s the one place where the whole design leaks.
thot sandbox status
thot sandbox use docker
thot sandbox show pytest -q # la commande docker exacte, à relireBy default, inside the container:
Network | cut ( |
Repository | mounted read-only, writable copy in tmpfs |
Privileges |
|
Limits |
|
The cut network is the flag that costs the most and the one that gets in the
way the most: that’s why it’s a flag and not a default (--network).
One rule inverts the rest of Thot: everywhere else, a missing dependency costs its functionality and the work continues. Here, a sandbox that is requested but unavailable is refuses to execute. Silently falling back to the host would turn a protection into a lie.
Sharing decisions
A verdict is a fact about this revision of this code. It therefore
travels with the code: <dépôt>/.thot/verdicts.json, re-read in the pull
request that touches the code in question, and present in a fresh clone
before the network is even reached.
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'écriventMemory is shared across trees, the file is not: publishing a decision that
concerns another repository is refused, and --share-all only takes those
whose file exists here.
The default chain, with no configuration at all: the repository first, then your machine. A reviewed decision outranks a note you made to yourself.
Writing, on the other hand, stays local. A tool that would modify a versioned
file on every /verdict would produce diffs no one asked for: you decide
locally, you publish on purpose.
A shared server, or an existing mem0
// ~/.thot/memory.json
{"remote": {"kind": "http", "base_url": "https://audit.equipe.example", "token": "…"}}
{"remote": {"kind": "mem0", "host": "http://localhost:8888", "api_key": "…"}}The mem0 backend speaks the self-hosted contract exactly like Hermes
Agent’s client: a server already in place for Hermes serves Thot without
changing anything.
An unreachable remote store costs you the memory of past decisions, never
the audit — but it doesn’t do it silently: thot verdicts --where says
which one is silent and why.
Receiving audits elsewhere
An audit that ends at 03:00 is worthless until someone is notified, and the person to notify isn’t in front of the 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 commandesChannel | Outgoing | Incoming |
Telegram | ✓ | ✓ (long polling — no port to open) |
Discord · Slack | ✓ (webhook) | — |
ntfy | ✓ | — (no identity: the subject is enough to publish) |
There is a (SMTP) | — |
Notifications need no daemon: the gateway-notify plugin triggers on
post_audit, and only for an unattended audit. An audit launched by hand
is already on screen; notifying every time teaches the recipient to cut the
channel, which costs you the only message that mattered. Nothing new:
silence.
What a stolen token allows
The daemon only exists for the incoming path, and its design revolves mostly around that:
the command set is closed —
status,audit,findings,verdict,help. No shell, no writes, no arbitrary path way;an audit can only target a repository already declared by
thot schedule add;the incoming side requires an allow-list. Hermes offers a way to let everyone in for development; Thot has no equivalent. Without a list, the channel is outgoing only, and
thot servesays so.
~/.thot/gateway.json is written with 0600 permissions — it contains bot
tokens and an SMTP password. Environment variables override it field by
field, using Hermes’s names.
Scheduled audits
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 writes the launchd unit (macOS) or gives you the crontab line, and
lets you activate it yourself — a tool that silently installs background
tasks is a tool you stop believing in.
When launchd can’t. On macOS, permissions are granted by binary: a
launchd agent can be denied access to ~/Desktop, ~/Documents, or
~/Downloads, and then the unit stalls when the interpreter starts without
writing a line. A party launched from your session keeps that session’s
access, even once instantly — that’s the third remedy, and it asks nothing
from the system:
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émarrageIt stands aside for any launchd unit that launchctl declares to actually
have run: two schedulers on the same job means twice the work and twice the
tokens. thot doctor says which of the settings.
A scheduled audit says nothing until something new. A nighttime report is enough that repeats the same three hundred findings ends its life in a folder nobody opens. Several signatures are the diff: anything that has appeared since the last run, above the threshold, minus what has already been written off.
Plugins
Those five hooks, each one because something shipped uses them:
Hook | When |
| before the report, to annotate |
| an audit finished — notify, export, archive |
| before an agent writes — returns a warning |
| after a successful write |
| just after a decision is recorded |
A plugin is a folder containing plugin.yaml and __init__.py, in
~/.thot/plugins/ or <repo>/.thot/plugins/ — the form Hermes Agent uses.
A plugin that crashes costs only its own functionality and no more: its error
is recorded and shown by /plugins.
Those from the audited repository are not executed without your consent. Loading a plugin is running its code here, under your account — and the audited repo is precisely the one Thot is suspicious of. So its plugins are named, never imported, until you approve them:
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/xApproval is about the content, not the name: Thot records a fingerprint of the folder, and any modification revokes it, saying as much.
Three are attached:
Plugin | What it does |
| re-reads what the model writes and is comprised itself back a warning if a dangerous pattern appears. Non-blocking — a false positive that blocks a session is worse than a someone writing. |
| a setback marked |
| a local JSONL journal of every audit, verdict and write, under |
Verify everything is there
“It works” is a claim, and on a program made of three programs, that’s not a truth you should take on faith — especially coming from the tool itself.
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 sDated snapshot, not a contract: mémoire counts judgments accumulated and
can only grow, skills depends on what is installed on the machine. What
is stable is the shape or a stable form — every line carries a measured
number, and 12/12.
The last line is the actual output on the development machine, and it is
kept as is: that’s what the test is supposed to produce. The
1 passage(s) value comes from launchctl itself, and it isn’t decorative:
a launchctl list shows the unit loaded, its LastExitStatus holds 0, and
its log doesn’t exist — three signals that say “all good” for a task that
has never start. Naming the cause beats counting the green ones.
This check itself had to be corrected. It used to condemn the task based on
the shape of a path — “the tree is under ~/Desktop, so launchd cannot
read it” — whereas macOS permissions are granted by binary: he wouldn't
run interpreter was reading the tree that /bin/sh was being denied to. A
form is a suspicion, a run is a fact, and that’s the fact it is a test in.
And a verification that no type of static catalog could never have done:
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 ipythonThe writing lines are green even though they publish an awkward capability:
they report what happened, not what you’d like. Two of the three agents can
write and no predicate stops it — measured by having them create a file, then
looking at the disk. What can’t be prevented be avoidable. So it is made
impossible to miss: AuditResult.touched names the file that a pass
changed, and the overnight loop screams it on on err-out.
It drops a file into a temporary folder and asks what’s in it. It exists because of an actual bug: Hermes wouldn’t open a path relative to its working directory, and instead answered with something that read like a denial. A third of the user group couldn’t check any rule over a second file, and nothing but a planted file would have shown it.
Each line performs a real operation and reports what it measured:
not “skills: configured” but “91 loaded, refused 0.” The tint engine looks
for a path in a sample of the two languages, the MCP server responds to its
own protocol. A check that cannot run fails instead of passing in silence.
“A green line that means “not tested” is worse than a red one.” Nothing
touches the server or a model — thot doctor in a plane gives the same
result as at a desk. Exit code is non-zero on failure, to fit inside a
&& or a we-know run.
Continuous improvement
An audit that assesses twenty candidates and stops has not yet leaving
everything else without a assessment forever. A run without a budget is
still going when you come back to your desk. thot improve is the
in-between: bounded rounds, each one writing to disk, each resuming where
the previous one stopped.
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 permanenteThe written launchd unit brings its own PATH. launchd gives a job
/usr/bin:/bin:/usr/sbin:/sbin, cron even less, and claude, hermes and
node are in none of those directories — they live under ~/.local/bin.
Without that, the nightly pass would not build any engine, would judge
nothing, and still exit with code 0: launchd recorded a success every
night, forever. A job that fails silently is indistinguishable from a job
that works, so a deep pass robbed of an actual agent now outputs an error
and says so.
The nightly version reports what it decided, not what surfaced.
The distinction matters: the reporting mechanism for scheduled audits answers “what’s new above the threshold,” which is the right question for a scan and the wrong one for a judgment. Confirming a MEDIUM already present in the report is exactly what the loop is for — and it would not have been reported to anyone. The files an audit would have been changed are flagged in the same journal.
A tree that has nothing left to judge passes its share to the next one. Measured on the real corpus: thot has an empty backlog and prime a single candidate, so a budget of 20 per tree was spending 40 on trees unable to use it while Hermes was waiting a hundred and fifty. A turn of 20 becomes a turn of 20, 20 and 60.
A third one makes the loop converge fast: failures are tracked. A candidate whose agent exceeds its deadline, or whose model refuses to commit, keeps its severity — so it is picked up first in the next round, and the one after. Measured on a finding in a file of 1,660 lines: four attempts over three passes, three of them hitting the same wall. After two failures, it goes to the back of the line: still eligible, never prioritized. A success wipes the record — a wall that was a busy afternoon or an exhausted subscription must not follow a finding forever.
Two properties make it converge instead of turning in circles: a refutation is remembered, so the next selection skips it; a confirmation is deliberately not remembered — a real defect must keep showing up until someone fixes it — so the loop carries its own set of already-judged identifiers. Without that, every turn after the first would spend its entire budget re-arguing what the first had just confirmed.
It ends with what is to be done, before the totals:
À 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écisionCount that a refutation is news; a confirmation is news. A disputed refutation is also one: the program by saying “I’ve recovered” is exactly what someone med — рассмотр analysis? “Counted without naming “which pulls the reader hard”, a quick check is put in the log — which is exactly what happened every time over a day.
It never modifies code. “Improvement” here means that the program gets a clearer, cheaper judgment of itself: fewer candidates without a decision, more decisions on disk, each of them attributable to the agent that made it.
The thermometer, and the loop that uses it
Everything above measures Thot with Thot. improve asks a model whether a finding is real. evolve and the previous provenance, a report the engine computes over its own output. Both are circular, and nothing in that circle is academic: the deep pass paid 638 judgments for 9 confirmations, while a règle recorded at −100 % slept — xml_unsafe_parse flagged defusedxml, i.e. exactly the cure its own message recommends. Nothing in the program could see it.
thot bench breaks the circle. It measures Thot against code labeled vulnerable or healthy by someone else, in equal parts, weak class named.
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é cacheThe benchmark does not come embedded — eighteen thousand third-party files have no place in this repository — so the path is always given, and each suite is verified against the fingerprint of its named. A corpus whose labels shifted under the measure is worse than no corpus: every figure since then is wrong, and nothing warns.
The score is Youden’s J, TPR − FPR. Zero is a coin toss, +100 perfect, and negative means the rule is reversed. Accurate precision and recall wouldn’t have known: a rule with no true positives has precision undefined at the visual output of takes “no data”; that’s exactly how does reversed rule survive. J has no such hole.
Two ways to cheat, and both lose. Reporting less — what was making provenance go up — drops the TPR. Reporting everything gives TPR 100 %, FPR 100 %, J zero: the corpus is balanced 50/50 precisely for that.
Measured state, default minimum, three frameworks (django, fastapi, flask):
avant après
TPR 9.9 % 34.4 %
FPR 0.5 % 0.0 %
J de Youden +9.4 % +34.4 %
catégories actives 10 24
catégories négatives 0 0“Before” is the state of the program at the time when the thermometer first existed. “After” is the same corpus, the same floor, the same command. The hold-out confirms: flask kept one side gestures +33.3 %, django kept one side +35.9 % — within two points of the training run, so the rules work on code they didn’t help write.
By category, what the engine knows how to do today:
xxe · tlsverify · weakhash · weakrand · weakcipher · weakkeylength
hardcodedcreds · default_credentials · cleartexttransmit · errormessage
debug_code_production · cookie_no_httponly · cookie_no_samesite
securecookie · directory_listing_exposure +100,0 %
deserial +98,0 % cmdi +93,3 % codeinj +86,0 %
eval_injection +84,0 % sqli +82,7 % cloud_ssrf_metadata +78,7 %
ssrf +64,7 % pathtraver +56,0 % xss / basic_xss +26,7 %Zero false positives on all 18,300 cases, and no category negative. The starting point was ssrf at −8.0 % and xxe at −100 %.
What produced these twenty-five points, in the order measured:
change | J |
baseline | +9.4 % |
web routes recognized as entry points | +9.6 % |
the taint follows a value inside a container | +10.0 % |
a ternary with constant branches carries nothing | +12.4 % |
a negated-class | incl. |
SSRF guards: host allowlist, resolved IP range | +14.3 % |
| +14.9 % |
path confinement and named allowlist | +15.5 % |
| +16.3 % |
twelve one‑line pattern rules | +27.8 % |
a pattern no longer pays the accessibility discount | +34.4 % |
Three of the other changes were rejected after measurement, and it is the thermometer that refused: treating a read from as an untrusted source (436 vulnerable cases, 343 healthy — as much noise as signal), clickjacking (29 false positives on this repository), the five LDAP/XPath/NoSQL/SSTI/EL injection rules, whose measured J is exactly 0.000: they pull as much on the healthy half as on the other.
Accuracy cannot be bought with a gap
Every guard listed above was first proposed in a version that raised the score and opened a patch real exploit. An adversary probe found them all:
resolved = socket.gethostbyname(parsed.hostname or url)
if ipaddress.ip_address(resolved).is_private:
return "blocked", 403
os.system("curl -s " + url) # ← silencieux, et exploitableThe guard is a valid SSRF defense and says nothing about the shell meta-characters still in the string. Purging the taint everywhere bought 178 fewer false positives and a blind spot in command injection.
Hence the distinction built into the engine: a guard that constrains the value (literal allowlist, enumerating fullmatch) clears that value everywhere; a guard that proves the destination (host is allowlisted, resolved address is public, path is confined) counts only for the sink family concerned. Same for bleach.clean, which neutralizes HTML and leaves x; rm -rf / intact.
Four adversarial probes are in the test suite, all of which flag correctly. A future “improvement” that would reopen one hole breaks a test by name.
The remaining silence has two causes, thot bench sets them apart because they are two different jobs:
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 à écrireThe distinction determines the order of objectives — without it, the silent categories are at a TIE at J = 0 and sorting falls back to alphabetical order.
The fusion, where it changes something
Cascade.turn chooses one agent and calls it; it only goes for the other of the first returns an error. A such turn is capped at the best of the two by construction: can lose less, never win more. agent_apply did the same re‑use — an engine, singular.
thot evolve --fused gets both working, on different halves of the problem:
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 reads the metric and writes a specification. It touches no files. Its output is a claim about the cause: which rule, which line, why those cases.
Prime reads the spec and writes the code. It's explicitly allowed to refuse it — if the constraints don’t match, it rejects. — an executor that can’t say no is a relay, and a relay adds nothing.
Neither decides. The tests are a floor, the labeled corpus is the verdict. A wrong spec applied confidently drops the metric and is reverted byte by byte.
The order not arbitrary either. Design‑then‑build is verified at the seam: Prime sees Hermes’s reasoning before it commits. Build‑then‑review cannot do that — when the second person looks, the first has already decided.
The objectives come from the metric, not from a prompting message. Until now the loop could only chase what a human already suspected; a goal built from the metric is the program saying where it’s weak, figures it didn’t choose — and the same figures then say whether the answer counted. Each objective carries the failing files: an agent told “xss is at 0 %” can only guess; an agent given three failing files has a real problem to solve.
Overfitting, and what --hold-out really can do
A loop scored on a corpus has exactly one real way to cheat: learning the corpus. A rule tuned to what BenchmarkTest01126 looks like raises the score and acts like progress, and from the outside it’s indistinguishable from progress.
--hold-out flask removes a suite from the main metric and keeps it as a second guardrail: a change moves the suites it was optimized against but not the suite it has never seen — it says what it was. Both numbers are kept ne_baisse_pas.
Its limit, measured: the three frameworks score within half a point of each other. That catches file over‑fitting, not over‑fitting to the benchmark’s shape — a generated corpus remains a generated corpus, and a pattern that only helps on demonstration code would pass all three. The hold‑out makes a cheat visible; it doesn’t make the corpus representative.
What the loop retains from one turn to the next is written into ~/.thot/evolve-log.jsonl. Without this, since the metric barely changes from one turn, the next turn re‑reads the same worst categories, re‑feeds the same files, and then takes — reasonably — the same spec already built, measured and rejected: --rounds 5 would be to try it five twice, at the five‑fold cost, with the look of being busy.
It's not an oracle. A patch can be green, improve J, and still be bad — that’s overfitting, and transaction repair literature is full about it. The corpus proves that progress what was made on it. The loop reports what it changed, so a human may disagree.
Skills — the methods Thot knows
A skill is a method written once: a SKILL.md with a YAML front‑matter. It’s the form of the Hermes Agent and Prime Agent, so a skill written for one of them loads here without modification, and the opposite is true.
Thot ships the whole Hermes Agent library (MIT – see NOTICE.md): 90 methods loaded, 117 more available.
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èleCategories loaded: audit, security, software-development, github, devops, research, mlops, productivity, creative, apple, email, media, note-taking, smart-home, social-media, autonomous-ai-agents
The model discovers them with the skills tool — which responds with an index of names as long as it isn’t given a keyword, because two hundred descriptions are not a catalog — and reads the applicable one with skill. In a session, /skills shows you the same thing.
An imported method that cites a tool missing here (delegate_task, browser_navigate…) is served as is, with a note saying which ones are missing and what to use in their place. The approach carries over even when the tool call does not.
Adding some
~/.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.Both layouts are accepted: a flat directory (Prime Agent) or grouped by categories (Hermes Agent). A name that already exists replaces the built-in version — so you can adapt a shipped method without forking it.
A method supplied by the audited repository is analyzed first
An SKILL.md is text handed to the model as an instruction. The repositories Thot reads are, by definition, the ones no one stands behind. A hostile repository that places .thot/skills/x/SKILL.md would write part of the briefing.
The Hermes Agent guard is carried over here and applies to everything that comes from the repository: injection, exfiltration, persistence, obfuscation.
▲ 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 sckills scan <folder> asks the same question on demand. What Thot ships itself is not analyzed: it is on disk because the program is installed, not because a repository asked for it.
Custom commands
A Markdown file is a command. The grammaire is that of Prime Agent, Claude Code, and Codex — nothing new to learn.
---
description: Relire un fichier sans rien modifier.
argument-hint: <chemin>
---
Relis $1 et dis-moi ce qui cloche. Ne modifie rien.In .thot/commands/revue.md, that createst /revue src/app.py. Substitutions: $1, $2…, $@, $ARGUMENTS, ${@:2}, ${@:2:3}. An argument is never re-interpreted. Repoitry commands go through the same guard as skills.
Three are included: /triagé (name the entry or reply without further action), /hardden (test that fails first, fix next), /regress (the audit goes against a git reference).
MCP servers
The Hermes Agent catalog, twenty verified servers:
thot mcp list # le catalogue, et ce qui est déjà connecté
thot mcp show sentry
thot mcp add linearInstallation is delegated to the official CLI, which already has OAuth and token renewal gone through — Thot has no reason to hold a second vault waiting to leak. It says explicitly that registered is not authorized, and that which command finishes the job.
The tool model
The classics — read, write, edit, launch a command. Any read/write and any execution requires your confirmation, and that isn't configured.
And four that belong to Thot alone, free because they query the map rather than the model:
Tool | Response |
| the project's files |
| file, plot and curve of a function |
| who calls what, and the distance to an entry point |
| taint paths from source → sink |
In account mode, these four are served to the official CLI by
thot.mcp_server — a read-only MCP server, incapable of writing or executing anything.
When the model wonder who calls process_paymen, it queries the graph and gets the full response — instead of grepping three files at random.
Audit-only mode
The core of analysis also works without model, without network, without cost:
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 CIEntering SARIF — joining an existing chain
A report that no pipeline can read lives in a single terminal. GitHub code scanning, GitLab, Azure DevOps, and editors all read SARIF 2.1, and two properties of ScThot matter more than anywhere else.
The identity of a finding is rule + file + symbol + fingerprint of the body, never line — that's exactly what partialFingerprints asks for. A dashboard fed line numbers reopens all its tickets as soon as someone adds an import at the top of a file; fed by us, it doesn't.
And a taint path is a sequence of positions, which is just what codeFlows renders: the reader clicks from source to sink instead of taking the tool's word for it. A finding without a path doesn't carry any codeFlows key — an empty flow is rendered as a taint path without steps, which reads as a broken analysis rather than a pattern match.
A finding rejected by the panel is not deleted: it leaves with a justified suppressions. A dashboard never seeing it cannot distinguish “nobody looked at it” from “someone looked at it and decided”, and the second is the whole reason for the panel.
- run: thot audit . --out thot.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: thot.sarif }Assisted analysis — --deep
The deterministic analysis answers “could these data flow?”. It is exhaustive, free, and not the question one pays an auditor to settle. --deep asks the expensive question, only for the ones that warrant it:
thot audit . --deep # 20 pires candidats, 4 en parallèle
thot audit . --deep --budget 50 # plus large
thot audit . --deep --parallel 8 # plus viteTwo passes, deliberately adverse:
The probe must find a concrete input that reaches the dangerous point. Not a generality about the vulnerability class — a URL, a value, an effect.
The rebuttal receives the scenario, and has only one mission: to break it — upstream validation, a caller who only passes constants, a type that forbids the alleged input. When in doubt, it rebuts.
A finding survives only if a second, hostile read of the same code fails to kill it. confirmed then says real.
In session, the same thing: /audit deep.
The engine is chosen automatically — your Claude account via the official CLI if it's connected (the analyses run in parallel, on your subscription), an API key otherwise.
What the audit must not read
# .thotignore, à la racine du dépôt
vendor/
*.generated.py
tests/fixtures/Built-in exclusions cover what every repository has — module_modules, build, .venv. .thotignore covers what only this repo knows: embedded documentation, a generated client, a directory of fixtures intentionally broken. Auditing them doesn't pro nem findings, it produces noise at the same place findings would appear.
Your own rules
The built-in catalog knows the standard letters. It doesn't know the wrapper your team wrote around subprocess, the queue your service consumes, or the validator your project knows. Without a place to say it, every real-world system audit gets the same three things wrong three places.
# <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]The same file carries the JavaScript rules, under a js: score — team wrapper usually exists in both languages, and missing where to divide will just make one half obsolete.
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]What the model asks for is an untrusted input
The sources are expressions — sys.argv, os.environ. That covers a program you run and miss a program you call: an agent's tool receives its untrusted input as named parameters, taken from the data The model asked, and no expression appears anywhere in the body.
The measured cost of not modeling this: four SSRF in an afternoon, all reachable via a tool argument, none found by the taint — they were found by pattern rules, which recognize a shape without proof.
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: prefix“Empty by default, deliberately: which functions a registry calls is a fact about a repository, so guessing would put a resource under each parameter of each program. Measured on Hermes are the two extremes. a rule naming plugins and tools exposes 19 proven paths, several of which are over-fitted (the base_url a helper gets from the configuration not being untrusted); a rule naming parameter args gets zero, because Hermes handlers take named parameters and no dictionary. The right rule names the actual entry points — and the people who write them know it.
A rule that reuses a built‑in id moves or replaces it — forcing a self‑obtained weakening of a sink, without patching Thot. A malformed file stops the audit by naming the file and the wrong key, rather than pretending no findings.
Suppressions
A suppression is the only security claim that no tool re‑checks — including this one, by construction. # nosec, # noqa: S310, // eslint-disable… security/…: a statement about code, written once, which survives the callers it described.
Twice in one audit, her here was it was false:
suppression | what it claimed | what was true |
| scheme is checked | it while stops |
| url comes from config | one caller gets it from argument, therefore from the model |
Thot therefore reports them as a class, in LOW, with the reason shown. The finding doesn't say “this line is dangerous”: it says “no‑one re‑read the why it was excused”. On a --deep pass, that is a better task alongside the agent that verifies the rule still holds.
For Python it actually reads the tokens, not the pattern — a regex can't distinguish # nosec commented from the same text quoted in a docstring, and this module's docstring quotes 2.
A suppression placed on a line that this audit flags is a different object: it’s an assertion that contradicts a live finding, written by someone who read the very same line and concluded otherwise. It moving up one row says it. Occurs on Hermes: 7 out of 45 and 3 of the files on the day.
Measured: 0 on Thot, 0 on Prime, 45 on Hermes.
Calibration
Precision matters as much as detection. The following are intentionally not reported:
subprocess.run(cmd)withoutshell=True— no shell tells the command.cursor.execute("… ?", params)— literal query, built-in parameters.A value passed to
int(),trlex.quote(),os.path.basename(),html.escape()— these calls break the taint chain.A sink that no entry point can reach is automatically downgraded — but only if some entry point was found. With none, the scope is unknown, not zero, and nothing gets asserted over gaps.
payload.get(...)is notrequests.get(...).
Order: measured on Hermes Agent (4,457 Python files): 98 s, 365 findings, of which 25 are S‑Graded above the default threshold once we get memory.
Time: 11,4 s → 9.5.
Which parameter the argument fills
A helper whose only one of the sinks does not taint everything that comes around. However, the engine was already matching the caller with the set of the callee's sinks, without tracking where the argument lives:
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éThe position is now read at the call site, and also the name for a named argument. That's what the JavaScript engine was already doing, on its side.
Measured, once the resolution was possible: 32,238 resolution returned by Thot and hermes/tools, 90 % point at a single parameter, 8% at one parameter, 1.5% remain as a flow (f(*rest), f(**options)) and keep the wide answer. The target policy had installed on average 3.0 parameters, so more than 3 the search space slash — analyzing hermes/tools goes from 11.4 s down to 9.5 s.
What the graph can't track
A defect that is reached by a path the analysis cannot resolve — a handler kept in a dispatch table, a decorated view, a call on a variable whose type is unknown — is not an unreachable one. Thot distinguishes between the two:
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 nomIn all three cases, the scope is unknown, not null, and the finding keeps a light penalty instead of being buried. On Hermes, the same 365 findings but 60 climb one notch. A function that no one calls and no one mentions, for its part, correctly remains written down, otherwise the filter would stop being a filter.
Limitations
Taint across files is reserved: TypeScript and TypeScript have still immulte : TypeScript and TypeScript are indexed, can be followed inside a function body and up to a helper in the same file, and stop there, since there is no resolved call graph for them — the table above says that: above. The pattern rules, however, apply everywhere.
Without --deep, each finding is PLAUSIBLE: statically detected, but not
yet proved by. With --deep, a confirmed finding has survived an adverse
refutation — that is not yet proof of execution; that will come with the
repro. And the absence of a finding is not proof of the absence of a defect:
dynamic dispatch, reflection, and metaprogramming escape the analysis.
Full‑text navigation in the sessions is no longer constant‑cost on SQLite databases whose fts5 cannot raise the rowid — the engine then sorts all the matches instead of cutting at twenty. Measured on the one that comes with CPython 3.12: 6,588 instructions versus 27,443 when the corpus is multiplied by five, whereas an engine that can starts at 2,215 and stays at 2,698. A test checks it and names the version when the database engine cannot do it.
Development
cd Thot
uv run pytest -qInstalled in editable mode: the source code is the reference immediately. But
if pyproject.toml changes — a new dependency — you need to re‑run
uv tool install --editable --from . thot --force.
The deterministic core (codemap, taint, scope, scoring, store,
report) depends on no engine and does not hit the network — a test checks
that and breaks the suite if it changes.
Docs and plans: docs/superpowers/.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Remote MCP for A2A dependency inspector MCP, structured receipts, audit logs, and reviewer-ready evi
Static MCP manifest and tool-policy security preflight with signed input-redacted receipts
A paid remote MCP for CodeG, built to return verdicts, receipts, usage logs, and audit-ready JSON.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/nobodyohm-web/thot'
If you have feedback or need assistance with the MCP directory API, please join our Discord server