{"slug": "put-a-policy-gateway-between-your-coding-agent-and-the-llm", "title": "Put a Policy Gateway Between Your Coding Agent and the LLM", "summary": "A developer has built Cencurity Engine, an open-source local proxy that sits between coding agents and LLM providers to inspect response streams and enforce allow, redact, or block policies. The gateway, written in Go, can be configured with a simple command and supports any agent that exposes a base URL, such as Roo Code, Continue, Claude Code, and Gemini CLI. It aims to prevent malicious or accidental outputs, like leaked API keys or unsafe code, from reaching the developer's editor.", "body_md": "Your coding agent talks to a model provider over HTTPS. That connection is a straight line: the agent asks, the provider answers, the answer lands in your editor. Nothing in the middle looks at what came back.\n\nFor most of what an agent produces, that's fine. For the rest of it — the query built by string concatenation, the API key the model helpfully echoed back into a code sample, the `eval()`\n\non user input — you find out later, in review, or in a scanner run, or never.\n\nThis is a walkthrough of putting a policy layer in that line: a local proxy your agent points at instead of the provider, which inspects the response stream and decides `allow`\n\n, `redact`\n\n, or `block`\n\nbefore the text reaches you.\n\nI'll use [Cencurity Engine](https://github.com/cencurity/cencurity-engine) because it's the one I build, it's Apache-2.0, and it runs entirely on your machine. The pattern generalises — if you're building your own gateway, the steps below are still the shape of the problem.\n\nThat last one is the real prerequisite. If your tool hardcodes the provider endpoint, none of this applies to it. Most don't: Roo Code, Continue, Claude Code and Gemini CLI all expose a base URL, and anything reading `OPENAI_API_BASE`\n\nwill work too.\n\nClone the repo, open a terminal in it, and run:\n\n```\ngo run ./cmd/cast serve \\\n  --listen :8080 \\\n  --upstream https://api.openai.com \\\n  --policy ./cast.rules.example.json\n```\n\nThree flags, and each one is doing something you should understand before moving on:\n\n`--listen`\n\nis where the gateway accepts traffic. Local only.`--upstream`\n\nis your real provider base URL. Swap it for `https://api.anthropic.com`\n\n, `https://api.deepseek.com`\n\n, `https://api.x.ai`\n\n— whatever you actually use.`--policy`\n\nis the rule file. `cast.rules.example.json`\n\nships in the repo and is a working starter set, not a placeholder.Note what is *not* in that command: your API key. The gateway forwards whatever `Authorization`\n\nheader your agent sends. The key stays where it already lives, which means adding this layer doesn't create a second place a credential can leak from.\n\n```\ngo run ./cmd/cast doctor\n```\n\n`doctor`\n\nloads your config and reports the active rule count. Run it now, and run it again every time you edit the policy file. A JSON typo that silently drops half your rules is the exact failure mode this catches — a gateway with zero loaded rules passes everything and looks perfectly healthy from the outside.\n\nThe gateway also exposes:\n\n`http://localhost:8080/healthz`\n\n— liveness`http://localhost:8080/metrics`\n\n— Prometheus-format plaintextCurl `/healthz`\n\nbefore you repoint anything. If it doesn't answer, your agent is about to fail every request and you'll waste twenty minutes blaming the agent.\n\nChange your agent's API base URL from the provider to `http://localhost:8080`\n\n. The paths are passthrough, so the endpoint shape you were already using keeps working:\n\n`http://localhost:8080/v1/chat/completions`\n\n`http://localhost:8080/v1/messages`\n\n`http://localhost:8080/v1beta/models/{model}:streamGenerateContent`\n\nThen use your agent normally. If you skip this step nothing breaks — your traffic simply keeps going straight to the provider and the gateway sits there doing nothing. That's a surprisingly easy state to end up in and believe you're protected, so verify with the tests in the next step rather than assuming.\n\nTesting a security control by hoping it never triggers is not testing it. Drive each outcome deliberately. Use `curl -N`\n\nso the SSE stream stays open:\n\n**allow** — ask for something ordinary, like a function that sums a list. The stream should flow normally and the structured stdout log should carry `\"action\":\"allow\"`\n\n.\n\n**redact** — ask the model to print a string shaped like a secret. The stream stays open, the matching token comes through as `[REDACTED]`\n\n, and the log shows `\"action\":\"redact\"`\n\n.\n\n**block** — ask for Python that uses `eval`\n\non a string. The stream terminates right after the matching chunk. Downstream receives `: blocked by cencurity`\n\nfollowed by `data: [DONE]`\n\n, and the log shows `\"action\":\"block\"`\n\n.\n\nThat last one is the detail worth internalising. A block is not a clean HTTP error — the connection is already open and streaming when the decision happens. Your agent sees a stream that ends early. If your tooling treats an early `[DONE]`\n\nas a successful empty completion, you'll get silent truncation rather than a visible refusal, and you'll want to know that before you turn enforcement on for a team.\n\nThe policy file is JSON. Each rule takes six fields:\n\n```\n{\n  \"id\": \"cast.custom.internal-hostname\",\n  \"category\": \"secrets\",\n  \"severity\": \"medium\",\n  \"action\": \"redact\",\n  \"pattern\": \"(?i)\\\\b[a-z0-9-]+\\\\.internal\\\\.example\\\\.com\\\\b\",\n  \"enabled\": true\n}\n```\n\n`pattern`\n\nis a **Go regex**, and that constraint matters more than it looks. Go's `regexp`\n\npackage is RE2: linear-time guaranteed, and therefore no lookahead, no lookbehind, no backreferences. If you're porting patterns from a PCRE-based tool, the ones leaning on `(?=...)`\n\nwill not compile. This is a good trade for something sitting in a hot path — RE2 can't catastrophically backtrack and stall your editor — but it does mean some rules have to be rewritten rather than pasted.\n\nSave the file. Rules reload automatically on the next access after the reload interval, which defaults to 3 seconds (`CENCURITY_POLICY_RELOAD_MS`\n\n). No restart. Run `doctor`\n\nagain to confirm the count went up.\n\nStart with `\"action\": \"redact\"`\n\nor a low severity while you're calibrating. A rule that blocks is a rule that can interrupt someone mid-task, and a pattern-based rule on generated code will produce false positives — that's the nature of the technique, not a bug you can tune away entirely.\n\nThe question that decides whether anyone keeps this turned on is: *does routing through the gateway change what I get back?*\n\n```\ngo run ./cmd/cast shadowtest \\\n  --upstream https://api.x.ai \\\n  --model grok-4-0709 \\\n  --api-key-file ./upstream-api-key.txt \\\n  --concurrency 1 \\\n  --iterations 5 \\\n  --timeout 90s\n```\n\n`shadowtest`\n\nruns the same prompts direct and through the proxy against a real upstream and compares the streams, across four default scenarios: `allow-short`\n\n, `allow-long`\n\n, `redact`\n\n, `block`\n\n. Add `--provider anthropic`\n\nor `--provider gemini`\n\nif auto-detection doesn't pick your upstream correctly.\n\nRun `allow-long`\n\nin particular. Short responses hide streaming bugs; long ones surface them. An inline control layer that subtly mangles a 2,000-token response is worse than no control layer, because you'll spend a week blaming the model.\n\nWhen a rule fires you get a structured finding rather than a line number:\n\n| Field | Example |\n|---|---|\n`language` |\n`python` |\n`framework` |\n`fastapi` |\n`rule_id` |\n`cast.fastapi.auth.jwt-verify-disabled` |\n`severity` |\n`high` |\n`confidence` |\n`high` |\n`action` |\n`block` |\n`evidence` |\n`eval(user_input)` |\n\nAnd the honest framing of what this is: heuristic, stream-time guardrails. Not a semantic analyser. No type information, no data-flow graph, no way to know whether the value being concatenated is genuinely attacker-controlled. The engine's own README says as much — these are guardrails, *not a full semantic SAST engine*.\n\nWhich is the point. It runs at a moment nothing else covers: while the code is being written, before it's in your file. Your SAST pipeline still runs afterward, and still catches things this can't. Keep both.\n\n*I build Cencurity, an open-source policy-driven security gateway for LLM coding agents (Apache-2.0). Writing about LLM security, guardrails, and the gap between generated code and reviewed code.*", "url": "https://wpnews.pro/news/put-a-policy-gateway-between-your-coding-agent-and-the-llm", "canonical_source": "https://dev.to/sangyeonpark/put-a-policy-gateway-between-your-coding-agent-and-the-llm-22mg", "published_at": "2026-08-27 18:03:37+00:00", "updated_at": "2026-08-27 18:48:59.835790+00:00", "lang": "en", "topics": ["ai-safety", "developer-tools", "ai-agents"], "entities": ["Cencurity Engine", "Roo Code", "Continue", "Claude Code", "Gemini CLI", "OpenAI", "Anthropic", "DeepSeek"], "alternates": {"html": "https://wpnews.pro/news/put-a-policy-gateway-between-your-coding-agent-and-the-llm", "markdown": "https://wpnews.pro/news/put-a-policy-gateway-between-your-coding-agent-and-the-llm.md", "text": "https://wpnews.pro/news/put-a-policy-gateway-between-your-coding-agent-and-the-llm.txt", "jsonld": "https://wpnews.pro/news/put-a-policy-gateway-between-your-coding-agent-and-the-llm.jsonld"}}