Security
The agent gets a proxy login
The proxy terminates authentication. The agent logs in with a username and password from tablebelt.yaml. The proxy logs in to Postgres with the upstream DSN, which only the proxy host has. Stealing the agent's URL gives access only through Tablebelt.
Three boundaries: your host (proxy, config, admin token, upstream DSN), the outbound HTTPS link to the control plane, and the control plane. Free installs have no second boundary. They send nothing.
The honest limit
An agent that can read a raw DATABASE_URL from .env can walk around any proxy. Give it only the Tablebelt string, keep the real URL off that machine, and use a role it can only reach through Tablebelt. Block .env reads with hooks if the agent supports them. The Claude Code hook is on Agents.
An agent running as the same OS user, with a TTY, could also run tablebelt approve. Approval asks for a TTY confirmation, and the admin token file is mode 0600. Run the proxy as a different OS user or in Docker, set cloud.local_approvals: false on Pro or Team, or add a Claude Code hook that blocks the command. A determined agent on that same user can still get a pty. That residual risk stays. Non-interactive approval also requires --yes and TABLEBELT_ALLOW_NONINTERACTIVE_APPROVE=1.
Own tablebelt.yaml as another user, or mount it read-only in Docker. The proxy reads it at start. A same-user agent that can edit the file can relax local rules. It still cannot override unparsed SQL or the protected tablebelt schema.
What leaves the box
On Pro and Team the proxy POSTs a hold intake and polls for a decision. The field list is the whole payload. Parameter values, the DSN, row data, and credentials are not fields in it.
| Field | Required | Meaning |
|---|---|---|
local_id | yes | Local hold id, hold_ plus at least four digits. |
held_at | yes | When the proxy held the statement. |
expires_at | yes | held_at plus hold.pending_ttl (default 24h). |
category | yes | Hold category, for example drop or unscoped_delete. |
kinds | yes | Statement kinds from the classifier. |
redaction | yes | full, normalized, or none. |
fingerprint | yes | 16 hex characters from the parser fingerprint. |
tables | yes | Schema, name, estimated rows, bytes, and whether the table exists. |
client | yes | application (startup application_name) and user (the proxy login, not the upstream role). |
snapshot_plan | yes | will_snapshot, and when relevant mode, bytes, and reason. |
rule_id | no | Built-in or custom rule that matched. |
statement | no | Text, or null when redaction is none. Truncated at 64 KiB. |
statement_count | no | How many statements were in the message. Default 1. |
est_rows | no | Estimate across the plan. |
cascade | no | Objects a CASCADE would also drop. |
in_transaction | no | Whether the client session is inside a transaction. |
Redaction
| Mode | What the statement field contains |
|---|---|
full (default) | The statement as written, truncated at 64 KiB. A literal you typed, such as UPDATE users SET email = 'a@b.c' with no WHERE, leaves the box inside that text. |
normalized | Constants replaced with $n placeholders. |
none | The statement is null. Approvers see the fingerprint, kinds, and table names. |
Threats
| Threat | What holds it | What remains |
|---|---|---|
| Agent connects with the real DSN | Proxy-terminated auth. Keep the DSN off the agent host. pg_hba so the role is reachable only from the proxy. Hooks that block .env reads. | Documented above. A raw URL still bypasses the proxy. |
Agent runs tablebelt approve | TTY confirmation. Admin token mode 0600. Separate OS user or Docker. cloud.local_approvals: false. Claude Code hook. | Same OS user with a pty can still approve. |
| Obfuscated SQL | Real parser, full tree walk, constant-true WHERE treated as unscoped, DO blocks and non-SQL functions held, COPY PROGRAM and FunctionCall denied. | Existing functions and triggers. See Rules. |
| Agent drops the snapshot store | Schema tablebelt is deny, not overridable. Prefer a separate admin role with no grant to the agent. | A superuser agent role relies on the classifier alone. |
| Control plane is compromised | It holds no credentials and no rows. Snapshots still run unless the approver chose otherwise. Use redact if you do not want statement text stored. | A hosted approve can still approve a pending Pro or Team hold. |
| Stolen API key | Scoped to one org's proxy endpoints. Revoke it. last_used_at is visible. It cannot read another proxy's holds. | A revoked key falls back to local approvals. |
Signing-secret checks cover Slack, Stripe, and webhooks. Webhook deliveries refuse private addresses. Supply-chain signing of release binaries with cosign is not in this version. Check checksums.txt on install. See Install.