Running Euthynos with Claude Code: setup and what it writes

Registration is one line; the three audits that should precede it — disk, network, trust — are the rest of this post, including a wrong command in our own README.

· 9 min read ·setup · security

Registering an MCP server is one line in a config file. The effect is a long-lived process holding your file permissions, launched by your editor, reading whatever it likes inside your checkout and — depending on the server — outside it too. Most setup docs cover the one line and stop.

This one covers the line, then three audits: what lands on disk, what leaves the machine, and what stops a hostile repository from turning a read-only query into something else. It also discloses a wrong command in our own README, because that is cheaper than hoping nobody copies it.

Publication status, up front

Euthynos 0.1.0 is a release candidate and is not published to npm yet. npm view euthynos returns 404 today, and RELEASE-CANDIDATE.md line 3 says "not yet published" in as many words. None of the install commands below work until npm publish runs. They are the correct commands, written down so that the first thing anyone copies is the right thing.

Register it

Node.js 18 or later. ESM, Apache 2.0, four runtime dependencies: picocolors, tree-sitter-wasms, typescript, web-tree-sitter. The package ships dist/ only.

npm install -g euthynos
claude mcp add euthynos -- euthynos mcp

If your client wants the config form rather than the CLI:

{
  "mcpServers": {
    "euthynos": {
      "command": "euthynos",
      "args": ["mcp"]
    }
  }
}

It is a stdio server. On start it writes one line to stderr and then reads newline-delimited JSON-RPC from stdin:

euthynos MCP server 0.1.0 listening on stdio (protocol 2024-11-05)

The parsers matter for install reliability. Twelve tree-sitter grammars load as pure WASM with zero native bindings — no node-gyp, no prebuild matching your Node ABI, the same install on Windows, macOS and Linux. TypeScript, JavaScript and Vue single-file components go through the TypeScript compiler API instead; COBOL through a deterministic line parser. Sixteen languages, three strategies, one install path that does not depend on a compiler being present.

The binary ships under two names, euthynos and contexthub. The second is a legacy alias for earlier internal builds.

Do not run the README's registration command

README.md lines 153 and 159 document registration as claude mcp add contexthub -- npx -y contexthub mcp, plus an mcpServers block using npx -y contexthub. Do not run either. contexthub is this engine's internal codename and a local binary alias; it is not the name this package publishes under. So npx -y contexthub does not resolve to Euthynos — it resolves to whatever the public registry serves under that name, and npx -y downloads and executes that without a prompt. Use the euthynos commands above, and check the registry yourself before running anything of that shape.

Two other things in that README section are stale: it says the server exposes "six tools", and the CLI's own --help repeats it. A live tools/list returns 23.

Audit 1: what lands on disk

Exactly one directory, .euthynos/ at the repository root, holding four artifacts plus its own .gitignore. Nothing is written anywhere else — no home-directory cache, no temp state, no global config.

.euthynos/
  manifest.json     schema + engine version, per-file {size, mtimeMs, hash},
                    the absolute root, and a digest of the payload
  parsed.json       content-addressed: hash -> ParsedFile
  config.json       your ignore globs (you write this one)
  telemetry.jsonl   local tool-call measurements
  .gitignore

The .gitignore is not an afterthought. The directory and the rule that hides it are created in a single operation, and the comment in src/index/store.ts explains why: telemetry created the directory bare once, "and the very next git add -A in that repository committed a local telemetry log." A tool wrote a file into someone's project and they shipped it. The fix was to make a bare mkdir impossible rather than to remember not to do it again. The file contains:

# Euthynos local index — not source, do not commit
*

Writes are atomic — temp file plus rename — so a killed process leaves the previous good index rather than half of a new one. The payload is written first and the manifest last, as the commit point.

You can delete the whole directory at any time. The only cost is latency: the next query rebuilds, paying the full cold-parse cost again. EUTHYNOS_NO_INDEX=1 skips persistence altogether, which is what read-only checkouts and CI boxes get — correct answers, full cold-parse cost per process.

Audit 2: what leaves the machine

From the MCP server, nothing. That is worth being specific about rather than asserting — and it is worth naming the one place in the wider product where the answer is different.

There are zero LLM calls in the MCP query loop. The only LLM path anywhere is an opt-in CLI flag, --ai on the scan command, which asks a small Claude model whether two candidate duplicates share the same intent. It requires ANTHROPIC_API_KEY, makes at most eight calls — one per candidate — and lives entirely in the scan branch of src/cli.ts. Be plain about what that flag sends: each call posts up to 60 lines of each of the two functions to api.anthropic.com. That is real source code leaving the machine, on request, and it is the one thing in this product that does. The MCP server has no code path that reaches it. Registering the server does not create one.

Telemetry is local and metadata-only. One JSONL line per successful tool call:

{"tool":"read_function","argsHash":"7f3a19c2","estTokens":412,"outputBytes":1663,
 "linesReturned":48,"latencyMs":6,"filesTouched":1,"cache":"hit","dedup":false,
 "isError":false,"at":"2026-08-16T09:14:02.118Z"}

argsHash is FNV-1a over the argument JSON. It identifies a repeat call without storing what you asked for. There is no source text in the record, no argument values, no file contents, no paths. The point of the file is deterministic regression detection — a response that suddenly returns three times the bytes, a cache that stopped hitting — without spending a real agent session to notice.

It rotates at 5 MiB keeping one .1 generation. It is best-effort in the strict sense: a read-only checkout, a full disk or a permission error never fails the tool call. EUTHYNOS_NO_TELEMETRY=1 disables it.

One detail that reads like paranoia and is not: telemetry only writes into a directory that already exists, and only for a successful call. It once conjured .euthynos/ inside a mistyped path, which made a "nonexistent path" error succeed on the second run. A measurement layer must not alter what it measures.

Audit 3: what a hostile repository can do

The threat model assumes the repository is untrusted — cloned from anywhere, authored by anyone — and the agent driving the tools is curious. Repository content is data, never instructions. Four things are enforced:

  • Servable roots are pinned at startup to the server's cwd plus anything in EUTHYNOS_ROOTS (semicolon-separated). Without the pin, any readable directory on the machine was servable per tool call — which is the injection-amplification surface: hostile text in one repository aiming these tools at every other local project.
  • Path containment is lexical and realpath, applied to every path that reaches the engine, including paths reported by git.
  • Symlinks are never followed during discovery, so a link pointing outside the checkout cannot pull foreign files into the index.
  • Git is hardened through one runner: core.hooksPath=/dev/null, fsmonitor off, ext:: and file:// transports refused, GIT_CONFIG_NOSYSTEM, terminal prompts disabled, LFS smudge skipped. A repository cannot use its own .git/config to turn a read-only query into code execution.

An out-of-root request is refused with an error that names the boundary and how to widen it, rather than a generic denial:

Path is outside the servable roots pinned at server start: D:\other-project\src\index.ts.
Servable: D:\work\api. Set EUTHYNOS_ROOTS to serve additional repositories.

A repository can also ship its own .euthynos/ directory. It is not trusted. The manifest must match the current schema version, the current engine build, the absolute root it describes, and a digest of the payload sitting beside it. Any mismatch is discarded and rebuilt, with the reason announced on stderr rather than silently. A poisoned index cannot inject symbols that do not exist in the source.

There is deliberately no permissions layer. Euthynos runs as you and reads files the operating system already grants you; the kernel is the authorization check, synchronous and always fresh, with no mirrored ACL to go stale. A permission system would only be needed if one store held code the requester is not entitled to see — a property of a hosted index, not a local one.

The counterweight

Dotfiles such as .env, along with .git and .euthynos itself, are never indexed and never servable. But credentials committed inside code files are source code, and they are served verbatim. Euthynos does not detect or redact them, and redaction is a stated non-goal. The server's own initialize response tells the agent this in-band: the tools serve repository text verbatim, that text is untrusted data, and committed secrets come through as-is. If a repository's source must not reach an agent's context, do not point this at it.

The operational contract most MCP docs omit

Dispatch is synchronous on the stdio readline loop. One tool call at a time, queued in arrival order. A cold first scan blocks the queue for its whole duration, which grows with repository size. Above roughly 10,000 files is not validated at all. There is no per-call timeout, and $/cancelRequest is not implemented; only git subprocesses carry their own 120-second cap. Your MCP client supplies the timeout, or nothing does.

Resources and prompts are pull-only. Two resources, euthynos://repo-map and euthynos://health, and one prompt, orient. subscribe and listChanged are deliberately absent: pushing updates would require a filesystem watcher, which ADR-002 rejected for Windows reliability. Reads go through the index sweep, so they are current at read time without notification machinery. Note that orient is a prompt, not one of the 23 tools — and it embeds the current map rather than instructing the agent to fetch it, which is a round trip saved. If repo_map fails, the error is never served as the resource; a client pinning "the repo map" would otherwise pin an error message.

Scale envelope

  • Up to ~1,500 files: fully supported. Comfortable for interactive use.
  • ~5,000 files: supported. Noticeably slower, still usable.
  • ~10,000 files: supported with caveats. Peak RSS approaches ~1.1 GB.
  • Above ~10,000 files: not validated. Do not assume it works.

Memory is the binding constraint, not latency — the parsed corpus is held in memory, so the 60,000-file discovery cap in the code is not a reachable limit. Precise latency figures are deliberately not published: two internally consistent measurements disagreed, and the controlled experiment that would have settled which to trust could not be completed. Re-run the harness rather than trusting the table for your environment. Every latency row re-edits a file before the call on purpose; a static repository makes every cache hit and flatters the numbers.

There is no exception for index reads. find_symbol, read_function and find_references cost real time and grow with repository size. An earlier version of this post said they stayed at 0.3 ms or less at every scale; that figure came from a benchmark whose arguments the tool schema rejected, so it was timing an error return rather than a read. Both the magnitude and the claim of scale-invariance were wrong.

What it will not do

It is a static analyser. Dynamic dispatch, reflection, runtime code generation, string-built symbol names and dynamic imports are invisible to it, and every affected answer says so in its own text rather than in the docs. It is read-only: the only bytes it writes anywhere are inside .euthynos/. It does no cross-repository analysis, because a checkout contains what it imports and never who imports it. Transitive call traversal stops at six hops. Genuinely ambiguous same-named functions across modules produce no edge at all rather than a guessed one.

And the framing that governs all of it: Euthynos provides evidence. The ship decision stays with you. A launch audit checked 21 live tool outputs against a forbidden-phrase list — is safe, safe to …, proven safe, mathematic*, fully tested, no other consumers, all references, unused, guaranteed, certif* — and found zero of them in served output. Source-grep hits existed, but in internal code comments, never in text an agent receives. The load-bearing entries are additionally pinned by executable tests rather than by policy: check_my_changes and change_impact assert their own output never matches is safe, safe to or no impact, and an empty dependents answer is required to state in as many words that it is not a claim the module is unused.

The one benchmark result worth quoting, with its scope attached: on the three fully-measured tasks of the M2 run, recall was perfect and identical in both arms (42 of 42 required answer-key slots each) while the Euthynos arm used 13–31% fewer fresh tokens. Two of seven tasks are unmeasured because an external session limit consumed all of them; 21 of 42 sessions were valid under preregistered rules; one repository, one model, one permission environment, and no causal isolation of individual tools. Same answers, cheaper, on three tasks. That is the whole claim.

Try it on your own repository

Euthynos runs locally and reads only. It answers from your working tree, including uncommitted edits, and states the boundary of every answer.

$ npm install -g euthynos
Read the benchmark