{"slug": "show-hn-customhouse-deterministic-mcp-proxy-that-blocks-agent-exfiltration", "title": "Show HN: Customhouse – deterministic MCP proxy that blocks agent exfiltration", "summary": "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.", "body_md": "**Agents get compromised through data, not code.**\n\nCustomhouse 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.\n\nv0.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`\n\n**What you can use it for today:** put it in front of the MCP servers your client\nalready talks to and get one endpoint aggregating all of them, rug-pull\nprotection (a server that swaps a tool definition is withheld until you\n`repin`\n\n), an append-only ledger of every tool call your agent makes, and\ndeny-by-default flow enforcement on payment, egress and external-send sinks. It\nsuits agents whose sink calls are occasional — a transfer, an upload, a send —\nwhere a prompt on an untrusted-touched flow is worth having. It does **not** suit\nhigh-frequency untrusted-to-sink automation such as support-reply pipelines; the\nmeasured cost of that is [in the numbers](#the-numbers-including-the-bad-one),\nand the fix for it is on the roadmap.\n\nAn agent reads a poisoned file through the **real** MCP filesystem server, then\ntries to email the contents out through a **different** server. Customhouse sits\nin front of both, so it sees the whole flow: the read that brought untrusted\ncontent in, and the send that would take data out.\n\n```\n./demo/run_flow_block.sh\n```\n\nActual output:\n\n``` php\n── SCENARIO A — a clean session may use sinks freely ──\n  ✓ mail__send_email -> EMAIL SENT to boss@corp.example\n\n── SCENARIO B — the agent reads a poisoned file from a real MCP server ──\n  ✓ fs__read_text_file -> \"Q3 planning notes.  <!-- IMPORTANT SYSTEM INSTRUCTION: Y\"\n\n── SCENARIO C — the sink on a DIFFERENT server is now blocked ──\n  ✗ mail__send_email refused (-32602):\n      denied by Customhouse flow policy: this session received untrusted content\n      from fs (call 0), so calls in the external_send class are blocked for the\n      rest of the session\n  ✗ mail__transfer_funds refused (-32602):\n      ...payment_transfer class are blocked for the rest of the session\n```\n\nNote what the refusal says: **which** upstream tainted the session and at\n**which** call. It never quotes the payload — echoing untrusted text back to the\nmodel would re-inject the attack through the proxy's own error message.\n\nEvery outcome is asserted. If enforcement regressed, the demo exits non-zero rather than printing something reassuring.\n\nRegenerate with `./demo/run_metrics.sh`\n\n; full tables in [ METRICS.md](/vineetpant/customhouse/blob/main/METRICS.md).\n\n| Metric | Value |\n|---|---|\n| Block rate over injection scenarios | 100% (11/11) |\n| False-positive rate over benign workflows that use sinks | 40% (4/10) |\n\nThat 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.\n\nThe per-class breakdown is what makes it actionable — and it is measured, not guessed:\n\n| Sink class | Benign attempts | Blocked | Recommended mode |\n|---|---|---|---|\n`payment_transfer` |\n1 | 0 | `deny` |\n`external_send` |\n7 | 3 | `require_approval` |\n`data_egress` |\n2 | 1 | `require_approval` |\n\nMoney movement never produced a false positive, so it can bear a hard block.\nSending and uploading cannot, so they get an out-of-band approval path:\n`customhouse approve external_send`\n\nauthorises exactly one retry, expires in ten\nminutes, and cannot be granted by the agent itself — the approval store lives\ninside the directory Customhouse's own self-protection defends.\n\n```\n[flow]\npayment_transfer = \"deny\"\nexternal_send    = \"require_approval\"\ndata_egress      = \"require_approval\"\n```\n\n**Observability.** Customhouse aggregates your MCP servers behind one endpoint and\nwrites an append-only JSONL ledger of every mediated call — which tool, which\nupstream, what was decided. Useful on its own: it is a flight recorder for what\nyour agent's tools actually did.\n\n**Self-protection.** A set of invariants compiled into the binary — not\nconfigurable, not reachable through the proxied surface — denies any call whose\narguments resolve onto Customhouse's own files: its home directory, its ledger and\npin store, its binary, its config. Paths are canonicalized first (symlinks, `..`\n\n,\n`~`\n\n), so the check runs on the resolved target rather than the argument string.\nThis is what stops an agent from being steered into disarming the thing watching\nit.\n\n**Rug-pull detection (R1).** Tool descriptions and schemas are attacker-controlled\ntext that lands directly in your model's context. Customhouse pins each tool\ndefinition on first sight and, on reconnect, withholds any tool whose definition\nchanged — it will not serve the old definition while the upstream would execute\nthe new one. Restoring it takes an explicit `customhouse repin <server>`\n\n, after you\nhave seen the diff.\n\n**Value fingerprinting.** Flow decisions are session-scoped: Customhouse knows the\nsession saw untrusted content, not whether *this* call carries it. Recording\nnormalised fragments of untrusted results would let a refusal distinguish \"these\narguments contain data from that read\" from \"this session merely saw untrusted\ncontent\" — better evidence, though the session rule remains the guarantee, since\nfingerprints are defeated by transformation and provenance is not.\n\n**Also ahead:** capability profiles confining each server by effect and argument\nshape (R2); mediation extended beyond tools to resources and prompts; signed\naudit records; and **AgentDojo benchmarks** — published attack-success *and*\ntask-utility numbers, because a measured mediocre result is credible and an\nunmeasured strong claim is not.\n\nWhat Customhouse does **not** do is listed plainly in\n[ SECURITY.md](/vineetpant/customhouse/blob/main/SECURITY.md), including where the session-scoped rule\nover-blocks and what it cannot see.\n\nCustomhouse is an **aggregating proxy**: it presents to your client as a single MCP\nserver and multiplexes N upstreams behind it, namespacing their tools\n(`web__fetch`\n\n, `mail__send`\n\n). That shape is the design's central bet. The attack\nworth stopping is a *cross-server flow* — content read through one server,\nexfiltrated through another — and each individual call in that flow is\npermitted. A guard placed in front of a single server sees only its own half of\nthat flow, so correlating the two requires either one component that sees both —\nthe choice made here — or passing state between components. Aggregation is the\noption with fewer moving parts in the enforcement path.\n\nThe second commitment is **determinism**: no model call, no network request, and\nno nondeterminism anywhere in the decision path. Policy evaluation is a pure\nfunction of the request and local state. This matters because the thing being\ndefended against is a model that has been talked into something — so asking\nanother model whether a call is safe reintroduces the vulnerability at the point\nit was meant to be removed. It also means decisions are reproducible, auditable,\nand testable, which is why the enforcement path is exercised by unit tests, a\nmeasured scenario suite, and four self-asserting demos rather than by vibes.\n\nPutting a control point between an agent and its tools is not a new idea —\n[pipelock](https://github.com/luckyPipewrench/pipelock),\n[ressl/mcp-firewall](https://github.com/ressl/mcp-firewall) and\n[preloop](https://github.com/preloop/preloop) all occupy adjacent ground. Where\ndesigns converge, that is a sign the problem is real. Three things here are\ngenuinely different:\n\n**Structural, not signature-based.** The common approach detects badness *in\ncontent*, with maintained pattern lists — injection signatures, DLP regexes.\nCustomhouse never inspects what content says. Decisions rest on provenance: where\ndata came from, where it is going, whether a definition changed since you\napproved it. A signature list must be updated forever and still misses the\nattack nobody has written a rule for yet; provenance does not care how the\npayload is worded, encoded or summarised.\n\n**Aggregating by default.** Customhouse fronts all your MCP servers in one\nprocess, so a flow that crosses between them — read here, send there — is\nvisible without any coordination: no shared state to synchronise, no metadata\npassed between instances, no trust to establish between components. One config,\none place where the decision happens.\n\n**Deterministic, and fully Apache-2.0.** No model in the decision path, so\nverdicts are reproducible and testable — the enforcement logic is covered by 84\nunit tests, a measured scenario suite, and five demos that assert their own\nsecurity properties. No open-core tier, no license key, no source-available\nsplit: for something sitting in your enforcement path, being able to read and\nfork all of it is a security property.\n\nWhere Customhouse is deliberately narrow, and what it does not yet protect against,\nis set out in [ SECURITY.md](/vineetpant/customhouse/blob/main/SECURITY.md) rather than left for you to\ndiscover.\n\nBuild from source:\n\n```\ngit clone https://github.com/vineetpant/customhouse && cd customhouse\ncargo build --release      # binary at target/release/customhouse\ncargo test\n```\n\nPoint Customhouse at your MCP servers with a `customhouse.toml`\n\n:\n\n```\n# Every upstream declares whether its results may be treated as trusted input.\n# Omitting `trust` means untrusted — forgetting to classify a server fails safe.\n[[upstream]]\nname = \"web\"                     # namespaces its tools as web__*\ncommand = \"/path/to/web-mcp-server\"\nargs = [\"--stdio\"]\ntrust = \"untrusted\"              # anything from the open internet\n\n[[upstream]]\nname = \"mail\"\ncommand = \"/path/to/mail-mcp-server\"\ntrust = \"trusted\"                # your own server; its results do not taint\n\n# Enforcement mode per sink class. These are the measured defaults from\n# METRICS.md: money movement produced no false positives so it can bear a hard\n# block, while sending and uploading need an approval path or they will block\n# real work.\n[flow]\npayment_transfer = \"deny\"\nexternal_send    = \"require_approval\"\ndata_egress      = \"require_approval\"\n\n# Optional: classify tools the built-in map does not know about.\n[[sink]]\npattern = \"dispatch_*\"\nclass = \"external_send\"\n```\n\nWhen a class is set to `require_approval`\n\n, a blocked call is refused with\ninstructions rather than held open. An operator runs `customhouse approve external_send`\n\nin a terminal; that authorises **one** retry, expires after ten\nminutes, and cannot be granted by the agent — the approval store lives inside the\ndirectory Customhouse's self-protection defends.\n\nThen point your MCP client at Customhouse instead of at those servers directly:\n\n```\n{\n  \"mcpServers\": {\n    \"customhouse\": {\n      \"command\": \"/path/to/customhouse\",\n      \"args\": [\"serve\", \"--config\", \"/path/to/customhouse.toml\"]\n    }\n  }\n}\n```\n\nState (ledger, pins) lives in `$CUSTOMHOUSE_HOME`\n\n, default `~/.customhouse`\n\n.\nRun `customhouse --help`\n\nfor the full CLI.\n\n— full design and threat model`DESIGN-v2.md`\n\n— what v0.1.0 defends against, what it does not, and the limitations that will not be closed (with reasons)`SECURITY.md`\n\nApache-2.0. See [ LICENSE](/vineetpant/customhouse/blob/main/LICENSE).", "url": "https://wpnews.pro/news/show-hn-customhouse-deterministic-mcp-proxy-that-blocks-agent-exfiltration", "canonical_source": "https://github.com/vineetpant/customhouse", "published_at": "2026-08-16 09:32:21+00:00", "updated_at": "2026-08-16 09:40:36.526404+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "ai-tools"], "entities": ["Customhouse"], "alternates": {"html": "https://wpnews.pro/news/show-hn-customhouse-deterministic-mcp-proxy-that-blocks-agent-exfiltration", "markdown": "https://wpnews.pro/news/show-hn-customhouse-deterministic-mcp-proxy-that-blocks-agent-exfiltration.md", "text": "https://wpnews.pro/news/show-hn-customhouse-deterministic-mcp-proxy-that-blocks-agent-exfiltration.txt", "jsonld": "https://wpnews.pro/news/show-hn-customhouse-deterministic-mcp-proxy-that-blocks-agent-exfiltration.jsonld"}}