All posts
Field notesSep 23, 20267 min read

Session GUCs aren’t a security boundary for AI agents

Role-level statement_timeout and default_transaction_read_only are user-context GUCs — any session can SET them off or bypass them with PGOPTIONS. For AI agents on Postgres, real boundaries live in pg_hba, grants, role attributes, watchdogs, and replicas — not in rolconfig.

VS

Venkat Sakamuri

Founder, DeepSQL · Ex Oracle Core Query Engine · CMU CS · YC

Every runbook I see for “give the AI agent a database login” still opens with the same two lines:

ALTER ROLE agent SET statement_timeout = '2s';
ALTER ROLE agent SET default_transaction_read_only = on;

They work. They also don’t mean what most teams think they mean.

I spent years on a query engine team, then years watching production Postgres and MySQL shops bolt agents onto clusters. The pattern keeps repeating: we put a default on the role, call it a security control, and ship. Then someone reads pg_settings.context, or worse, an MCP CVE drops, and we rediscover that session-local parameters are not a boundary.

Christophe Pettus wrote this up cleanly in The Call Is Coming From Inside the Session (thebuild.com, 17 Sep 2026). I’m not restating his post. I’m filing what it means for anyone wiring coding agents — or DeepSQL — to a live database.

What ALTER ROLE … SET actually buys you

What those lines write is the value a session starts with. Whether it stays there is the session’s business.

statement_timeout, lock_timeout, default_transaction_read_only, idle_in_transaction_session_timeout, and friends are context = user in pg_settings. The docs are blunt: any user is allowed to change their session-local value.

So this is enough:

SET default_transaction_read_only = off;

One statement. Done. Before the change, pg_settings.source said user (role config). After, it says session. Almost nobody monitors that column.

The quieter bypass doesn’t even leave a SET in your logs. libpq options / PGOPTIONS applies client switches after per-role settings load:

PGOPTIONS="-c statement_timeout=0" psql "postgresql://agent@localhost:5432/app"

Source becomes client. Cap gone before the first query. Grep for SET statement_timeout and you’ll find nothing, because nothing was set.

That’s not a Postgres bug. That’s the design of user-context GUCs.

The revoke that accepts and does nothing

I’ve seen hardening guides try:

REVOKE SET ON PARAMETER statement_timeout FROM agent;

PostgreSQL 15’s GRANT SET ON PARAMETER exists to hand a non-superuser a parameter it couldn’t otherwise touch — superuser / superuser-backend context. Point it at a user-context parameter and there’s nothing to revoke. Every role already has it. The command is accepted. That’s the entire extent of what it does.

If your checklist treats a successful REVOKE as “agent can’t raise timeouts,” your checklist is lying to you.

What actually binds

Everything that holds has one property: it’s evaluated somewhere the session cannot reach — before the session exists, or from a different session entirely.

pg_hba.conf — earliest gate. Which role, which database, which address, before SQL is parsed. Agents that shouldn’t touch billing from outside the VPC get refused here, not in a GUC.

Object grants — answered on every statement. CONNECT, USAGE on the schemas needed, SELECT on the specific relations needed. Not pg_read_all_data, which reaches every table anyone creates later forever. Check inherited membership properly (pg_has_role); \du dropped member-of in PG 16 and two hops of group membership can still give you COPY … TO PROGRAM.

Role attributes — read at authentication. CONNECTION LIMIT, NOLOGIN. Blunt. That’s why they work. The agent can’t SET them.

A privileged watchdog — second connection reading pg_stat_activity, filtering on usename (auth-time), not application_name (user context — one SET and the agent drops out of your filter). Prefer pg_terminate_backend(pid, wait_ms) over cancel-and-hope. Take NOLOGIN first or the driver reconnects while you read the log line.

Replica for read-only agents — highest-value move on the list. On a hot standby, read-only is enforced by recovery, below anything a parameter can reach. SET default_transaction_read_only = off changes nothing. Connection-string options change nothing. For a read-only agent this retires most of the GUC theater.

The same mistake one layer up

Two MCP advisories this month made the identical architectural point. CVE-2026-87911 (awslabs.postgres-mcp-server) and CVE-2026-85620 (Postgres MCP Pro) weren’t Postgres bugs. In both cases a parser in front of the database decided what a statement was allowed to do — and had to agree with the server’s parser about arbitrary SQL, forever. It won’t.

AWS’s advisory said the quiet part out loud: the strongest control is a role that only has the privileges it needs, so the database itself enforces the boundary regardless of what SQL arrives. Minimal-privilege users were never affected.

Then some workaround lists still say “force read-only at the role level.” File that correctly: it’s a default that stops accidents. It is not a control that stops an attacker or a determined agent client.

So set the timeouts anyway

I’m not telling you to delete those ALTER ROLE lines.

A statement_timeout that kills a runaway analytical query at two seconds is doing useful work every day. Most of what an agent does wrong is an accident, not an attack. Defaults that catch accidents earn their place.

Just stop calling them security.

Security is pg_hba, the grant list, role attributes, the watchdog, and the replica. Ergonomics is rolconfig. If you’re auditing, log the value the session was actually holding when the statement ran (pg_settings), not the value in rolconfig. The role’s copy stopped being evidence the moment the session connected.

What this means for DeepSQL

DeepSQL is a self-hosted AI DBA agent for Postgres and MySQL — web UI, CLI, Slack, MCP. We sit in front of your databases with Schema Guardian, confirmed writes, role-aware digests, and a context layer. We do not pretend that sticking default_transaction_read_only on a role makes an agent safe.

How we think about the boundary:

  1. Least privilege on the DB role DeepSQL (or any agent) uses. Grants and HBA first. Timeouts as defaults, not fences.
  2. Policy and confirmation in the product, not “the model pinky-swore.” Confirmed DDL/DML for DBA. Hard blocks on DROP / TRUNCATE. Schema Guardian classifies risk before LLM narration softens the story.
  3. MCP talks to our gates, not past them. Coding agents should not get raw superuser DSNs “because the role has a timeout.” Credentials are dumb. Policies and confirmation are not.
  4. Honest gaps. We are not your HA runbook and not your PITR console. We won’t claim session GUCs as a security model either.

If your current agent stack is “role with read-only GUC + MCP in front of the primary,” Pettus’s post is your incident report written early. Move the boundary to where the session can’t reach it. Keep the timeouts for the accidents.

We’re building the context layer and the gates at deepsql.ai / github.com/DeepSQLAI/deepsql. If you’ve already been burned by a SET that undid your “hardening,” I’d like the blunt version — what failed, what you changed.

Credit where it’s due: Christophe Pettus, The Call Is Coming From Inside the Session — read the original; this is the field note for teams shipping agents.


Venkat Sakamuri · Founder, DeepSQL · Ex Oracle Core Query Engine · Peer DBA/eng building a self-hosted AI DBA for Postgres & MySQL · venkat@deepsql.ai