Show HN: Customhouse – deterministic MCP proxy that blocks agent exfiltration Customhouse v0.2.0, a deterministic MCP proxy that blocks agent data exfiltration by tracking input provenance, reports a 100% block rate (11/11) over injection scenarios and a 40% false-positive rate (4/10) on benign workflows using sinks. The tool, designed for local use and not yet a production guarantee, denies payment, egress, and external-send calls in sessions that have received untrusted content, with measured per-class breakdowns showing payment_transfer can be hard-blocked while external_send and data_egress require approval. Agents get compromised through data, not code. Customhouse tracks which upstream every input came from, and deterministically blocks money-moving or data-egress calls in any session that has received untrusted content. No model sits in the decision path, and no payload is ever pattern-matched — the block follows from provenance alone, so it cannot be evaded by rewording, summarising or base64-ing the payload. v0.2.0 is a working reference monitor with measured results — use it locally, read the numbers, break it. It is not yet a production exfiltration guarantee, and says exactly where the line is. SECURITY.md What you can use it for today: put it in front of the MCP servers your client already talks to and get one endpoint aggregating all of them, rug-pull protection a server that swaps a tool definition is withheld until you repin , an append-only ledger of every tool call your agent makes, and deny-by-default flow enforcement on payment, egress and external-send sinks. It suits agents whose sink calls are occasional — a transfer, an upload, a send — where a prompt on an untrusted-touched flow is worth having. It does not suit high-frequency untrusted-to-sink automation such as support-reply pipelines; the measured cost of that is in the numbers the-numbers-including-the-bad-one , and the fix for it is on the roadmap. An agent reads a poisoned file through the real MCP filesystem server, then tries to email the contents out through a different server. Customhouse sits in front of both, so it sees the whole flow: the read that brought untrusted content in, and the send that would take data out. ./demo/run flow block.sh Actual output: php ── SCENARIO A — a clean session may use sinks freely ── ✓ mail send email - EMAIL SENT to boss@corp.example ── SCENARIO B — the agent reads a poisoned file from a real MCP server ── ✓ fs read text file - "Q3 planning notes. < -- IMPORTANT SYSTEM INSTRUCTION: Y" ── SCENARIO C — the sink on a DIFFERENT server is now blocked ── ✗ mail send email refused -32602 : denied by Customhouse flow policy: this session received untrusted content from fs call 0 , so calls in the external send class are blocked for the rest of the session ✗ mail transfer funds refused -32602 : ...payment transfer class are blocked for the rest of the session Note what the refusal says: which upstream tainted the session and at which call. It never quotes the payload — echoing untrusted text back to the model would re-inject the attack through the proxy's own error message. Every outcome is asserted. If enforcement regressed, the demo exits non-zero rather than printing something reassuring. Regenerate with ./demo/run metrics.sh ; full tables in METRICS.md /vineetpant/customhouse/blob/main/METRICS.md . | Metric | Value | |---|---| | Block rate over injection scenarios | 100% 11/11 | | False-positive rate over benign workflows that use sinks | 40% 4/10 | That 40% is not a bug to be explained away — it is the cost of a rule that cannot be evaded. Session-scoped taint blocks legitimate work too: reading a support ticket and replying to it looks identical, at the tool boundary, to reading a poisoned ticket and exfiltrating through the reply. The per-class breakdown is what makes it actionable — and it is measured, not guessed: | Sink class | Benign attempts | Blocked | Recommended mode | |---|---|---|---| payment transfer | 1 | 0 | deny | external send | 7 | 3 | require approval | data egress | 2 | 1 | require approval | Money movement never produced a false positive, so it can bear a hard block. Sending and uploading cannot, so they get an out-of-band approval path: customhouse approve external send authorises exactly one retry, expires in ten minutes, and cannot be granted by the agent itself — the approval store lives inside the directory Customhouse's own self-protection defends. flow payment transfer = "deny" external send = "require approval" data egress = "require approval" Observability. Customhouse aggregates your MCP servers behind one endpoint and writes an append-only JSONL ledger of every mediated call — which tool, which upstream, what was decided. Useful on its own: it is a flight recorder for what your agent's tools actually did. Self-protection. A set of invariants compiled into the binary — not configurable, not reachable through the proxied surface — denies any call whose arguments resolve onto Customhouse's own files: its home directory, its ledger and pin store, its binary, its config. Paths are canonicalized first symlinks, .. , ~ , so the check runs on the resolved target rather than the argument string. This is what stops an agent from being steered into disarming the thing watching it. Rug-pull detection R1 . Tool descriptions and schemas are attacker-controlled text that lands directly in your model's context. Customhouse pins each tool definition on first sight and, on reconnect, withholds any tool whose definition changed — it will not serve the old definition while the upstream would execute the new one. Restoring it takes an explicit customhouse repin