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_error → multi_statement → table_access → dangerous_statement. Two are always-on guards (parse_error, multi_statement); the rest are driven by your policy — table_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:
read_write:
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.
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:
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 withmidplane 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.