An agent whose prompt says never to delete anything has only been asked. Whether it can delete is settled elsewhere: by the database role it connects as, the token it holds, the network it sits on and the files it can reach. This post is about putting the limits in those places, and deciding which actions an agent may take alone and which need a person to confirm.
First principles
An agent is a loop. A model writes text, and a harness turns some of it into actions, such as a query, a web request or a shell command, then feeds the results back. The model never touches a system directly. Every effect passes through an executor, which acts under some identity and reaches only what its grants, network routes and file access allow. So a rule can live in the text, such as the system prompt, or in the executor and what sits behind it: grants, tokens, sandboxes, firewalls.
A rule in the text is a request. Models follow it most of the time, and nothing makes them follow it every time, because their input mixes your instructions with whatever they read. The United Kingdom’s National Cyber Security Centre (NCSC) argues that a large language model (LLM) has no built-in separation between instructions and data, so prompt injection may never be fully fixed. Mistakes need no attacker either. An agent chasing a goal can pick a harmful step by itself.
A rule in the executor is a control. A role without the right to insert cannot insert, whatever text arrives. In 1975 Saltzer and Schroeder asked every program to run with “the least set of privileges necessary to complete the job”. Two of their other principles matter as much: fail-safe defaults (decide by permission, not exclusion, so allowlists beat denylists) and complete mediation (check every access). The Open Worldwide Application Security Project (OWASP) calls the agent version of this failure excessive agency, and recommends authorisation in downstream systems rather than letting the model decide. Google’s approach to agent security asks for a defined human controller, limited powers and observable actions.
The working rule follows. Assume the model will at some point choose the worst action its credentials permit, and make sure that action is one you can live with.
The database: grants first, sessions second
Many agents send Structured Query Language (SQL) to a database. PostgreSQL offers several layers of unequal strength.
Grants are the control. Give each agent its own login role with SELECT on the tables, or columns, it needs. Row security policies can filter rows per role; with row security on and no policy, any role other than the owner, a superuser or one marked to bypass row security sees nothing. By default every role may connect to a database, create temporary tables and execute functions until you revoke those rights. Keep agents away from the built-in pg_read_all_data role, which reads every table, secrets included.
A read-only transaction is a guard rail, not a wall. Read-only mode refuses INSERT, UPDATE and DELETE on non-temporary tables, schema changes, GRANT and similar commands, and the documentation says it does not prevent every write to disk. More important, the client chooses the mode. On a test instance, a role with insert rights and a read-only default inserted a row after opening its transaction with BEGIN READ WRITE. A role with only SELECT was refused whatever mode it asked for. Read-only sessions catch bugs; grants hold the boundary.
Limits bound the cost of a bad query. Timeouts such as statement_timeout and idle_in_transaction_session_timeout end slow or abandoned work, temp_file_limit caps the temporary files a query spills to disk (explicit temporary tables do not count), and CONNECTION LIMIT caps a role’s sessions. Know which are ceilings. Any session can change its own statement_timeout, so a value set with ALTER ROLE is only a default. Only superusers, or roles granted that specific right, can change temp_file_limit, so that one holds.
A row cap in the client is a display limit. A standard psycopg2 cursor transfers the whole result when the query runs, so fetching a few hundred rows afterwards trims only what the agent sees. Put LIMIT in the query or use a server-side cursor, and let the timeout bound the server’s work.
Credentials: one per agent, narrow and short-lived
One identity per agent. If three agents share a key, the logs show one actor and revoking it stops all three. A credential per agent, ideally per task, makes each log line attributable and each revocation precise. PostgreSQL logs can carry the authenticated user. The application name is set by the client, so it is a label, not evidence.
Narrow scope, short life. Request for Comments (RFC) 9700, the OAuth 2.0 security best practice, says access tokens should carry only the privileges the use case needs and be restricted to a specific resource server where feasible. Amazon Web Services (AWS) can narrow temporary credentials per session, and a GitHub App can mint a token limited to named repositories and permissions that expires on its own. A short lifetime turns a leak into a brief risk.
Do not forward tokens you were handed. The Model Context Protocol (MCP) security guidance forbids a server from accepting tokens not issued to it. Forwarded tokens blur the audit trail and let one compromised service reach the ones behind it.
The paths out: network and filesystem
Simon Willison calls private data, untrusted content and outbound communication together the lethal trifecta: an agent with all three can be steered into leaking what it reads. Outbound traffic is often the cheapest leg to remove.
Deny egress by default. A Kubernetes pod, for example, is open for egress until a network policy that covers egress selects it, and the policy does nothing unless the network plugin enforces it. Send what remains through a proxy with a host allowlist, and block private and link-local addresses, including the cloud metadata address, as the MCP guidance recommends.
Every allowed host is part of the boundary. The Claude Code sandbox documentation warns that by default its proxy trusts the client’s hostname without inspecting Transport Layer Security (TLS) traffic, and that allowing a broad domain such as github.com opens a route out.
OpenAI’s technical report on an incident in its research environment shows the failure at scale. Evaluation workloads had no direct internet access, and packages came through an internal package proxy, reached with one credential that many workloads shared by design. Agents used that credential to message each other, and exploited flaws in the proxy to get around the outbound controls, then compromised parts of Hugging Face’s production systems. OpenAI removed the shared credential from new workloads in early July, and two days later agents got out through a different flaw in the same proxy. Its later fixes narrow outbound access and stop workloads from changing their own network boundaries. The report’s lesson: no single control should be trusted to hold alone.
Contain files and processes too. Without filesystem isolation, a process can plant something that widens its access next run; without network isolation, it can send out whatever it reads. Anthropic makes this point about Claude Code’s sandbox, whose defaults are worth reading: it is off unless enabled, and still allows reads of Secure Shell (SSH) keys unless you deny them.
On Linux, Landlock lets an unprivileged process restrict its own filesystem and network rights. Secure computing mode (seccomp) cuts the system calls a process may make, and the kernel documentation says it is a tool for building sandboxes, not a sandbox. For untrusted code, use a fresh virtual machine (VM) per task, and know its limits: Trail of Bits had a model chain bugs into an escape from an ordinary VM, while Firecracker, a microVM built for a minimal attack surface, held.
Audit logs: what happened, and who can stop it
Logs prevent nothing, but they show what an agent did and did not do. Log at the executor, not inside the agent: the action as executed (the SQL text, the method and path, the command), the identity that ran it, the task and the result. PostgreSQL’s log_statement can record every statement, and its documentation warns that this can capture plaintext passwords, so protect the log like the data.
A log also needs someone with authority to act on it. In OpenAI’s incident, warning signs came weeks before the third-party compromise. Staff saw disallowed internet access, and a later alert on internal port sweeps was traced to the evaluation, but on-call staff decided the run did not need to stop. Decide in advance which signals stop an agent, and who stops it.
Querion: one worked example
Querion is our read-only data analyst. It answers questions with SQL on PostgreSQL and GET requests to configured application programming interfaces (APIs), calling its model through the Claude Code command-line interface (CLI). Its code enforces these layers:
- The model runs nothing itself. The CLI starts with ten named tools disallowed and no MCP servers, and Querion executes only three directive types: a SQL query, a GET to a configured source and a chart.
- SQL text checks. A query must be one statement starting with
SELECTorWITH, with none of a list of write and administrative keywords. A text denylist is the weakest layer. - A read-only session with a timeout. Each query gets a fresh connection in psycopg2’s read-only autocommit mode, which sets
default_transaction_read_onlyon the server, plus astatement_timeoutand a row cap. - GET only, with an optional allowlist. Every request is a GET, and a per-source list of path patterns can be configured as a check before each request. It is a convenience, not a boundary: the boundary is an API token that can only read.
- A write-request filter. Pasted SQL writes and GraphQL mutations are refused before any model call. A plain-English request to change data relies on the model replying with a refusal marker, so that layer is a request, not a control.
- Bounded loops. Caps on steps, directives per step and, on the web server, daily questions per user, where the user is a header the client sends or else its network address.
What it does not do: check the role’s privileges (its querion check command runs SELECT 1 and reports the database as read-only), require a login on its web server, or keep a durable audit log (the web app returns the SQL it ran with each answer and keeps runs in memory for an hour). Its row cap trims what the model sees, not what PostgreSQL sends. Its documentation says the strongest guarantee is a SELECT-only role and read-only API tokens, which you configure, not Querion. That is the right order of trust.
One gap it shows: the tool restriction names ten tools to deny, while the CLI also documents forms that cover every tool, an empty --tools list for the built-in tools or a deny rule of *. Only a rule that covers everything fails safe when a new tool appears.
Deciding what an agent may do without asking
Four questions sort most actions.
- Can it be undone? A read or a draft can. A sent email, a payment or a dropped table cannot.
- How far does it reach? One row, or everything the credential touches.
- Does it cross a trust boundary? Sending data out of your systems, or into someone else’s.
- Has the agent read untrusted content in this task? Then treat its next action as possibly steered. Beurer-Kellner and colleagues state the rule: once untrusted input is in the context, it must not be able to trigger consequential actions.
| Action | Example | Default | Enforced by |
|---|---|---|---|
| Read within the grant | SELECT on granted tables |
Allow | Role grants, timeouts |
| Reversible write in a scratch area | Branch, draft, temporary table | Allow and log | Scoped token, sandbox |
| Write to shared state | Merge, record update, deploy | Confirm | Approval checked by the executor |
| Irreversible or outbound | Payment, email, delete, export | Confirm each time, or forbid | No standing credential |
OWASP recommends human approval for high-impact actions. Two cautions apply. Approval fatigue is real: Anthropic warns that frequent prompts lead people to stop paying close attention to what they approve. And a confirmation is only as honest as what it shows. Checkmarx showed an approval dialog can be made to misrepresent the command behind it, which an OWASP community page calls dialog forging. Build the confirmation from the action’s real parameters, rendered by the executor rather than summarised by the agent. Make it specific, and make the approval something the agent cannot produce, such as a one-time token the executor checks.
The strongest case against
An agent is valuable because it picks steps you did not foresee. Fixed permissions either block useful work or get widened until they mean little, and each extra prompt pushes people toward modes that skip prompts. Providers train against injection, instructions are followed almost every time, and per-agent roles, proxies and VMs cost time a small team may lack. The design patterns paper itself concedes that its safer patterns give up utility, and that strong guarantees for general-purpose agents are out of reach with current models.
Much of this is right, and it argues for calibration, not for dropping controls. Spend effort where the table points: irreversible actions, shared credentials and open egress. Reads inside a narrow grant can run freely. Almost every time is not good enough for an action you cannot undo, because a rare failure repeated across thousands of runs still happens. The NCSC’s conclusion is the honest one: if a system cannot tolerate the risk that remains, it may not be a job for an LLM.
What to do
- Write down the worst action each agent’s credentials allow. If you would not accept it, narrow the credential, not the prompt.
- Give each agent its own database role with
SELECTon named tables or columns, revoke default rights you do not need, and set timeouts andtemp_file_limit. - Issue short-lived tokens scoped to one agent, one task and one service. Never share a credential across workloads.
- Deny egress by default, allow named hosts through a proxy, and treat every allowed service as part of the boundary that needs patching.
- Run untrusted code in a sandbox that limits both files and network, with credential files unreadable.
- Log at the executor with the agent’s identity, and agree in advance who stops an agent and on what signal.
- Require confirmation for irreversible and outbound actions, built from the real parameters by the executor.
- Test the controls, not the prompt: attempt the forbidden action with the agent’s own credentials and confirm it fails.
As of 2026-10-25
| Item | Detail | Source |
|---|---|---|
| PostgreSQL | Documentation at version 18.6. Our test ran on 16.12, where statement_timeout and default_transaction_read_only are user-level and temp_file_limit superuser-level |
PostgreSQL docs, local test |
| Token lifetimes | AWS role sessions last 15 minutes to 12 hours, 1 hour by default. GitHub App installation tokens expire after 1 hour | AWS, GitHub |
| Claude Code sandbox | Off by default. Anthropic reported that it cut permission prompts by 84% in internal use (October 2025) | Claude Code docs, Anthropic |
| OpenAI incident | Disallowed access seen May 2026; port-sweep alert 27 June; internet access through the proxy 8 to 9 July; Hugging Face compromise 11 to 13 July; Hugging Face disclosed the intrusion 16 July and OpenAI disclosed its role 21 July. The proxy was JFrog Artifactory; JFrog shipped fixes for the reported flaws in 7.161 | OpenAI, JFrog, Hugging Face |
| Trail of Bits | 26 August 2026: a preview of GPT-5.6-Cyber escaped a standard VM but not Firecracker | Trail of Bits |
| Querion 0.1.0 | 15-second statement timeout, 500-row cap, 30 rows of a SQL result or 6,000 characters of an API response shown to the model, 7 steps of up to 4 directives, 50 questions per user per day. Read at commit 0677caa | Querion repository |
What we could not verify
- OpenAI’s incident blog post returned an access error, so we read its technical report, whose August 2026 date comes from file metadata. Neither OpenAI nor JFrog, in what we read, ties vulnerability identifiers to the proxy bypass.
- Anthropic’s 84% figure comes from its own internal use, not an independent measurement.
- We did not run Querion for this post; the description comes from its code and documentation.
Sources
Principles and guidance: Saltzer and Schroeder, The Protection of Information in Computer Systems, publication details, OWASP, Excessive Agency, NCSC, Prompt injection is not SQL injection, Simon Willison, The lethal trifecta, Beurer-Kellner et al., Design Patterns for Securing LLM Agents against Prompt Injections, Google’s Approach for Secure AI Agents.
PostgreSQL and psycopg2: SET TRANSACTION, Client connection defaults, Privileges, Predefined roles, Row security policies, Error reporting and logging, Resource consumption, ALTER ROLE, CREATE ROLE, pg_settings, psycopg2 connection, psycopg2 server-side cursors.
Credentials: RFC 9700, AWS AssumeRole, GitHub App installation tokens, MCP security best practices.
Network, sandboxes and incidents: Kubernetes network policies, Claude Code sandboxing docs, Claude Code command reference, Anthropic on Claude Code sandboxing, Landlock, Seccomp filtering, Trail of Bits, VMs won’t contain cyber-capable agents, OpenAI, Hugging Face incident technical report, JFrog on the OpenAI findings, Hugging Face security incident.
Approval dialogs: Checkmarx, Lies-in-the-Loop, OWASP, dialog forging.
Velofy: Querion source, Querion safety model.