RuvNet Brain runs on your machine, downloads a knowledge bundle, and (with your consent) can update itself — so we take reports seriously and fix them in the open. This policy exists because the project's first security review was a private, responsible disclosure; the next reporter should have a clear path.
Please do not open a public issue for a security problem. Instead:
- Use GitHub's private vulnerability reporting (Security tab → "Report a vulnerability"), or
- Email the maintainer at the address on the GitHub profile.
Include: what you found, a file:line or reproduction, the impact, and (if you have one) a suggested fix.
"Confirmed" reports — where you ran the exact command or read the exact line — are the most actionable.
- We verify every report against the real code before acting (we do not dismiss, and we do not rubber-stamp). Fixes are proven with a real command before they're called done.
- Each finding is tracked with a
file:lineroot cause, the exact fix, and its verification — seedocs/adr/0010-security-hardening-sec-0010.mdfor the format (that ADR is the record of the first review). - We credit reporters unless you ask us not to.
Installing the plugin installs everything under plugin/ (that's the marketplace source —
.claude-plugin/marketplace.json points at ./plugin), but only the
hooks actually registered in plugin/hooks/hooks.json run automatically. Here
is every one of them, what it does, and whether it can block you:
| Event | Script | What it does | Can it block a turn? |
|---|---|---|---|
SessionStart |
session-start.sh |
Prints the "brain active" confirmation, checks a handful of local state files (nightly-failure marker, brain-health flags), and — rate-limited to once per ~15 min — does a single read-only curl to check whether a newer plugin version exists. First-run-only, it asks two one-time yes/no questions (auto-update? anonymous usage counts?) and records your answer to a local file. |
No — always exits 0. |
UserPromptSubmit |
ground-ruvnet.sh |
Reads your prompt text and, if it matches certain keyword patterns, injects grounding/context text before Claude answers (e.g. call search_ruvnet before asserting a stack capability). Also runs a rate-limited (~6 h) background version check against the public npm registry for a few packages. |
No — always exits 0. |
PreToolUse (Write|Edit|Bash) |
hijack-ruvnet.sh |
Scans the content of a proposed Write/Edit/Bash for third-party defaults it thinks the rUv stack already has a native replacement for, and injects a suggestion via additionalContext. |
No — permissionDecision: "defer", never denies. |
PreToolUse (Task) |
route-dispatch.sh |
If — and only if — you've opted into model-cost routing (~/.claude/model-router/profile.json exists), blocks a subagent dispatch that doesn't declare an explicit model, so it can't silently inherit the calling session's (possibly expensive) model. |
Yes, but opt-in only. No profile file → this hook is a no-op for you. |
PreToolUse (Bash) |
verify-interface.sh |
Same opt-in gate as above. When it fires, it blocks invoking a handful of named ecosystem CLIs (ruflo, claude-flow, agentic-flow, agentic-qe, ruvector, agent-browser, ruv-swarm) on a subcommand whose --help hasn't been read in the last 24h — a guardrail against guessing CLI flags. |
Yes, but opt-in only (same profile gate). |
PreToolUse (Bash) |
design-wall.sh |
Not opt-in-gated. Blocks git commit when the staged files include anything under explainer/, console/, or a file named README.md (plain substring match against git diff --cached --name-only — this can match paths in any project, not just this repo), blocks vercel ... --prod, and blocks open-ing a small hardcoded list of this project's own URLs — unless a fresh (≤45 min) passing design-grade stamp exists for that surface. Escape hatch: RUVNET_SKIP_DESIGN_WALL=1. |
Yes, unconditionally for the specific patterns above — this is the one hook here that isn't behind an opt-in profile check. |
PostToolUse (Write|Edit|MultiEdit|Bash) |
learn-capture.sh |
Appends one line per tool call to a local per-session queue: the tool name plus either a Bash command's first ~120 chars up to its first embedded quote (workflow verbs like git push, not full arguments) or an edited file's basename (never its path or contents). |
No — always exits 0. |
SessionEnd |
learn-flush.mjs |
Reads that session's queue, dedupes to at most 8 distinct actions, and feeds them into the ruflo/AgentDB self-learning store at $HOME (i.e. your global, cross-project learner, not this project's .swarm/memory.db) via ruflo hooks post-command/post-edit. Deletes the queue file when done. |
No — best-effort, every failure swallowed. |
plugin/scripts/ also ships four more scripts — ground-before-write.sh, grounding-stamp.sh,
kling-preflight.sh, version-bump-gate.sh — that are not referenced in hooks.json. They ship as
inert files with every install; they only ever run if something else explicitly wires them into a
settings.json (this repo's own .claude/settings.json wires
version-bump-gate.sh for this repo's own maintainers — that is a project-scoped dev convenience, not
part of what an installed plugin does for you).
Everything below is a plain, unauthenticated GET/POST to a public endpoint — none of it carries an
API key, a machine identifier, your prompts, your code, or file contents. Grep the cited file yourself;
that's the point of this section.
Version / update checks (read-only, no payload beyond the HTTP request itself):
plugin/scripts/session-start.sh—curltoraw.githubusercontent.com/.../plugin.json, rate-limited to once per ~15 min, 3 s timeout, to detect a newer plugin version.plugin/scripts/ground-ruvnet.sh—curltoregistry.npmjs.org/<pkg>/latestfor a handful of stack packages, rate-limited to once per ~6 h, backgrounded so it can never block a prompt.bin/install.mjs—api.github.com/repos/.../releases/latest(find the current release) and the matchinggithub.com/.../releases/download/...asset URL (download the ~500 MB knowledge bundle zip).scripts/onboarding-console.mjs(gatherTrust, serving the local Console's Trust card) —api.github.com/repos/.../releases/latestplus the published.sha256asset, to show you the release bundle's fingerprint. The Console server itself binds127.0.0.1only and rejects any request whoseHostheader isn't loopback (DNS-rebinding guard) — nothing it serves is reachable off your machine.
Optional, consent-gated usage counts (kb/telemetry-ping.mjs, bin/install.mjs):
- Nothing is ever sent unless
~/.cache/ruvnet-brain/.telemetry-consentcontains the literal wordyes— written only after the installer explicitly asks you ("Share anonymous usage counts... [Y/n]"), and never written at all on a non-interactive install (so a scripted/CI install stays silent by default). Decline up front with--no-telemetry, or flip the file back tono/ delete it at any time. - The entire payload, always:
{ event: "install", v: <version> }once at install time, and at most one daily-batched{ event: "search"|"session", v: <version>, n: <count> }per event type — a name, the installed bundle's version string, and an integer count. No query text, no repo names, no file paths, no code, no username, no machine ID is ever read by this code path, let alone transmitted — see the contract comment at the top ofkb/telemetry-ping.mjs. - Hard kill-switches:
RUVNET_BRAIN_TELEMETRY=0orRUVNET_BRAIN_TEST=1disable the module outright.
Model weights (first run only, then fully local) — see Model & data provenance below; the short version is: a specific pinned HuggingFace revision, only when not already cached, never on repeat runs.
What never leaves your machine, full stop: your prompts, your source code, the text of your
search_ruvnet queries, anything AgentDB/ruflo memory stores (.swarm/memory.db is local SQLite), the
RVF vector stores themselves, and the contents of any file the hooks touch (they log that a Write/Edit
happened and a basename — never the diff or the file body).
Every point where RuvNet Brain can do something beyond "answer inside this session" is opt-in and recorded as a plain file you can read or flip yourself:
- Anonymous usage counts — asked once, at install; default is OFF on a non-interactive install;
~/.cache/ruvnet-brain/.telemetry-consent. - Background plugin auto-update — asked once, at first
SessionStart; even when enabled, a new version only downloads automatically — loading it still requires you to restart/--continueyour session (Claude Code only loads plugins at process start, a hard platform constraint, not a choice this project makes);~/.cache/ruvnet-brain/.auto-update-pref. - Knowledge-bundle updates — detect-and-notify only;
forge-update.mjs --applyis never run automatically, by design, because it overwrites executable.mjstool files from a GitHub Release (see the signing posture below) — you run--applyyourself after reviewing. - Model-router cost routing / the interface-check and dispatch-model walls — nothing in
route-dispatch.shorverify-interface.shdoes anything until you've answered the router's setup questions and~/.claude/model-router/profile.jsonexists. - Console Apply flow — every mutating action is a POST that must echo a random, per-launch token
(never persisted to disk), and the undo is journalled before the mutation runs so any applied change
is reversible; see
scripts/onboarding-console.mjs's header comment for the full contract.
- The knowledge bundle is Ed25519-signed with transitional enforcement.
scripts/sign-bundle.mjsemits a detached<zip>.sig(over the bundle's SHA-256) plus<zip>.sha256, and the installer verifies it before extracting, against an Ed25519 public key embedded inbin/install.mjsso the trust root travels with the installer code (an attacker who swaps the bundle can't also swap the key). A signature that is present but invalid always fails closed — the download is deleted and extraction refused. A missing signature currently warns and proceeds (SIGNING_REQUIRED = false) so releases predating signing still install; this flips to hard-required once every release is signed. The unattended code-overwrite path remains disabled (updates detect-and-notify, they do not auto-apply executable files). - Model weights download from HuggingFace on first run only when not already cached, and are now
pinned to exact commit SHAs (no longer the floating
mainbranch) so the weights — and therefore every embedding — cannot silently change under an upstream re-publish. See Model & data provenance below. - The grounding hooks are POSIX shell — on native Windows without WSL/Git-Bash they don't fire (the
search_ruvnettool still works).
The brain's answers are only as trustworthy as the weights that produce its embeddings, so those are pinned and loaded locally:
- Embedding models (pinned by exact HuggingFace commit SHA, quantized ONNX):
Xenova/all-MiniLM-L6-v2— 384-dim, the default query embedder and the small build (kb/forge-ask.mjs,kb/forge-build.mjs) — pinned at751bff37182d3f1213fa05d7196b954e230abad9.Xenova/bge-base-en-v1.5— 768-dim, the sharper big build (kb/forge-big.mjs, and the query side ofkb/forge-ask.mjswhen a big bundle is present) — pinned at4d6cd88e18e51a5e020c2c305726d76ada9c03cf.Xenova/ms-marco-MiniLM-L-6-v2— cross-encoder reranker (kb/forge-rerank.mjs) — pinned ata09144355adeed5f58c8ed011d209bf8ee5a1fecwhen the default model is used (an operatorCE_MODELoverride falls back tomain).
- Weights load from a local ONNX cache, not the network by default. The loader
(
@xenova/transformersv2.17.2) points at a local model cache (KB_MODEL_CACHE, else akb/-localmodels-cache) and permits a remote HuggingFace fetch only when that specific model is not already cached. Once cached it is fully offline. The cache lookup is revision-agnostic, so pinning a SHA never forces a re-download of an already-present model — it only makes the first fetch on a fresh machine deterministic. - Pin vs. package version are independent. The pinned SHA fixes the weights; the
@xenova/transformersnpm version (bumped via Dependabot) fixes the loader code. A loader upgrade never changes the pinned weights — review such updates for API compatibility with the pinned revisions. - Bundle integrity is covered by the Ed25519 signing posture described above (signed
.sig+.sha256, public key embedded in the installer, verify-before-extract, invalid-signature fail-closed).
- A CycloneDX SBOM for the shipped surfaces.
npm run sbomruns the standard@cyclonedx/cyclonedx-npmtool (--omit dev, so test/build-only tooling likevitestis excluded) and writessbom/ruvnet-brain.cdx.json— a CycloneDX 1.6 document listing every production package the installer and plugin actually ship (currently@metaharness/routerand its one transitive dependency,@metaharness/flywheel; the installer and the plugin's MCP launcher themselves import only Node's standard library — zero third-party runtime dependencies of their own). The Onboarding Console's Trust card reads this file locally and, once you've generated it, shows the real component count and generation date in place of the "coming v3.3" placeholder — the wiring is already live inscripts/onboarding-console.mjs(gatherTrust) andconsole/app.js(renderTrust) as of this change. A signed, published SBOM attached to each GitHub Release (matching the existing.sha256/.sigpattern) is the remaining step to make this row "coming v3.3" a full "measured from the published asset" the way the bundle signature row already is. - An npm provenance plan (not yet executed — no publish has happened as part of this work) for the
ruvnet-brainnpm package, usingnpm publish --provenanceover GitHub Actions OIDC (the "trusted publisher" flow) rather than a long-lived npm token. Seedocs/research/npm-provenance-plan.mdfor the exact requirements and steps.
Active development is on main; fixes land there first and flow to users via the plugin's update path.
The exact current version is on the badge at the top of the README.