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:
{
"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.