← All documentation

Harness Setup Guide

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.

What it covers

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.

DoorwayHow the harness covers itCovers
MCP tools launched by a clientRewrites the client's config so each server runs through medusa proxyClaude Code, Cursor, Claude Desktop, Windsurf, VS Code
MCP tools over HTTP/SSEPoints URL servers at a loopback proxy (medusa serve)Remote MCP servers, for example GitHub's
Agent hooksClaude Code and Cursor hooks run medusa hook before and after every native tool callBash, 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 planeclaude.ai, Cowork and Claude Code for people with nothing installed
Model APIs from CLIs and SDKsSets the model base URL to a local proxy (medusa llm-proxy)Anything reading OPENAI_BASE_URL / ANTHROPIC_BASE_URL
Model APIs from GUI appsA local HTTPS proxy that inspects only model hosts (medusa capture)Cursor app incl. autocomplete, Claude Desktop, ChatGPT app
Agents you write yourselfcreateHarness() wraps your tools and model callsCustom 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.

Before you start

  • Node 20 or newer on the machine. Check with node --version.
  • An org enrollment key. Create one in the dashboard under Settings → API access. It is used only to enroll; every machine then mints its own endpoint key.
  • A free seat. Each machine is one endpoint against your plan's endpoint limit, the same as a browser.
  • A policy worth enforcing. Not required to install, but the harness enforces what your org policy says, so review the Tools and Models tabs before rolling out widely.
The enrollment key is a secret. Pass it through your MDM or a secret manager rather than pasting it into a shared shell history. It can be revoked at any time from Settings → API access without touching machines that already enrolled.

Quick start

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.
Restart your agents afterwards. Claude Code, Cursor and Claude Desktop read their MCP config at launch, so a running client keeps using the unwrapped servers until you restart it.

What setup did

Each step is something you can also run on its own if you prefer to wire seams by hand.

1

Enrolled the machine

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.

2

Synced and verified the org policy

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.

3

Wrapped every MCP client it found

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.

4

Routed model APIs

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).

5

Installed a background service

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.

Verify it works

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

  • Tool calls — every gated call with its verdict and receipt, within seconds.
  • Harness — the machine, its version, policy state and the tool servers it exposes.
  • Compliance — the endpoint against the five fleet controls.
  • Receipts — verify the chain against the pinned key.

Seam by seam

setup wires all of these. This section is for when you want to configure one deliberately, or understand what a given seam does.

MCP tools launched by a client (stdio)

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.

MCP tools over HTTP or SSE

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.

Model APIs from CLIs and SDKs

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.

Agents you write yourself

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");

GUI apps (full model-traffic coverage)

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
1

Trust the harness CA

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.

2

Point the machine's HTTPS proxy at it

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.

The harness never performs these two steps itself.Trusting a root CA and redirecting a machine's traffic are real security decisions, so they stay with an administrator or your MDM even when everything else is automated.

What it does and does not touch

  • Inspects only model API hosts — OpenAI, Anthropic, Google, Azure OpenAI and Mistral. Everything else is tunnelled through untouched and never decrypted, so ordinary browsing and app traffic are unaffected.
  • Gates identically to every other seam — allowed models, providers, budgets, prompt and response DLP, and a signed llm_request receipt tagged to the harness/egress seam.
  • Apps that pin certificates will refuse it. That is the app defending itself and is expected; those calls fail rather than being silently downgraded. If a critical app pins, leave the proxy off for that host.
  • HTTP/1.1 only, and one request per tunnel — clients reconnect transparently, at a small efficiency cost.

Agent hooks (Claude Code and Cursor native tools)

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
AgentHookWhat the harness does
Claude CodePreToolUse on every tool, including mcp__server__toolGates 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.
PostToolUseScans 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.
CursorbeforeShellExecution, beforeMCPExecution, beforeReadFileGates shell commands, MCP calls and file reads, fail-closed. Reading a file that contains an injection taints the conversation.
beforeSubmitPromptScans 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.
Hook invocations are one process each, so taint is persisted per agent session in ~/.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: the cloud hook (nothing to install)

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.

  1. Settings → Claude Enterprise inference hook → Create endpoint, copy the URL.
  2. Anthropic console → Organization settings → Inference hooks: paste the URL, run Test connection, copy the signing secret it generates.
  3. Paste the secret back into Medusa, enable the hook, leave shadow mode on, and watch the Tool calls page under the “Claude Enterprise (cloud)” doorway.
  4. Turn shadow mode off when the verdicts look right. Anthropic's own rollout percentage and role exclusions still apply.

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 seesWhat Medusa does
Transcript text, tool calls, tool results, extracted attachment textScans 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 blockDenies 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 textRecorded; 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 requestsRejected once a secret is set. The last signature result is shown in Settings.
Anthropic never sends system prompts, tool definitions or raw file bytes, and Medusa never stores prompt text: only the verdict, category counts and identifiers. The endpoint must be public HTTPS on a domain you control, so self-hosted control planes need a public hostname for this one route.

Configure policy

Start in Monitor mode. Settings → Enforcement mode → Monitor puts the whole organization in observe-only: every seam (browser, agent hooks, MCP, model APIs, GUI apps, SDK, the cloud hook) still scans, decides and signs a receipt, but nothing is blocked. Tool calls and Findings show the would have verdict, and the dashboard shows a “Monitor mode” pill until you switch to Enforce. The only things enforced regardless are protected-path tamper and fail-closed on an endpoint that has never received a policy.

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.

TabWhat it controls
ToolsBlocked 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.
ModelsAllowed models, enabled providers and their base URLs, daily token and spend budgets, and whether prompts and responses are scanned.
Content & DetectorsThe 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.

Fail mode decides what happens when a machine has never synced a policy and cannot reach the control plane. Tool calls default to fail-closed (refused, while initialize and tools/list still answer so clients do not hang). Model calls default to fail-open so a control-plane outage never stops work. Both are switchable per tab.

Fleet rollout

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
VariableWhy
MEDUSA_ORG_KEYThe enrollment key, so nothing is prompted or stored in a script.
MEDUSA_UNATTENDED=1No prompts; wires the shell profile automatically.
MEDUSA_USER_EMAILStamps receipts with the person's identity when the OS user is generic.
MEDUSA_HARNESS_HOMERelocates 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.

Operating it

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.

Troubleshooting

SymptomCause and fix
An agent's tools stopped appearingThe 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_unavailableThe 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 openRun chmod 600 on the files in ~/.medusa/harness. The harness refuses to read secrets with wider permissions.
Policy shows “none” for signatureYour 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 captureEither 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_budgetThe daily token or spend budget in the Models tab is exhausted. Check with medusa budget; the counter resets daily.
A session went read-only unexpectedlyA 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.

Uninstall and rollback

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.

Uninstalling is always safe to attempt: config rewrites keep the original command inside the file and a backup beside it, so a restore is exact even if the harness binary is already gone.

Command reference

CommandWhat it does
medusa setupThe whole onboarding flow. --dry-run, --json, --profile, --no-service, --clients a,b
medusa doctorHealth check; non-zero exit when something needs attention. --json
medusa enrollEnroll only. --org-key, --label, --control-plane, --dashboard
medusa policySync and verify the org policy. --force
medusa checkDry-run the gate. --tool, --args, --server, --record
medusa clientsList MCP client configs and what is wrapped
medusa install / uninstallWrap or restore MCP client configs. --client, --path, --servers, --dry-run
medusa proxyThe stdio seam; what wrapped configs invoke
medusa serveThe HTTP/SSE MCP proxy (port 27182)
medusa llm-proxyThe model API proxy (port 27183)
medusa captureThe GUI egress proxy (port 27184)
medusa capath | fingerprint | trust — the CA the egress proxy uses
medusa runHTTP and model seams in one process; what the service runs
medusa servicestatus | install | uninstall — the background service
medusa envModel base URLs for this machine. --export, --json; on Windows --persist / --unpersist (user environment, reaches GUI apps)
medusa status / budget / flushEnrollment state, model usage, force a telemetry flush
medusa hooksstatus | install | uninstall | managed — wire the harness into Claude Code and Cursor hooks. --client claude_code,cursor, --dry-run
medusa hookclaude-code | cursor — what those hooks run; reads the payload on stdin, never invoked by hand
medusa receiptsThe 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

What is recorded

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

  • Prompt text, file contents and tool arguments stay on the machine.
  • Detected values are never transmitted in full. Events carry a category count and a short truncated preview, so a secret that triggered a block does not itself become a leak in your logs.
  • Traffic to hosts outside the model allowlist is never decrypted by the egress proxy.

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.

Ready to install

The dashboard's install page has the exact commands filled in for your organization, including the unattended one-liner for your MDM.