Skip to main content
When Midplane denies a query, the MCP tool result and the DECIDED audit row both carry a policy_rule naming the rule that denied, plus a human-readable reason. This page maps each rule to the cause and the fix. The engine evaluates four rules in order, and the first rule that denies wins. The order is parse_errormulti_statementtable_accessdangerous_statement. Two are always-on guards (parse_error, multi_statement); the rest are driven by your policytable_access and the dangerous_statement guardrails default on. See the policy engine.

table_access

A query referenced a table at a permission level its policy doesn’t grant. A query is denied if any referenced table fails — including writes hidden in CTEs, subqueries, UNION arms, or JOINs, caught by the recursive AST walk. There are three sub-cases:
To grant the write, mark the table read_write:
Run midplane policy test to confirm the change before you ship it.

multi_statement

The query contained more than one top-level statement. Midplane allows exactly one statement per query — stacked statements (the classic SQL-injection vector) never reach the database.
Fix: split the query into one statement per call. A trailing semicolon is fine; a second statement is not.

dangerous_statement

A guardrail denied a categorically-destructive operation — regardless of table_access. On by default (0.9.0+), so this fires even on a table you’ve marked read_write. Two independent guards produce it:
The agent-facing message names the operation and the flag to relax it, for example:
Because the guardrail runs last, a statement another rule would also deny (for example a DROP under the no-YAML deny-all-writes default) surfaces that other rule’s reason instead — the statement is blocked either way. See the guardrails schema.

parse_error

The input failed to parse. libpg_query, the real Postgres parser, decides this — anything that isn’t valid Postgres SQL is denied. Midplane never enforces policy on text it can’t read. Fix: rewrite the query as valid Postgres SQL. Midplane parses with the Postgres parser, so dialect-specific or exotic syntax it doesn’t recognize is denied — rewrite it into a supported form.

Result masking

column_masking is not one of the ordered policy rules above — those decide whether a query runs. It fires when the engine can’t safely apply column masking to a query that touches a masked table, failing closed: the query is denied before it runs and all rows withheld rather than risk returning an unmasked value. Plain aggregates and joins over a masked table are not denied — they rewrite and run. The agent-facing message hints at the fix: query the masked column directly rather than through a view, a whole-row serializer, or an opaque function, so the engine can mask it at the source. See column masking for the mechanic and transforms.

Still stuck?

If a query is denied and you can’t tell why, dry-run it with midplane policy test — it prints the deciding rule and the exact agent-facing message against your policy file, with no database connection. For more, see troubleshooting.

Troubleshooting

Common denials, connection problems, and agent-wiring issues.