Recipe
Claude Code, read-only, on named tables
The smallest useful policy. Claude Code gets SELECT on the tables you name, every write is refused with a reason the model can act on, and every statement is recorded against this credential rather than a shared role.
- Client
- Claude Code
- Policy shape
- read-only with a table allowlist
A coding agent needs to read production to answer questions, and must not be able to write or to wander into tables nobody cleared.
This is the baseline every other recipe on this page starts from. Read-only is the access mode, so the statement kind is checked before anything else, and the allowlist names the relations the credential may touch. Two rules, both enforced in the Postgres wire protocol, so they hold for the MCP endpoint and for a connection string alike.
Wire it up
Three commands and one file. The credential carries the policy, so nothing in the client config expresses a rule and nothing in the client can weaken one.
Create the policy
Read-only is the access mode and it is a ceiling: the statement allowlist cannot lift it, so no later edit to this policy can turn an UPDATE into an allowed statement without changing the mode itself.
Terminalpgbeam policies create \ --name claude-code-ro \ --mode read_only \ --allow public.orders \ --allow public.order_items \ --allow public.products \ --statement-timeout-ms 8000Issue a credential for this agent
One credential per agent rather than one shared credential, because that is what makes the audit trail able to name which agent ran a statement. The --client flag prints the config for you.
Terminalpgbeam agents create --name "Claude Code" --policy pol_xxx --client claude --expires 30dAdd the server to your project
Claude Code speaks HTTP to the endpoint directly, with no bridge process. Add it with the CLI, or write the file yourself.
.mcp.json{ "mcpServers": { "pgbeam": { "type": "http", "url": "https://<project>.proxy.pgbeam.app/mcp", "headers": { "Authorization": "Bearer pba_..." } } } }Check the refusals before the agent finds them
dry-eval runs a statement through the data plane's own policy engine and prints the verdict without touching your database.
Terminalpgbeam policies dry-eval --policy pol_xxx --sql "UPDATE public.orders SET status = 'paid'" pgbeam policies dry-eval --policy pol_xxx --sql "SELECT * FROM public.customers"
Ask the agent to run the briefing tool first. It returns the policy in force, so the model plans against the rules instead of discovering them one refusal at a time.
What the agent sees
Four statements against this policy, with the verdict and the rule the engine emits for each.
1 SELECT id, total FROM public.orders WHERE created_at > now() - interval '7 days'
2 UPDATE public.orders SET status = 'paid' WHERE id = 42
3 SELECT * FROM public.customers
4 SELECT id FROM public.orders FOR UPDATE1 Verdict: allow Rule: ok
2 Verdict: block Rule: read_only
Reason: this credential is read-only; UPDATE is not permitted
Hint: only SELECT, SHOW, and EXPLAIN are allowed unless the statement kind is explicitly allowlisted
3 Verdict: block Rule: table_not_allowed
Reason: table "public.customers" is not in this credential's allowlist
4 Verdict: block Rule: locking_select
Reason: SELECT ... FOR UPDATE/SHARE takes row locks and is blocked in read-only modeLine 2 is the refusal people expect, and it comes back to the agent as SQLSTATE 42501 with the reason text attached, so the next turn is a corrected query rather than a retry loop.
Line 4 is the one worth knowing about. It is a SELECT, so a control that classifies statements by keyword lets it through, and it takes row locks that a stuck agent session then holds.
Line 3 is a read of a table nobody allowlisted. Neither the client config nor the model has any say in this: the relation is checked against the credential's policy on the way in.
What this does not cover
- The allowlist is a list, so a table added by tomorrow's migration is outside it until somebody adds it. That fails closed, which means a legitimate new table breaks the agent until the policy is updated.
- Read-only bounds what the agent can change, not how much it can read. A SELECT with no LIMIT against an allowed table is permitted; add --max-rows and a budget if that matters, which is the recipe below this one.
- Nothing here masks a column. If an allowed table has a column the agent should not see, allowlisting the table exposes it, and masking is the rule that covers that case.
Questions
Safe Postgres access for your agents
Start with a 14-day free trial. No credit card required.
Get Started