← Back to blog

The moves your coding agent can't make

Your coding agent (Claude Code, Cursor, whatever you're driving) will eventually try to deploy before the tests pass, or approve its own pull request, or retry a flaky call and charge the customer twice. Your system prompt politely asks it not to. It does anyway. This post is about the layer that makes those moves not discouraged but unreachable: a call the model literally cannot place.

flowgate sits between the model and your tools as a single checkpoint, and the model only ever sees two tools through it: flowgate.query for reads and flowgate.command for writes. Everything below is the payload: how the agent gets refused, why it can't talk its way around the refusal, and the part most tools skip — exactly where these guarantees stop and you have to wire the rest yourself.

The controls that don't fit in a tool definition

A tool definition can describe a tool: here's the name, here are the arguments, here's the schema. It cannot say any of the things you actually want said about a dangerous action:

  • only after the tests have passed
  • never without a human in the loop
  • at most three times, then stop
  • and write down every attempt, including the refused ones

Those aren't properties of a tool. They're policy. And policy needs somewhere to live that every call has to pass through. That's the gateway: a single checkpoint between the model and the tools, where the rules are enforced instead of hoped for.

What the gateway enforces before the executor runs

Three checks happen between a call arriving and a tool actually running. Each one can stop the call cold.

Schema validation. A transition's inputSchema is JSON Schema, and it runs before the executor. Malformed input, a missing required field, a value outside an allowed enum — rejected with INPUT_SCHEMA_VIOLATION, and the tool never sees it. That's a real boundary: bad input stops at the gate, not inside your code.

Guards. Preconditions evaluated before the executor — permission, role, expr, evidence. If a guard fails the call is rejected with GUARD_REJECTED and the executor never runs.

Actor enforcement. A transition tagged actor: human requires a human principal. An agent or anonymous caller is rejected with ACTOR_MISMATCH before anything executes. The model cannot walk itself through a human-only step — there is no "agree to my own approval" path.

Every one of these is deny-at-a-checkpoint, not hope-the-model-behaves. The call has to get past the gate, and the gate runs whether or not the model expected it to.

Gates anchored to what actually happened

The evidence guard deserves a closer look, because it's a different kind of control. Most checks test a value. An evidence guard tests that something occurred.

When an executor succeeds, it can record evidence — a named kind. A guard on a later transition can require that evidence to exist before the transition is legal. So "this deploy can't run until a security scan recorded a clean result" isn't a flag the model could set on its way past. It's a requirement that a scan step actually ran and actually produced that record on this workflow. And in its counting form, an evidence guard can demand two independent approval records — a real two-person rule. Controls anchored to events, not to mutable state, are the ones a clever caller can't talk its way around.

Approvals the model can't resolve itself

Actor enforcement stops the model from taking a human-only step. The natural next question: who takes it, and how — and can the model reach that path?

It can't, by construction. The human executor records a human.approval.requested event to a named queue and stops. Resolution happens out of band, on a channel the model isn't on: the built-in flowgate approvals command lists and resolves pending approvals from a terminal, and because every approval request is an audit event, you can just as easily tail the audit stream into Slack or a Linear issue so approvals land where people already work.

The security property is the separation. The model emits a request and halts. A human, on a different channel, resolves it. There is no argument shape in flowgate.query or flowgate.command — the model's two tools — that approves its own pending action. The request path and the resolution path are not the same path.

Retries that can't double-charge

Here's a safety property that's easy to miss. The gateway retries failed calls and falls back to alternates — necessary, because networks fail. But a retried side-effecting call is dangerous on its own: a payment that timed out but actually went through, retried, charges the customer twice.

Declaring an idempotency key closes that gap. The runtime computes one key per submit and reuses it across every retry and every fallback executor — surfaced to a REST tool as an Idempotency-Key header, to a CLI tool as an environment variable. The expense-approval example uses exactly this for its payment step. "Retry safely" becomes a line of config, and a double-execution becomes structurally hard instead of something you hope the downstream API deduplicates for you.

What the gateway records

The most underrated security feature is the one that doesn't block anything: the audit trail.

Every meaningful step emits a structured audit event — around eighteen event types, covering workflow starts, transitions, guard evaluations, executor attempts, approval requests, and rejections. Crucially, that includes the calls that failed. A transition.rejected event is written even when the model hit a wall and quietly recovered:

audit event
{
  "id":            "evt_e0b9...",
  "timestamp":     "2026-05-10T18:42:01Z",
  "workflowId":    "wf_3f8b...",
  "correlationId": "cor_9c12...",
  "actor":         "tester",
  "eventType":     "transition.rejected",
  "payload": { "transition": "approve", "code": "ACTOR_MISMATCH" }
}

Each event carries a correlationId that ties together everything from a single call, so you can reconstruct exactly what happened and in what order. This is the difference between "we think the agent didn't do that" and "here is every call it made, every gate it hit, and every refusal." Incident response needs the second answer. An audit trail you didn't have to build yourself is worth more than it looks.

One honest caveat: the default audit sink is stdout, and a none sink exists that drops everything. For any deployment where the trail matters, use the file sink and set up rotation. Audit is only ground truth if you keep it.

The honest part: where these guarantees stop

Here's where most tools would stop, having listed their features. This is the section that matters most — and the good news comes first. If you're a single developer running an agent on your laptop, everything above already works, with zero setup beyond the quick start. The guards you actually reach for there — schema validation, evidence, expr, and actor: human — need no notion of who the caller is. Your OS user is the only principal, and "the model can't deploy before tests pass" and "the model can't approve its own pending action" both hold with nothing wired in.

The one thing that doesn't come for free is telling callers apart. The bundled binary you run from the quick start treats every caller as a single anonymous user. That's the correct default for a laptop, but it has a sharp consequence the moment more than one human shares the server: the permission and role guards — the only identity-based ones — can't express "Alice may deploy, Bob may not." There is no Alice and no Bob, just one anonymous caller. If you lean on those two guards in a shared deployment, they aren't doing the job you think they are. Making them real means wiring identity in yourself — which is the next section. It's a known seam, not a hidden one.

When you outgrow the laptop

When a team starts sharing the server, you wire identity in once — not at every layer. The mechanism is a custom server handler (the bundled binary's swappable front door) that reads a verified identity off the transport — a checked JWT, a client-certificate subject, an authenticated session — and hands it to the runtime as the caller's principal. The project ships four concrete patterns for this.

The clean shape is to do it once at the top. In a stacked deployment the enterprise gateway handles SSO and mints the token; every layer below it receives the already-verified principal through a standard header and trusts it. For callers you don't control — across a real trust boundary — put an identity proxy like Envoy or OAuth2-proxy in front. Same-trust, single-user use is production-ready today; high availability is limited to multiple processes on a single host sharing one SQLite store (there's no networked store backend yet). Throughput under real production load hasn't been measured yet — only the gateway's own per-call overhead, in microbenchmarks. This is a pre-1.0 project, and that "measured versus not yet" list is part of the honest picture.

The trap: identity the model asserts

When you do wire identity, there's one shortcut to refuse outright: never let the caller tell you who it is. A field in the request where the model says "I'm acting as the admin" is not authentication — it's a suggestion, and a model can be talked into making any suggestion.

Identity has to come from the transport and be verified before it ever reaches the runtime — a signature you checked, a certificate subject, a session you established. The model is the thing being governed. It does not get to name its own principal. The project's embedding guide calls this out specifically, because it's the easy mistake.

So where do the guardrails actually live?

Not in the system prompt, and not in the protocol — in a gateway between the model and the tools. flowgate gives you that layer with real teeth for the controls that don't need identity: schema validation, evidence and expression guards, actor enforcement, idempotent retries, and a complete audit trail. For the controls that do need identity, it gives you a documented seam — a verified caller wired in through a custom server handler — and it does not pretend the anonymous-by-default binary is a multi-tenant security boundary.

That last part is the point. A gate that claims to check identity but doesn't is worse than no gate, because you'll trust it. The most useful thing a security layer can do is be exact about its own edges: here is what is enforced today, here is precisely what you wire to enforce the rest, and here is the audit trail that proves what happened either way.

This is a pre-1.0 project, and that honesty is the feature. Read it, wire the seam your deployment needs, and keep the trail.

The security policy and reporting process are in SECURITY.md; identity wiring for multi-tenant deployments is covered in the MCP control architecture guide and the embedding guide.