The Medusa Harness sits between the AI agents on a machine and everything they touch: MCP tools, model APIs and the desktop apps your team already uses. One command onboards a machine. Every action it gates is written to a signed, hash-chained receipt your control plane verifies.
About 5 minutes for one machine. Node 20 or newer. macOS, Linux and Windows.
Agents act through three kinds of doorway. The harness puts one policy and one evidence trail across all of them, so you do not need a different tool per doorway.
| Doorway | How the harness covers it | Covers |
|---|---|---|
| MCP tools launched by a client | Rewrites the client's config so each server runs through medusa proxy | Claude Code, Cursor, Claude Desktop, Windsurf, VS Code |
| MCP tools over HTTP/SSE | Points URL servers at a loopback proxy (medusa serve) | Remote MCP servers, for example GitHub's |
| Agent hooks | Claude Code and Cursor hooks run medusa hook before and after every native tool call | Bash, Edit, Write, Read, shell commands, file reads and edits that never touch MCP |
| Claude Enterprise (cloud) | Anthropic's Inference hook posts each governed prompt to this control plane | claude.ai, Cowork and Claude Code for people with nothing installed |
| Model APIs from CLIs and SDKs | Sets the model base URL to a local proxy (medusa llm-proxy) | Anything reading OPENAI_BASE_URL / ANTHROPIC_BASE_URL |
| Model APIs from GUI apps | A local HTTPS proxy that inspects only model hosts (medusa capture) | Cursor app incl. autocomplete, Claude Desktop, ChatGPT app |
| Agents you write yourself | createHarness() wraps your tools and model calls | Custom agents and scripts |
Whatever the doorway, every call runs the same ladder: protected paths, workspace confinement, private-network checks, server and tool rules, argument rules, rate limits, on-device DLP on both arguments and results, prompt-injection taint, and administrator approval holds. Blocked calls are answered with a policy error and coaching so the agent explains itself to the user instead of failing mysteriously.
node --version.Three lines onboard a whole machine.
npm install -g @medusasec/harness medusa setup --org-key sk-med_your_key_here --profile medusa doctor
setup is the whole flow. --profile also adds the model environment to your shell profile so CLI agents route through the proxy. Add --dry-run first if you want to see exactly what it would change without touching anything.
What you should see
Medusa Harness 0.1.0 — setup ✓ enroll: enrolled as hx:cb023e8d-9736-493a-a10d-365fe0e0db9e ✓ policy: synced (verified) etag W/858bea63-… ✓ wrap:claude_code: wrapped github, filesystem ✓ wrap:cursor: wrapped postgres ✓ model-env: wrote ~/.medusa/harness/env.sh and added it to ~/.zshrc ✓ service: launchd agent loaded (starts at login, restarts on crash) ✓ doctor: all checks passed Next: → Restart your MCP clients so they pick up the wrapped servers. → Watch the traffic on the dashboard: Tool calls, Harness endpoints, Compliance.
Each step is something you can also run on its own if you prefer to wire seams by hand.
Created one endpoint (hx:…) and generated an ES256 receipt-signing key that never leaves the machine. The public half is published so the control plane can pin it; a later key change raises a critical alert instead of being trusted silently. Files live in ~/.medusa/harness/ with the keys at owner-only permissions.
Fetched your policy, checked its signature, and cached it to disk so enforcement keeps working offline. Run medusa policy any time to re-sync and print what is currently enforced.
Rewrote each client config so servers launch through the harness. The original command is preserved under a __medusa_harness__ marker and the file is backed up, so medusa uninstallrestores it exactly. URL-based servers are pointed at the loopback HTTP proxy instead, with the real URL moved to ~/.medusa/harness/http-routes.json.
Wrote ~/.medusa/harness/env.sh exporting OPENAI_BASE_URL, ANTHROPIC_BASE_URL and friends at the local model proxy, and with --profile sourced it from your shell profile. This covers CLIs and SDKs. GUI apps need the extra step in GUI apps. On Windows, setup writes env.ps1 and adds it to your PowerShell profile instead; medusa env --persist also sets the same variables in your user environment so GUI apps started from Explorer inherit them (medusa env --unpersist reverts).
Registered medusa run (the HTTP and model proxies in one process) as a launchd agent on macOS, a systemd user unit on Linux, or a logon Scheduled Task on Windows (a hidden PowerShell launcher that restarts it), so the seams come back after a reboot and restart on crash. Skip with --no-service if you supervise processes yourself.
1. Health check
medusa doctor
Checks enrollment, key permissions, policy freshness and signature, control-plane reachability, spool health, and which clients are wrapped. Exits non-zero if anything needs attention, so it works as an MDM health probe. Add --json for machine-readable output.
2. Prove a block from a shell
medusa check --tool write_file --args '{"path":"~/.cursor/mcp.json","content":"{}"}'
# → block · protected_path_tamper
medusa check --tool echo --args '{"text":"AKIAIOSFODNN7EXAMPLE"}' --record
# → coach · dlp_detection (and, with --record, a real receipt)check runs the real gate without needing an agent. Without --record nothing is written; with it, the call is spooled and receipted exactly like a live one.
3. Confirm on the dashboard
setup wires all of these. This section is for when you want to configure one deliberately, or understand what a given seam does.
medusa clients # what is configured and what is wrapped medusa install --client all --dry-run medusa install --client all # or --client claude_code | cursor | claude_desktop | windsurf | vscode medusa install --path ./.mcp.json # a project-level config
A wrapped entry becomes node …/medusa proxy --server github -- npx -y @modelcontextprotocol/server-github. Every JSON-RPC frame in both directions passes the gate: allowed frames forward untouched, results with sensitive data are redacted before the agent sees them, and blocks answer with JSON-RPC error -32001 plus coaching. Entries from the retired Python agent are migrated automatically.
medusa serve # loopback proxy on 127.0.0.1:27182 (the background service runs this for you)
URL-based servers are rewritten to http://127.0.0.1:27182/proxy/<name> and the real URL, with any static headers, moves to the routes file. Streamable HTTP and SSE are relayed event by event with each message gated, and one session is kept across the handshake so client identity, taint and receipt continuity survive.
medusa env # show the variables medusa env --export # a sourceable script source ~/.medusa/harness/env.sh
Points the SDK base URLs at 127.0.0.1:27183. The proxy checks allowed models, enabled providers and the daily token or spend budget, scans the prompt with your detectors before it leaves the machine, forwards to the real provider, optionally scans the response, and signs a receipt. OpenAI chat/completions and responses, Anthropic messages and Google generateContent shapes are all understood; streaming passes through.
import { createHarness } from "@medusasec/harness";
const harness = createHarness({ server: "my-agent", client: { name: "my-agent", version: "1.0" } });
// Gate your tools — blocked calls throw with err.medusa.rule and coaching.
const tools = harness.wrap({
readFile: { name: "read_file", run: async ({ path }) => fs.readFileSync(path, "utf8") },
});
await tools.readFile({ path: "/tmp/notes.txt" });
// Gate a model request before you send it (returns possibly-redacted messages).
const { messages } = harness.llm({ provider: "openai", model: "gpt-4o", messages: prompt });
// Feed untrusted content in so injection taints the session.
harness.observeContent(fetchedPage, "web");The MCP tool traffic of a GUI app is already covered by the config wrapping above, because the app reads the same mcp.json whether it is a GUI or a CLI. The one remaining gap is the direct model API call a GUI app makes, because those apps ignore shell environment. Cursor routes only its chat panel through a custom base URL, never autocomplete or inline edit; Claude Desktop and the ChatGPT app expose no base-URL setting at all.
Closing that gap takes a local HTTPS proxy plus two administrator decisions.
medusa capture # starts the proxy and prints everything below medusa ca trust # prints the exact command to trust the harness CA medusa ca fingerprint # SHA-256 of the CA, to verify what you are trusting
The harness generates a private certificate authority into ~/.medusa/harness/ca/ (key at owner-only permissions) and mints a short-lived leaf per model host. Nothing works until the machine trusts that CA. On macOS:
sudo security add-trusted-cert -d -r trustRoot \ -k /Library/Keychains/System.keychain ~/.medusa/harness/ca/medusa-ca.crt
Windows, as Administrator (the machine store, which Electron apps and .NET honour):
certutil -addstore -f Root %USERPROFILE%\.medusa\harness\ca\medusa-ca.crt
Linux: copy it to /usr/local/share/ca-certificates/ and run update-ca-certificates; Chrome and Electron apps also need it in the NSS store. medusa ca trust prints the exact commands for the machine it runs on. For a fleet, push the same certificate as a trusted root through your MDM, Intune or Group Policy instead.
macOS, for the active network service:
sudo networksetup -setsecurewebproxy "Wi-Fi" 127.0.0.1 27184
Windows, per user (WinINET settings, which Cursor, Claude Desktop, the ChatGPT app and most GUI apps use):
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer /t REG_SZ /d "https=127.0.0.1:27184" /f netsh winhttp set proxy 127.0.0.1:27184 # as Administrator, for services and WinHTTP apps
For a fleet, ship a proxy PAC file or a network payload from your MDM, Intune or Group Policy. On Linux, set the system or desktop proxy; HTTPS_PROXY=http://127.0.0.1:27184 covers command-line tools everywhere. medusa capture prints both the routing commands and the revert for the machine it runs on.
What it does and does not touch
llm_request receipt tagged to the harness/egress seam.A coding agent's own tools are not MCP. Claude Code's Bash, Edit, Write, Read and WebFetch, and Cursor's shell commands, file reads and file edits, never pass through an MCP config, so config wrapping cannot see them. Both agents expose hooks for exactly this, and medusa setup wires the harness into them: every native tool call runs through the same gate as an MCP call, with the same policy, taint tracking and signed receipts.
medusa hooks # status per agent medusa hooks install # wire Claude Code (~/.claude/settings.json) and Cursor (~/.cursor/hooks.json) medusa hooks managed # print the organization-wide files an MDM ships so users cannot remove them
| Agent | Hook | What the harness does |
|---|---|---|
| Claude Code | PreToolUse on every tool, including mcp__server__tool | Gates the call. A block answers with permissionDecision deny and the reason; an approval hold answers ask; a redaction rewrites the arguments. An allow returns no decision, so Claude Code's own permission prompts still apply. |
PostToolUse | Scans the result. Prompt-injection patterns taint the session (later writes are denied, same as an injected MCP result) and Claude is told to treat the content as untrusted. | |
| Cursor | beforeShellExecution, beforeMCPExecution, beforeReadFile | Gates shell commands, MCP calls and file reads, fail-closed. Reading a file that contains an injection taints the conversation. |
beforeSubmitPrompt | Scans the prompt on the way out, because Cursor's GUI ignores the shell environment and may never reach the model proxy. | |
afterFileEdit, after* | Records what was written or returned as evidence; a write that already happened is not blocked. |
~/.medusa/harness/hook-sessions.json. If the harness itself fails, tool calls fail closed with a reason that points at medusa doctor, matching the MCP seams.For a fleet, medusa hooks managed prints Claude Code's managed-settings.json with allowManagedHooksOnly and Cursor's enterprise hooks.json, with the paths for macOS, Linux and Windows. Ship those from your MDM and a user cannot switch the hooks off. The hook files themselves are protected paths, so an agent cannot edit them either.
Claude Enterprise organizations can route every governed prompt in claude.ai, Cowork and Claude Code through an Inference hook: Anthropic sends the transcript to a security server before inference and waits for allow or deny. This control plane is that server. It covers people who use Claude without the harness or the extension installed, and it applies to the desktop, web and mobile apps alike.
No Enterprise organization to test with? node scripts/inference-hook-simulate.mjs <endpoint-url> --secret whsec_… --case tool-secret in the control-plane repository sends a frame signed exactly the way Anthropic signs it (cases: clean, secret, tool-secret, injection, config-test) and prints the verdict, so the whole path can be exercised and demonstrated.
| What it sees | What Medusa does |
|---|---|
| Transcript text, tool calls, tool results, extracted attachment text | Scans all of it with the same detectors as the endpoint seams, against this organization's policy. Findings in tool results and attachments are flagged as such. |
| Secrets, PII, financial, health, insurance data set to block | Denies with a reason the person sees (what to remove), and records the event with a reference id that also appears in Anthropic's own activity feed. |
| Prompt-injection text | Recorded; denied only if you switch “deny on prompt injection” on, because a denied turn stalls a Claude Code session. Anthropic cannot redact, only allow or deny. |
| Unsigned or wrongly signed requests | Rejected once a secret is set. The last signature result is shown in Settings. |
The harness enforces what your org policy says. Both blocks live in the policy editor on the Policy page, and scope, groups, versioning and signing work exactly as they do for browsers.
| Tab | What it controls |
|---|---|
| Tools | Blocked and allowed MCP servers, blocked tools and patterns, argument rules, rate limits, extra protected paths, workspace roots, the MCP command allowlist, taint behaviour, fail mode and observe. |
| Models | Allowed models, enabled providers and their base URLs, daily token and spend budgets, and whether prompts and responses are scanned. |
| Content & Detectors | The same sensitive-data detectors the browser extension uses. They run on tool arguments, tool results and model prompts. |
Roll out safely with observe mode
Turn on Observe onlyin the Tools tab first. Every verdict is recorded as “would have blocked” without actually blocking, so you can watch Tool calls for a few days and tune rules before enforcing. Protected-path tampering is still blocked in observe mode, by design.
One unattended command per machine, run as the user, from Jamf, Intune, Kandji or any config manager.
MEDUSA_ORG_KEY=sk-med_your_key_here \ MEDUSA_UNATTENDED=1 \ MEDUSA_USER_EMAIL="$USER@yourcompany.com" \ npx -y @medusasec/harness setup --json
| Variable | Why |
|---|---|
MEDUSA_ORG_KEY | The enrollment key, so nothing is prompted or stored in a script. |
MEDUSA_UNATTENDED=1 | No prompts; wires the shell profile automatically. |
MEDUSA_USER_EMAIL | Stamps receipts with the person's identity when the OS user is generic. |
MEDUSA_HARNESS_HOME | Relocates the state directory if you do not want it under the home directory. |
--json prints a structured report and exits non-zero if any step failed, so your MDM can report success or failure per machine. Re-running setup is safe: it reuses an existing enrollment and only wraps what is not already wrapped.
For GUI coverage, add the CA and proxy payloads described in GUI apps to the same MDM profile.
medusa doctor # full health check (use --json in monitoring) medusa status # enrollment, receipts issued, spool depth medusa receipts --verify # verify the local receipt ledger (hashes, links, signatures) on this machine medusa policy # re-sync and print what is enforced medusa budget # today's model token and spend usage medusa service status # is the background service installed and running medusa clients # which MCP clients are wrapped
The background service logs to ~/.medusa/harness/run.log. Events are written to a durable spool before delivery and only trimmed once the control plane acknowledges them, so a laptop that goes offline keeps enforcing and backfills its receipts when it reconnects. Receipts are never dropped, even under the spool size cap.
Upgrades are an npm install away: npm install -g @medusasec/harness then medusa setup again to re-wrap anything new. The Compliance page flags machines running an outdated harness.
| Symptom | Cause and fix |
|---|---|
| An agent's tools stopped appearing | The client was not restarted after wrapping, or the wrapped command cannot spawn. Run medusa clients to confirm it is wrapped, then restart the client. Check ~/.medusa/harness/run.log. |
Everything is blocked with policy_unavailable | The machine has never synced a policy and cannot reach the control plane, so tools fail closed. Run medusa policy and check medusa doctor for connectivity. |
| Doctor says “MCP servers found but none wrapped” | Run medusa install --client all. If a config is TOML (Codex) or has comments, it is reported but not rewritten — add the proxy entry by hand. |
| Doctor says key permissions are too open | Run chmod 600 on the files in ~/.medusa/harness. The harness refuses to read secrets with wider permissions. |
| Policy shows “none” for signature | Your control plane has no Ed25519 policy-signing key configured. Transport is still TLS-protected; set the signing key to get full verification. |
| A GUI app fails to connect after enabling capture | Either the CA is not trusted on that machine, or the app pins certificates. Check medusa ca fingerprint against what the system trusts; if it pins, exclude it. |
Model calls are refused with llm_budget | The daily token or spend budget in the Models tab is exhausted. Check with medusa budget; the counter resets daily. |
| A session went read-only unexpectedly | A tool result contained prompt injection, or came from a server not on your approved list, so the session was tainted. This is intentional. Start a new session, or adjust taint rules in the Tools tab. |
medusa uninstall --client all # restores every original MCP config from backup medusa service uninstall # removes the background service npm uninstall -g @medusasec/harness
If you enabled the GUI proxy, also remove the system proxy setting and delete the trusted CA from the keychain or trust store. Finally, remove the endpoint from the Harness page to revoke its key — its receipts stay for your retention window, so evidence is not lost when a machine leaves the fleet.
| Command | What it does |
|---|---|
medusa setup | The whole onboarding flow. --dry-run, --json, --profile, --no-service, --clients a,b |
medusa doctor | Health check; non-zero exit when something needs attention. --json |
medusa enroll | Enroll only. --org-key, --label, --control-plane, --dashboard |
medusa policy | Sync and verify the org policy. --force |
medusa check | Dry-run the gate. --tool, --args, --server, --record |
medusa clients | List MCP client configs and what is wrapped |
medusa install / uninstall | Wrap or restore MCP client configs. --client, --path, --servers, --dry-run |
medusa proxy | The stdio seam; what wrapped configs invoke |
medusa serve | The HTTP/SSE MCP proxy (port 27182) |
medusa llm-proxy | The model API proxy (port 27183) |
medusa capture | The GUI egress proxy (port 27184) |
medusa ca | path | fingerprint | trust — the CA the egress proxy uses |
medusa run | HTTP and model seams in one process; what the service runs |
medusa service | status | install | uninstall — the background service |
medusa env | Model base URLs for this machine. --export, --json; on Windows --persist / --unpersist (user environment, reaches GUI apps) |
medusa status / budget / flush | Enrollment state, model usage, force a telemetry flush |
medusa hooks | status | install | uninstall | managed — wire the harness into Claude Code and Cursor hooks. --client claude_code,cursor, --dry-run |
medusa hook | claude-code | cursor — what those hooks run; reads the payload on stdin, never invoked by hand |
medusa receipts | The local receipt ledger. --last N, --since <iso>, --json; --verify checks hashes, links and signatures against the pinned key and exits non-zero on any problem |
Every gated action produces an event and, for tool and model calls, a signed receipt in a hash-chained sequence. A receipt records the endpoint, session, action, upstream server or model, the verdict and the rule that fired, the agent and OS user, and category counts.
What is never sent
Receipts are signed on the machine with a key that never leaves it, and every one is also kept in a local append-only ledger (~/.medusa/harness/receipts.jsonl, owner-only) so the chain can be verified on the endpoint itself, offline, without trusting the control plane. medusa receipts lists it; medusa receipts --verify recomputes every hash, link and ES256 signature against the pinned key and exits non-zero on any problem, which is what an on-site auditor runs. The control plane pins the first key it sees, verifies chains nightly, and can produce an attestation over any range — the basis of the evidence pack, which maps each artifact to SOC 2, NIST AI RMF, ISO 42001, EU AI Act and CIRCIA controls.
The dashboard's install page has the exact commands filled in for your organization, including the unattended one-liner for your MDM.