# AI agent has more write access than the newest hire

> Source: <https://dev.to/pawan_singhkapkoti_ea8a0/ai-agent-has-more-write-access-than-the-newest-hire-h4e>
> Published: 2026-07-17 00:31:35+00:00

When you hire a junior engineer, they get a probation period, a code review process, and strict IAM boundaries. When people deploy an AI agent against their data, it usually gets a system prompt and a prayer.

Hoping the model behaves isn’t a security strategy. I learned this the hard way when an LLM tried to drop a table on my live ADX cluster. It didn't get to, because I stopped hoping and built a wall.

Instead of relying on semantic governance or prompt engineering, I enforce a hard boundary where reads pass and writes die at the door.

Here is the one governance pattern I use to lock down agents, and the three different ways I’ve built it.

The architecture is deliberately simple. The agent never talks to the database. It talks to a gatekeeper. The gate does not care how confident the model is about this DROP.

``` php
[ LLM Agent ] ---> ( SQL Query ) ---> [ Gatekeeper Policy ] --(If SELECT)--> [ Database ]
                                              |
                                              v (Outcome Log)
                                     [ Append-Only Ledger ]
```

A fair objection: a read-only connection or a read replica makes writes physically impossible at the driver level, and that is a stronger boundary than any allowlist. True. This policy layer sits on top of that, not instead of it. A read-only replica still won't stop an over-broad SELECT from hoovering up PII, and it won't hand you the append-only ledger. Defence in depth, not a silver bullet.

Depending on the environment, the enforcement mechanism changes, but the logic remains identical.

For real infrastructure, I use an OPA (Open Policy Agent) container provisioned by Terraform. This is the actual `sql_guard.rego`

policy from my public terraform-lab. It's about twenty lines of modern `rego.v1`

syntax acting as a stand-in for the sql-steward gatekeeper shape. It parses the verb directly from the statement, uses a complete blocklist, and defaults to a hard closed state:

``` python
package sql_guard
import rego.v1

default allow := false

blocked_verbs := {"insert", "update", "delete", "drop", "alter", "truncate", "create", "grant"}

verb(stmt) := lower(split(trim_space(stmt), " ")[0])

allow if {
    verb(input.statement) == "select"
}

deny contains msg if {
    some v in blocked_verbs
    verb(input.statement) == v
    msg := sprintf("statement verb %q is not permitted by sql_guard", [v])
}
```

For the browser-based demo, I enforce the exact same shape using client-side JavaScript. The page is static HTML, and the JS runs directly in the visitor's browser. Notice it shares the identical `verb()`

parser and blocklist structure as the Rego. An unknown verb gets treated the way a compiler treats a typo — it refuses to guess:

``` js
var blocked = ["insert", "update", "delete", "drop", "alter", "truncate", "create", "grant"];

function verb(s) {
  var m = s.trim().toLowerCase().match(/^[a-z]+/);
  return m ? m[0] : "";
}

function judge(s) {
  var v = verb(s);
  if (v === "select") return { allow: true, reason: "read-only statement" };
  if (blocked.indexOf(v) !== -1)
    return { allow: false, reason: 'statement verb "' + v + '" is not permitted by sql_guard' };
  return { allow: false, reason: 'unrecognised verb "' + (v || "?") + '", the gate fails closed' };
}
```

Precision matters. I don't have one monolithic "gateway" — I have one pattern applied across three different environments based on the required risk profile:

| Environment | What it is | How it works |
|---|---|---|
| Browser Demo | The Gatekeeper Page | Policy written in JS running in the browser. Anyone can try it with zero setup to see the pattern in action. |
| Terraform Lab | OPA Container | A real, running container enforcing the ~20-line `sql_guard.rego` policy (a stand-in for the sql-steward pattern). |
| sql-steward | Semantic Layer | Built via sqlglot. No raw SQL ever reaches the DB. The agent's intent is parsed, checked, and entirely rewritten before execution. |

Stopping a `DROP TABLE`

command is the easy part. The actual frontier of AI data governance is managing what the model is allowed to read.

If your agent has access to raw PII, you've already lost. This is where the enforcement has to go: moving toward k-anonymity. The gateway design must evolve so it doesn't just check the verb, but evaluates the query's target against redaction policies. If an agent tries to pull a single identifiable row, the future state of this gateway will need to intercept, generalize, or aggregate that data before returning it, ensuring the LLM only ever receives anonymized datasets.

I wrote the full design for this — column classification, the quasi-identifier / k-anonymity pass, and how it wires into enforcement — here: [PII classification and anonymization design](https://github.com/Pawansingh3889/schema-scout/blob/main/docs/pii-anonymization-design.md).

I put the browser implementation online. Go ahead and try to get a destructive command past it. Worst case, you find a hole and I look silly — better you than my production database.

**Link:** [https://pawan-portfolio.pawankapkoti3889.workers.dev/gatekeeper.html](https://pawan-portfolio.pawankapkoti3889.workers.dev/gatekeeper.html)

This setup solves the immediate bleeding of unauthorized writes, but it isn't finished. The roadmap from here involves tightening the semantic enforcement in sql-steward to handle complex, nested SQL injections that might try to disguise a write as a read, and integrating dynamic k-anonymity thresholds directly into the OPA policy.

If you’re building AI agents that touch production data, stop trusting the prompt. Build a wall.

So where do you draw your boundary — read replica, policy layer, or both?
