Agents

Give the agent the proxy URL

tablebelt init prints a DATABASE_URL whose user and password exist only on the proxy. That string is the only database URL the agent should have. The upstream DSN stays on the machine that runs tablebelt run.

Claude Code

Put the proxy URL in the project's .mcp.json for the Postgres MCP server.

.mcp.json
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres", "postgres://agent:PASSWORD@127.0.0.1:5439/prod"],
      "env": {
        "DATABASE_URL": "postgres://agent:PASSWORD@127.0.0.1:5439/prod"
      }
    }
  }
}

Block two things the agent can otherwise do on the same machine: reading .env, and running tablebelt approve. This is a PreToolUse command hook. It reads JSON on stdin and, when it denies, prints hookSpecificOutput.permissionDecision of deny.

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Read",
        "hooks": [
          {
            "type": "command",
            "command": ""$CLAUDE_PROJECT_DIR"/.claude/hooks/block-tablebelt.sh"
          }
        ]
      }
    ]
  }
}
.claude/hooks/block-tablebelt.sh
#!/bin/sh
# PreToolUse hook. Docs: https://code.claude.com/docs/en/hooks
input=$(cat)
tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')

deny() {
  jq -n --arg reason "$1" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: $reason
    }
  }'
  exit 0
}

case $file in
  *.env|*/.env|*.env.*)
    deny "Reading .env is blocked. Use the Tablebelt DATABASE_URL, not the upstream DSN."
    ;;
esac

case $cmd in
  *"tablebelt approve"*)
    deny "tablebelt approve is blocked in the agent. Approve from your own terminal."
    ;;
esac

exit 0

chmod 755 .claude/hooks/block-tablebelt.sh. The hook needs jq.

Codex

Codex reads MCP servers from ~/.codex/config.toml. Use the proxy URL in the server arguments and in DATABASE_URL.

~/.codex/config.toml
[mcp_servers.postgres]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-postgres", "postgres://agent:PASSWORD@127.0.0.1:5439/prod"]

[mcp_servers.postgres.env]
DATABASE_URL = "postgres://agent:PASSWORD@127.0.0.1:5439/prod"

Cursor

Cursor reads .cursor/mcp.json in the project.

.cursor/mcp.json
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres", "postgres://agent:PASSWORD@127.0.0.1:5439/prod"],
      "env": {
        "DATABASE_URL": "postgres://agent:PASSWORD@127.0.0.1:5439/prod"
      }
    }
  }
}

MCP

Any MCP client that starts a Postgres server should receive the proxy URL as DATABASE_URL. Do not put the upstream DSN in the MCP env.

MCP server env
{
  "mcpServers": {
    "postgres": {
      "command": "your-mcp-server",
      "env": {
        "DATABASE_URL": "postgres://agent:PASSWORD@127.0.0.1:5439/prod"
      }
    }
  }
}

Other

psql, an ORM, or any other Postgres client uses the same URL. Your application keeps the real connection string and does not go through Tablebelt.

DATABASE_URL
DATABASE_URL=postgres://agent:PASSWORD@127.0.0.1:5439/prod

Same-user approval risk and the raw URL bypass are on Security.