---
title: "Read-only Enforcement"
description: "Block every write and DDL statement for an agent credential. Reads pass, writes are rejected in the wire protocol before they reach your database."
canonical: "https://pgbeam.com/docs/read-only"
last-updated: "2026-10-29T19:01:20.000Z"
---

# Read-only Enforcement

> Block every write and DDL statement for an agent credential. Reads pass, writes are rejected in the wire protocol before they reach your database.

URL: https://pgbeam.com/docs/read-only

Read-only is the safest access mode for an agent. With it, PgBeam allows reads and rejects every `INSERT`, `UPDATE`, `DELETE`, and DDL statement before it reaches your database. The agent receives an LLM-readable reason, so it can adjust instead of failing blind.

## Turn it on

```bash title="Read-only policy"
pgbeam policies create --name read-only --mode read_only
# → Policy profile created: pol_1a2b3c…
pgbeam agents create --name analytics-bot --policy pol_1a2b3c…
```

A credential's policy is fixed at creation. To move an existing credential to a different policy, rotate or re-issue it against the new profile.

In the dashboard, set **Access mode** to **Read-only** on the policy profile.

## What is allowed and blocked

| Statement                                | Read-only |
| ---------------------------------------- | --------- |
| `SELECT`                                 | Allowed   |
| `WITH … SELECT` (read-only CTE)          | Allowed   |
| `EXPLAIN` of a read                      | Allowed   |
| `INSERT` / `UPDATE` / `DELETE`           | Blocked   |
| `CREATE` / `ALTER` / `DROP` / `TRUNCATE` | Blocked   |
| `COPY`                                   | Blocked   |
| Data-modifying CTE (`WITH … UPDATE`)     | Blocked   |
| `SELECT … INTO t` (creates a table)      | Blocked   |
| `EXPLAIN ANALYZE` (runs the statement)   | Blocked   |

`SELECT … INTO t` and `CREATE TABLE t AS SELECT …` are the same statement in Postgres, so both are blocked. `EXPLAIN ANALYZE` runs the statement it is given rather than only planning it, so it is blocked whenever the statement underneath it writes or creates a table. A plain `EXPLAIN` of a read executes nothing and stays allowed.

## What the agent sees

A blocked write comes back as a Postgres error (SQLSTATE `42501`):

```text
ERROR:  blocked by PgBeam agent policy: this credential is read-only; update is not permitted; only SELECT, SHOW, and EXPLAIN are allowed
```

The error is written to be read by an LLM, so an agent can correct its plan and retry within the rules.

## Fails closed

A multi-statement batch is rejected if any statement in it would be blocked. Unparseable SQL and unknown statement types are rejected too. This prevents an agent from slipping a write past the parser.

> **Need writes sometimes?**
>
> Read-only is the right default. When an agent genuinely needs to write,
> read-write mode with a tight [allowlist](/docs/allowlists) is one option.
> [Dry-run writes](/docs/dry-run-writes) run each write for real and then roll
> it back, and [approvals](/docs/approvals) hold a production write until a
> human signs off.

## Related

* [Policies](/docs/policies)
* [Allowlists](/docs/allowlists)
* [Audit log](/docs/audit-log)