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.

  1. 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.

    Terminal
    pgbeam policies create \
      --name claude-code-ro \
      --mode read_only \
      --allow public.orders \
      --allow public.order_items \
      --allow public.products \
      --statement-timeout-ms 8000
  2. Issue 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.

    Terminal
    pgbeam agents create --name "Claude Code" --policy pol_xxx --client claude --expires 30d
  3. Add 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_..." }
        }
      }
    }
  4. 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.

    Terminal
    pgbeam 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.

Statements
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 UPDATE
pgbeam policies dry-eval
1  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 mode

Line 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