Agent Governance Belongs in Your Data Layer

Your Claude agent just got write access to the CRM, the invoicing database, and the support inbox. Everything works in testing. Then someone asks the question every architecture review eventually hits: when the agent tries something it was never authorized to do, what actually stops it? If the honest answer is "the system prompt says not to," you don't have governance. You have a polite request.
The core problem: instructions are not controls
An agent's system prompt is an instruction. A database permission is a control. The difference is whether the system can say no when the agent asks anyway.
A recent sponsored piece on VentureBeat, written by Max Romanenko of EDB, makes this point well: governance has to be executable, enforced at the operational data layer, in context, at the moment the agent acts. Two details from it are worth keeping. An access policy only matters if the system can deny the access when the agent requests it. And an auditability principle only matters if you can reconstruct what the agent did, what data it touched, which user it acted for, and what resulted.
A caveat on sourcing: that article is sponsored content from a vendor that sells Postgres-based tooling, so treat it as one perspective, not independent evidence. The underlying argument, though, doesn't depend on any vendor. It's a standard security principle applied to a new kind of caller: enforce at the boundary the caller can't talk its way past.
An LLM can be prompt-injected, can misread a task, and can confidently call the wrong tool with the wrong arguments. Any control that lives inside the model's reasoning inherits those failure modes. Any control that lives in the database, the OS sandbox, or the credential scope does not.
Here's the mental model I use. Think of four layers, from least to most reliable:
| Layer | What it is | Can the agent bypass it? |
|---|---|---|
| Prompt | "Never delete records" | Yes: injection, misreading, drift |
| Tool-level rules | Allow/deny lists in the agent runtime | Not by reasoning, but they only cover the tools the runtime knows about |
| Credential scope | What the token or DB role can do | No, unless you over-grant |
| Data layer | Row-level security, constraints, grants | No, the database refuses |
You want all four, but you should design from the bottom up. If the data layer says no, the layers above it become defense in depth instead of the only defense.
Why this matters now, with honest numbers
Gartner predicted in June 2025 that over 40% of agentic AI projects will be canceled by the end of 2027, citing escalating costs, unclear business value, and inadequate risk controls (Gartner press release). That's a forecast, not a measured result, and it's a year-old prediction, so don't read it as "40% have already been canceled." But "inadequate risk controls" is exactly the failure this post is about.
The same Gartner release predicts that agentic AI will make a growing share of day-to-day work decisions autonomously and show up in a growing share of enterprise software over the next few years. If those forecasts land even approximately, small teams will be running agents with real authority over real data, and most will be doing it with permissions designed for humans.
For a 5-person business, the realistic version of this risk isn't a rogue superintelligence. It's an agent that drafts and sends 200 follow-up emails to the wrong segment, marks open invoices as paid, or updates every customer record instead of one. All of those are boring, and all are preventable at the data layer.
Step 1: Give the agent its own database role, and make it small
The most common mistake I see: the agent connects with the same credentials as the app, or worse, an admin connection string pasted from a tutorial. In PostgreSQL, superusers and roles with the BYPASSRLS attribute skip row-level security entirely, and table owners bypass it too unless FORCE ROW LEVEL SECURITY is set. So the first decision is which role the agent connects as.
Create a dedicated role per agent, not per team:
-- One role per agent. No superuser, no BYPASSRLS, not a table owner.
CREATE ROLE agent_followup LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS;
-- Grant only what this agent's job requires
GRANT SELECT ON leads, email_templates TO agent_followup;
GRANT INSERT ON outreach_log TO agent_followup;
GRANT UPDATE (last_contacted_at, status) ON leads TO agent_followup;
-- Explicitly nothing on: invoices, customers.payment_*, users
Notice the column-level grant on leads. The agent can update two columns, not the whole row. If a prompt injection convinces it to overwrite email or owner_id, Postgres rejects the statement. No prompt can change that.
Separate roles also fix your audit problem. When every action shows up under agent_followup, you can answer "what did this agent touch?" with a query instead of a forensic investigation.
Step 2: Row-level security so the agent only sees its slice
Table grants control which tables and columns. Row-level security controls which rows. This is where you encode business rules like "this agent only works leads assigned to the sales pipeline, never the existing-customer list."
Two behaviors from the PostgreSQL docs matter:
- A policy only takes effect once you run
ALTER TABLE ... ENABLE ROW LEVEL SECURITYon the table. - If RLS is enabled and no applicable policy exists, Postgres assumes default deny: no rows are visible or updatable.
Default deny is the property you want. A forgotten policy fails closed, not open. For a plain-language walkthrough, the Neon guide to row-level security is a useful companion.
ALTER TABLE leads ENABLE ROW LEVEL SECURITY;
ALTER TABLE leads FORCE ROW LEVEL SECURITY; -- applies even to the table owner
-- The follow-up agent may only see leads in an active outreach stage
CREATE POLICY followup_read ON leads
FOR SELECT TO agent_followup
USING (stage IN ('new', 'contacted', 'nurture'));
-- And may only modify leads that stay in those stages
CREATE POLICY followup_update ON leads
FOR UPDATE TO agent_followup
USING (stage IN ('new', 'contacted', 'nurture'))
WITH CHECK (stage IN ('new', 'contacted', 'nurture'));
The USING versus WITH CHECK distinction is easy to get wrong. Per the PostgreSQL docs, USING expressions check existing rows, while WITH CHECK expressions check new rows produced by an INSERT or UPDATE. An INSERT or UPDATE that fails the WITH CHECK expression raises an error. Without the WITH CHECK above, an agent could take a visible lead and update it into a stage it shouldn't be able to set, say closed_won, because the old row was visible. With it, the write itself is refused.
Test this the way you'd test any security boundary: connect as the agent role and try to break out.
SET ROLE agent_followup;
SELECT count(*) FROM leads WHERE stage = 'customer'; -- expect 0
UPDATE leads SET stage = 'closed_won' WHERE id = 42; -- expect ERROR
SELECT * FROM invoices; -- expect permission denied
RESET ROLE;
Put these in your CI. A permissions regression should fail a build, not surprise you in production.
Step 3: Make the audit trail something you can query
The second half of the governance claim is reconstruction: what did the agent do, to which data, on behalf of which user, with what outcome. Postgres can't know "on behalf of which user" unless you tell it, so pass the context in explicitly at connection time and record it.
A pattern that works at small-team scale:
CREATE TABLE agent_audit (
id bigserial PRIMARY KEY,
at timestamptz NOT NULL DEFAULT now(),
agent_role text NOT NULL DEFAULT current_user,
acting_for text NOT NULL DEFAULT current_setting('app.acting_for', true),
task_id text DEFAULT current_setting('app.task_id', true),
table_name text,
operation text,
row_id text,
detail jsonb
);
# The orchestrator sets context per task, before the agent runs anything
with conn.cursor() as cur:
cur.execute("SELECT set_config('app.acting_for', %s, false)", (user_email,))
cur.execute("SELECT set_config('app.task_id', %s, false)", (task_id,))
Attach a trigger to the sensitive tables that writes to agent_audit using those values. Now "what did the follow-up agent do for Maria last Tuesday?" is a SELECT. The agent can't edit that table either, because its role was never granted anything on it.
One honest limitation: session settings are only as trustworthy as the process that sets them. The orchestrator, not the agent, must set them, and the agent role must not be able to change them mid-task. If your agent can run arbitrary SQL, treat app.acting_for as advisory and also log at the connection or proxy level.
Step 4: Layer the agent runtime's controls on top
Data-layer controls cover what the agent can do to your database. Your agent also touches files, shells, and the network. For Claude Code specifically, the official docs describe a permission system worth understanding precisely, because third-party blog posts get it wrong.
Per the official documentation, Claude Code evaluates permission rules in the order deny, then ask, then allow, and the first match decides the outcome. Specificity does not change that order, so a narrower allow rule cannot carve an exception out of a broader deny rule. Some blogs claim the most specific rule wins; the docs say otherwise, so follow the docs.
Other points that matter for governance design:
- Managed settings sit at the top of precedence. No other level, including command-line arguments, can override a managed permission rule, and a tool denied at any level cannot be allowed by another. If you're deploying agents to a team, this is where baseline deny rules belong.
- Hooks don't override deny rules. A
PreToolUsehook runs before the permission prompt and can deny a call, force a prompt, or let it proceed. But a matching deny rule still blocks the call even if the hook returns "allow." Use hooks for custom checks (for example, validating a SQL statement against an allowlist), not as a way to grant permissions. - Permissions and sandboxing are complementary. Permissions control which tools and which files or domains the agent can use. Sandboxing gives OS-level enforcement on the filesystem and network access of shell commands. Permissions alone don't stop a shell command from doing something you didn't anticipate; the sandbox does.
- Permission modes differ a lot. The documented modes are
default,acceptEdits,plan,auto,dontAsk, andbypassPermissions. Inautomode a background classifier checks that actions align with the user's request. IndontAskmode every call that would otherwise prompt is auto-denied, which is a good fit for unattended runs where nobody is there to click "approve."
A sketch of a conservative project-level config for an unattended agent:
{
"permissions": {
"deny": [
"Read(./.env*)",
"Read(./secrets/**)",
"Bash(psql:*)",
"Bash(curl:*)"
],
"allow": [
"Read(./src/**)",
"Edit(./src/**)"
]
}
}
Treat that as an illustrative starting point, not a drop-in config. The documentation changes over time, so settings syntax and modes may differ from what's shown here. Check the current version of Configure permissions in the Claude Code docs before you copy anything. One more caveat: these are tool-level controls enforced by the Claude Code runtime, not by your database. Constraining agents at the data layer is my own engineering recommendation, not an official position from the tool's vendor.
Step 5: Scope credentials when agents talk to tools over MCP
If your agent reaches your systems through MCP servers, credentials and token handling become part of governance. Some things from the Model Context Protocol authorization docs:
- The authorization spec is optional. Implementations using an HTTP-based transport should conform to it, while STDIO implementations should instead take credentials from the environment. MCP servers act as OAuth 2.1 resource servers.
- Per the security considerations in the 2026-07-28 revision, MCP servers must validate that access tokens were issued specifically for them as the intended audience, and must not pass through the token received from the MCP client to upstream APIs.
That second rule is the practical one. A token-passthrough server turns "the agent has a token for the calendar tool" into "the agent's token works against everything that server can reach." Audience validation keeps each token useful for exactly one thing.
The spec exists as both a draft and a dated revision, and requirements differ between versions. When you cite a specific MUST or SHOULD in a design doc, name the version: the 2026-07-28 security considerations and the draft authorization page are not interchangeable. For STDIO servers, the credentials come from the environment, which brings you back to Step 1: whatever database credentials sit in that environment are the agent's authority, so make them the small, per-agent role.
Step 6: A permission design checklist for a small team
Here is the order I'd work through for a new autonomous workflow, from most to least important:
- Write the job down as verbs on nouns. "Reads leads, writes outreach_log, updates two lead columns." If you can't list it, you can't scope it.
- Create a dedicated DB role with no superuser, no
BYPASSRLS, and not a table owner. One role per agent. - Grant by table and column, then add RLS with
FORCE ROW LEVEL SECURITYand explicitUSINGandWITH CHECKclauses. Rely on default deny for anything you forgot. - Run attack tests as the agent role in CI: out-of-scope reads, out-of-scope writes, forbidden tables.
- Log at the data layer with an agent identity, an acting-for user, and a task ID, written by the orchestrator, not the agent.
- Set runtime deny rules and a sandbox for the agent's tools, and use
dontAskfor unattended runs so nothing silently escalates. - Scope every MCP token to its audience and refuse passthrough.
- Put irreversible actions behind a human. Sending money, deleting data, and emailing your full customer list get an approval step regardless of how good the agent looks.
None of this requires a new platform. It requires treating the agent like what it is: an untrusted-but-useful contractor who gets a key to one room, not the building.
How I approach this
When I build autonomous Claude workflows, I do the permission design before the prompt design. My approach is to give each agent its own narrowly scoped credentials, let the data layer enforce what it can read and write, and have the orchestrator record who the agent was acting for and what it did, so the audit trail exists from day one instead of being retrofitted after an incident. The prompt still matters for quality; it just isn't carrying the safety load.
If you're about to give an agent real authority over your CRM, billing, or inbox, the highest-leverage hour you can spend is mapping its allowed actions and encoding them as database roles and policies.
Work with BizFlowAI
If you'd rather have this built for you, that's what we do: production AI automation for solo founders and small teams — agents, integrations, and document pipelines that actually ship.
Request a free discovery call — 30 minutes, we map the highest-ROI automation in your workflow. No pitch deck, just engineering.
More guides like this on the BizFlowAI blog.
Frequently asked questions
Why isn't a system prompt enough to govern an AI agent with database access?
A system prompt is an instruction, not a control, because the model can still ignore it. Prompt injection, misreading a task, or calling the wrong tool with the wrong arguments can all bypass it. A real control is enforced outside the model's reasoning, such as a database permission that rejects the request. If the system cannot say no when the agent asks anyway, you do not have governance.
How do I give an AI agent least-privilege access in PostgreSQL?
Create a dedicated role for each agent with NOSUPERUSER and NOBYPASSRLS, and make sure it is not a table owner. Grant only the specific tables and columns the agent's job requires, for example SELECT on leads and UPDATE on just two columns. Anything not granted, such as invoices or payment fields, is refused by Postgres no matter what the agent is prompted to do. Separate roles also let you attribute every action to a specific agent.
How does PostgreSQL row-level security restrict what an AI agent can see and change?
Row-level security lets you attach policies that decide which rows a role can read or modify. Once RLS is enabled on a table, Postgres denies access by default when no policy applies, so a forgotten policy fails closed. A USING clause checks existing rows, while a WITH CHECK clause validates new or updated rows, which stops an agent from moving a record into a state it should not set. Adding FORCE ROW LEVEL SECURITY makes the policies apply even to the table owner.
How do I create an audit trail for what an AI agent did and on whose behalf?
Create an audit table that records the agent role, the user it acted for, a task ID, the table, the operation, and the row affected. Have your orchestrator set per-task context with set_config before the agent runs, then attach triggers on sensitive tables that write those values to the audit table. The agent's role should have no write access to that table, so it cannot alter its own trail. You can then answer questions like what an agent did for a given user on a given day with a simple SELECT.
How should I test that my AI agent's database permissions actually hold?
Treat the permissions like any security boundary and try to break out of them. Connect as the agent role with SET ROLE and run queries that should fail, such as reading rows outside its policy, updating a record into a forbidden stage, or selecting from a table it has no grant on. Expect zero rows, errors, or permission denied. Put these checks in your CI pipeline so a permissions regression fails the build instead of surfacing in production.