Deny sql.write before the database sees a mass DELETE
Cursor MCP pointed at production. Claude ran:
DELETE FROM users WHERE email LIKE '%+test%'
The predicate matched 1,847 production accounts. Point-in-time recovery was not configured. Prompt instructions were advisory. The driver still ran the statement.
Origin: Gate SQL writes so agents never hold DATABASE_URL.
Fix credential ownership
Once the agent holds DATABASE_URL, every session is a write session. Blocking every query kills the workflow. Giving write credentials repeats the failure. Prefer: gate owns the URL; classify; evaluate; execute only on allow.
Hold DATABASE_URL in the gate
@limetry/sql moves DATABASE_URL into the gate process. The agent gets limetry_sql_query. Unknown classification maps to sql.write on purpose.
import { SqlActionGate } from "@limetry/sql"
const gate = new SqlActionGate({
connectionString: process.env.DATABASE_URL!,
apiKey: process.env.LIMETRY_API_KEY!,
baseUrl: process.env.LIMETRY_BASE_URL,
policyId: process.env.LIMETRY_SQL_POLICY_ID!,
agentId: "sql_mcp",
resourceLabel: "postgres://production",
dryRun: false,
})
const result = await gate.evaluateAndMaybeExecute({
sql: "DELETE FROM users WHERE email LIKE '%+test%'",
execute: true,
})
process.stdout.write(JSON.stringify({
sql_class: result.sqlClass,
action_type: result.intent.action_type,
decision: result.evaluation.decision,
executed: result.executed,
reasons: result.evaluation.reasons ?? [],
}, null, 2))
Shipped policies/postgres.json is read-only: allow sql.read, deny sql.write and sql.ddl. For cleanup that must run later, use an approval-gated write policy — not a prompt, and not a raw URL in MCP config. See Adding write access without giving away the keys.
Record deny before execution
Against the read-only policy:
{
"intent": {
"agent_id": "sql_mcp",
"action_type": "sql.write",
"resource": "postgres://production",
"metadata": {
"sql_class": "write",
"sql_preview": "DELETE FROM users WHERE email LIKE '%+test%'",
"dry_run": "false"
}
},
"decision": "deny",
"executed": false,
"reasons": [
"action_type sql.write is denied"
]
}
The database never sees the statement. Audit keeps decision shape and a truncated sql_preview (200 characters). Do not expect a full WHERE clause at rest.
Outcome with the gate
- 1,847 account deletes avoided.
- 0 database writes executed.
- 1 audit row with
action_type=sql.write,decision=deny, truncated preview. - Recovery work avoided.
Over-gating unusual SQL as unknown → sql.write is acceptable. Next step for legitimate cleanup is a write-capable policy with a human on the WHERE clause — not another paste of DATABASE_URL into MCP config.
Write path: Adding write access without giving away the keys.
