← Back to blog
Use CasesSQL11 min read

September 17, 2026

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.

Run Limetry on your own stack

Self-host the open source evaluation server, wire evaluate into your agents, and keep privacy-safe audit under your control.