Recipe

Claude Desktop, scoped to the tables you name

Claude Desktop reaches a remote endpoint through a bridge process, which is a config quirk rather than a security one. The rules still live on the credential.

Client
Claude Desktop
Policy shape
an allowlist plus a denylist

Somebody wants to ask questions of the database from the desktop app, and the database has schemas in it that nobody outside the team should be reading.

Allowlist and denylist are not two ways of saying the same thing. An allowlist is a closed set, so anything new is outside it until somebody adds it. A denylist is an open set with holes in it, so anything new is inside it. Use the allowlist as the rule and the denylist for the handful of relations you want refused with an unambiguous reason even if a future edit widens the allowlist by mistake.

Wire it up

One policy, one credential, and one merged block in a file whose path depends on the operating system.

  1. Create the policy

    The allowlist is the rule. The denylist is belt and braces on the relations whose exposure would be the worst outcome.

    Terminal
    pgbeam policies create \
      --name desktop-analyst \
      --mode read_only \
      --allow public.orders --allow public.order_items --allow public.products \
      --deny internal.payouts --deny internal.api_keys \
      --max-rows 500
  2. Issue the credential and print the config

    The claude-desktop client value emits the bridge form, including the environment variable indirection that keeps the header intact on Windows.

    Terminal
    pgbeam agents create --name "Claude Desktop" --policy pol_xxx --client claude-desktop --expires 30d
  3. Merge it into the desktop config

    The header argument has no space after the colon, and the token comes from env, both on purpose: Claude Desktop on Windows does not escape spaces inside args, which mangles the header otherwise. Restart the app afterwards.

    claude_desktop_config.json
    {
      "mcpServers": {
        "pgbeam": {
          "command": "npx",
          "args": [
            "-y",
            "mcp-remote",
            "https://<project>.proxy.pgbeam.app/mcp",
            "--header",
            "Authorization:${PGBEAM_AUTH_HEADER}"
          ],
          "env": { "PGBEAM_AUTH_HEADER": "Bearer pba_..." }
        }
      }
    }
  4. Check both lists fire

    A denied relation and an un-allowlisted one are refused for different reasons, and the reason is what tells you which list you actually configured.

    Terminal
    pgbeam policies dry-eval --policy pol_xxx --sql "SELECT * FROM internal.payouts"
    pgbeam policies dry-eval --policy pol_xxx --sql "SELECT * FROM public.customers"

Three relations, three answers

The difference between the two rules is visible in the rule name, which is worth knowing when you are debugging a policy rather than writing one.

Statements
1  SELECT id, total FROM public.orders LIMIT 10
2  SELECT * FROM internal.payouts
3  SELECT * FROM public.customers
pgbeam policies dry-eval
1  Verdict: allow    Rule: ok
2  Verdict: block    Rule: table_denied
3  Verdict: block    Rule: table_not_allowed
   Reason:  table "public.customers" is not in this credential's allowlist

Line 2 and line 3 both refuse, and the rule names differ. table_denied means somebody named that relation as forbidden; table_not_allowed means nobody named it at all. The second is the one that will fire on a table created next month.

The denylist takes precedence, so a relation that appears in both is refused. That ordering is what makes the denylist useful as a backstop against a careless edit to the allowlist.

Line 1 is the whole point of the recipe. The desktop app is a normal MCP client here, and the bridge process it needs has no say in any of this.

What this does not cover

  • Claude Desktop cannot reach a remote endpoint directly, so this recipe runs a bridge process on the machine. That process holds the bearer token in its environment, and anyone with the machine has it.
  • The config file is machine-wide and holds every MCP server the user has, so merge the entry rather than replacing the file.
  • An allowlist names relations. It does not stop a permitted relation from returning a column somebody considers sensitive, which is what masking is for.

Questions

Safe Postgres access for your agents

Start with a 14-day free trial. No credit card required.

Get Started