We built SentinelGate — an open-source MCP proxy that intercepts every AI agent tool call and evaluates it against your policies before it executes. Go, single binary, CEL policy engine, full audit trail.
We thought it was ready three times. Each time, something proved us wrong — bugs, sure, but also architectural decisions that felt obvious until someone tried to use the thing and they weren't obvious at all.
Why a proxy, not a wrapper
The first instinct when you want to control what an AI agent does is to wrap it. Hook into the agent's process, intercept calls from inside, apply your rules. We tried this. It works — until you have more than one agent.
A wrapper means integration. If you're running Claude Code, Gemini CLI, Cursor, and a Python script using the MCP client SDK, that's four different integration points. Four different hook mechanisms. Four things that break when the agent updates. And when Codex ships next month with its own MCP support, that's a fifth.
We scrapped the wrappers and went proxy.
SentinelGate sits between the agent and the upstream MCP servers. The agent connects to localhost:8080/mcp — that's the only address it knows. The real servers are configured inside SentinelGate. The agent can't skip the proxy because it doesn't have the information to reach anything else. Not enforcement by cooperation. Architecture.
This matters for a specific reason that goes beyond convenience. A wrapper lives inside the agent's process. If the agent gets compromised — prompt injection, tool poisoning, whatever — the wrapper goes with it. The attacker is already inside the house; the lock on the bedroom door isn't going to help much. A proxy is a separate process. The agent can be fully compromised and the proxy still evaluates every tool call the same way it did before the compromise.
A wrapper protects the agent from itself. A proxy protects the system from the agent. Those are different trust models, and for access control the proxy is the right one.
Why CEL, not our own language
We needed rules that evaluate in microseconds and can't accidentally bring down the proxy. That narrowed the options fast.
We could have designed our own DSL — tailored syntax, exactly the semantics we wanted. There's always a pull towards building the thing yourself.
We went with CEL instead. Not because it was easier, but because we'd have been idiots not to.
CEL is what Kubernetes uses for admission webhooks. It's what Firebase uses for security rules. It's what Envoy and Google Cloud IAM use. These are systems where getting policy evaluation wrong means production outages or security breaches. CEL has survived that pressure for years. A custom DSL we wrote in a month wouldn't have.
What actually sealed it for a proxy: CEL expressions can't loop, can't call external services, can't modify state. They evaluate and return a boolean. Evaluation takes microseconds. When you're sitting in the hot path of every tool call, you can't afford a policy engine that sometimes takes 50ms because someone wrote a recursive rule. CEL makes that structurally impossible.
And cel-go is a library, not a daemon. It compiles into our binary. No separate process, no network call, no dependency to manage. Consistent with the "single binary, zero dependencies" promise.
We looked at OPA/Rego. More powerful, absolutely. But it requires a separate daemon, Rego has a steep learning curve, and it's built for evaluating complex policy bundles across distributed systems. We're evaluating a single tool call against a rule set. CEL does exactly that, nothing more.
In practice, most users don't write CEL at all. Simple patterns cover the majority of cases:
# Block tools with "secret" in any argument
action_arg_contains(arguments, "secret")
# Only admins can run shell commands
action_name == "bash" && !("admin" in identity_roles)
# Block exfiltration to paste services
dest_domain_matches(dest_domain, "*.pastebin.com")
The full expression language is there when you need it. Most people don't.
But having a powerful policy language also means you can build things that shouldn't exist. We had a "Budget Guardrail" feature — a button on each tool that opened a CEL editor pre-filled with something like session_cumulative_cost > 50.00. Sounds reasonable. In practice, the CEL variable behind it didn't even work properly, writing a CEL expression to say "maximum $50" was overkill for what should be a simple input field, and the UI put the button on individual tools while the backend calculated budgets per identity. Everything about it was confused. We ripped it out and replaced it with a straightforward budget field. Sometimes the right decision is to remove the clever thing and build the obvious one.
Why zero-config matters more than we thought
We had zero-config from early on. Download, run, working proxy with an admin UI — that was always the idea. What we underestimated was how much further we needed to go.
The reasoning was simple: our users are developers already mid-task with an AI agent. They want security without stopping what they're doing. If a security tool adds friction, it doesn't get used. It sits in a README with a star and never gets installed.
So the baseline was always: sentinel-gate start gives you a working proxy with a browser-based admin UI. Policies created visually. Upstreams added with a URL. State persisted automatically. YAML exists for infrastructure tuning — port binding, rate limits — but it's not required for any base use case.
That part was the easy decision. The hard part was everything we discovered when we actually watched people use it.
Every new build went through the same ritual. Every page, every flow, every feature — notes in a markdown file, one line per issue. The first round came back with about 80 items. Fix, rebuild, retest. The next round: 60-something. Then lower. But the number never just dropped — while checking the fixes, new issues would surface. Things that only became visible once the previous layer of problems was out of the way. Each round carried forward what was still broken and added what was newly discovered.
Some of what came up:
We had UUIDs everywhere. Activity logs, agent views, notifications, cost tracking — all showing id-7f3a2b1c instead of claude-prod. We hadn't noticed because we knew who id-7f3a2b1c was. Nobody else did.
The notification system was generating 16 identical HEALTH-MONITOR alerts and 23 identical tool.removed notifications. No grouping, no distinction between things you need to act on and things that are informational. Just noise.
A page called "Permission Health" — which made perfect sense to us — meant nothing to anyone who hadn't designed the data model. We renamed it "Access Review." Small change, big difference in whether someone actually clicks on it.
The Policy Builder's "New Rule" panel was too narrow to write conditions in. The CEL tab had no examples. Content scanning could be enabled but there was no visible indication it was actually detecting anything — you'd turn it on and wonder if it was working. Date formats were American (MM/DD/YYYY) on a tool built in London.
None of these are glamorous fixes. All of them are the difference between someone trying SentinelGate for five minutes and someone actually using it.
We're not done. The usability is still a work in progress — maybe it always will be. If you try it and something blocks you or doesn't make sense, that's exactly the feedback we want. The help panels, the getting started flow, the one-click reset to start over — those all exist because someone told us what was broken.
Why the threat model is in the README
SentinelGate is an MCP proxy. It controls what passes through the MCP protocol. If an agent bypasses MCP entirely — a direct syscall, a native file operation, a curl command that doesn't go through any MCP server — SentinelGate doesn't see it. For that, you need containers, VM sandboxes, OS-level isolation.
We put this in the README, not buried in the docs. Deliberately.
Anyone who's worked in security knows that no single tool covers everything. The tool that claims it does is the one you don't trust. Being explicit about the perimeter — what SentinelGate protects, what it doesn't, and what you should pair it with — builds more credibility than a feature list twice as long.
Try it, break it, tell us what's wrong
curl -sSfL https://raw.githubusercontent.com/Sentinel-Gate/Sentinelgate/main/install.sh | sh
sentinel-gate start
Point your agent at localhost:8080/mcp, create a deny rule from the admin UI, and watch it block in real time.
GitHub: github.com/Sentinel-Gate/Sentinelgate
Site: sentinelgate.co.uk
Top comments (0)