Freeze Destructive QuickBooks Operations
Denies every destructive QuickBooks Online (QBO) tool call on the agent channel before it reaches the MCP server.
- Direction
- ingress
- Rego package
quickbooks.ingress.freeze_destructive_ops- App
- quickbooks
- Bundles
- soxsoc2
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:99d6257087dcfe5541c12a941bbadadf32fde18bbec355c8c7fcc786e5f70040
quickbooksfreeze-destructive-opsingresssoxsoc2
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny destructive tool calls, allow everything else
Package: quickbooks.ingress.freeze_destructive_ops
What it does
Denies every destructive QuickBooks Online (QBO) tool call on the agent channel before it reaches the MCP server. In QBO semantics a transaction delete is a hard delete — the row is gone and is recoverable only from the audit log, not from a recycle bin. An agent (or an agent following a poisoned instruction) must therefore never be allowed to consummate one. A person performs any deletion in the QuickBooks Online UI instead. There is no group exemption: this policy blocks all callers.
Two destructive shapes are covered:
- Dedicated destructive tools (official server, LibreChat, Claude connector).
The official Intuit server exposes
verb_entitytools, and the policy denies any tool whose name carries a destructive verb (delete,void, ordeactivate) followed by an entity separator (_or-) — matched at a word boundary, case-insensitively — which covers all 20 destructive tools in the official inventory:delete_invoice,delete_payment,delete_bill,delete_bill_payment,delete_journal_entry,delete_deposit,delete_transfer,delete_credit_memo,delete_refund_receipt,delete_purchase,delete_purchase_order,delete_sales_receipt,delete_vendor_credit,delete_estimate,delete_time_activity,delete_customer,delete_vendor,delete_employee,delete_item, anddelete_attachable. (Transaction deletes are hard deletes; the name-entity "deletes" — customer/vendor/employee/item — are QBO deactivations, blocked here too for a consistent no-destruction posture.) No surveyed build exposes a dedicatedvoid_<entity>ordeactivate_<entity>tool — voids/deactivations there ride thedelete_*name or theoperationargument — but the name rule matches those verbs too, symmetric with theoperationrule, so a dedicatedvoid_*/deactivate_*tool on the unverified connector build cannot slip past. - Parameterized 6-mega-tool shape (archived hvkshetry server). That server
hides the verb in an argument —
transaction(operation="delete"|"void"),party(operation="deactivate"), etc. — so a tool-name match sees onlytransaction,party,item, etc. To cover it, the policy also denies whenever theoperationargument resolves todelete,void, ordeactivate(case- and whitespace-insensitive), read from eitherinput.payload.args.operationorinput.payload.params.arguments.operation.
Reads (get_*, search_*), the 11 financial reports, and non-destructive
create/update writes (create_invoice, update_customer, …) pass through
unchanged. Because the check runs at ingress, a blocked delete never touches the
ledger.
Compliance alignment
- SOX §802 / 18 U.S.C. §1519 — anti-destruction/alteration of records. QBO is the general ledger for most SMBs; every transaction row is a book of record. This policy keeps agent-initiated destruction (and voids) of invoices, payments, journal entries, deposits, transfers, bills, and the rest off the MCP path, so a financial record cannot be erased or altered-by-deletion by an automated actor. §802 / SEC Rule 2-06 — supports retention and legal-hold posture by keeping agent-driven deletion off evidence paths.
- SOC 2 PI1.5 — integrity of stored records. Supports processing-integrity by preventing agent-initiated destruction of stored accounting records on the agent channel.
Tool name matching
Matches case-insensitively on the tool name. The DTwo gateway prefixes tool
names with the configured MCP server name (e.g. quickbooks-delete_invoice), and
that prefix is not standardized across the official, LibreChat, and
Claude-connector builds — so instead of an exact name the policy matches a
destructive verb (delete, void, or deactivate) at a word boundary (start of
name, or preceded by a non-letter such as the gateway's -/_ separator)
followed by an entity separator (_ or -).
This is portable across server-name prefixes and across builds that render the
verb/entity boundary with either _ (delete_invoice) or - (delete-invoice),
and covers every delete_<entity> tool — and any dedicated void_<entity> /
deactivate_<entity> tool — without enumerating each one. It does not
match a restore-style undelete_* or reactivate_* name, nor a read tool that
merely contains the word "deleted"/"voided" (e.g. get_deleted_invoices,
get_voided_invoices), nor an unrelated avoid_*, because in each case the
character right after the verb is a letter (or a letter precedes it), not a
separator at a boundary.
The name is read from both the PARC field (input.resource.name) and the
legacy alias (input.payload.name), matched independently: a request that omits
the resource block cannot skip the name match by carrying the name only in
payload.name (fail-open hardening). Each field is coerced to a lowercased,
whitespace-trimmed string — a missing or non-string value (number, null, array,
object) resolves to the empty string rather than leaving the match undefined, and
trim_space strips leading/trailing spaces, tabs, and newlines so a padded name
such as quickbooks-delete_invoice\n cannot slip past the boundary match.
Confirm the exact names your gateway sends with a live tools/list (or the
dump-input debug technique) before relying on this in production — the landscape
note flags the Claude-connector tool names as unpublished/unverified, and the QBO
report tool-name strings were likewise not verified from source.
Argument shape
operationverb (parameterized servers). Read frominput.payload.args.operationandinput.payload.params.arguments.operationviaobject.getchains. Each value is stringified, lowercased, and whitespace-trimmed before comparison against the destructive set{delete, void, deactivate}. A padded" delete ", a case variant (DELETE), or the same verb delivered under the alternateparams.argumentscontainer are all caught.- The dedicated
delete_*tools are denied on name alone; theirid/ entity-id arguments are not inspected.
Examples
Allowed — read tool, untouched
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "quickbooks-get_invoice", "type": "tool" },
"payload": { "name": "quickbooks-get_invoice", "args": { "id": "145" } }
}
}
allow = true, no reason.
Allowed — non-destructive create write
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "quickbooks-create_invoice", "type": "tool" },
"payload": {
"name": "quickbooks-create_invoice",
"args": { "customer_ref": "58", "line_items": [{ "item_ref": "1", "qty": 2, "unit_price": 50 }] }
}
}
}
allow = true, no reason.
Denied — dedicated delete tool
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "quickbooks-delete_invoice", "type": "tool" },
"payload": { "name": "quickbooks-delete_invoice", "args": { "id": "145" } }
}
}
allow = false, reason names the tool and points to the QuickBooks Online UI.
Denied — parameterized mega-tool hiding the verb in operation
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "qbo-transaction", "type": "tool" },
"payload": {
"name": "qbo-transaction",
"args": { "operation": "void", "entity_type": "Payment", "id": "22" }
}
}
}
allow = false, reason names the void operation.
Composition
Single-purpose: this policy only freezes destruction. Useful companions:
- PF-09 gate-money-movement — group-gate and cap
create_payment,create_bill_payment,create_refund_receipt, andcreate_transferso money-movement writes are governed even though they are not deletes. - PF-12 role-gate-writes — allow
create_*/update_*only for a finance IdP group, leaving reads and reports open; attach both for full write governance. - A journal-entry lockout — deny
create_journal_entry/update_journal_entryfor all agents, since direct ledger restatement is the highest-risk write.
Known limitations
- No identity exemption by design. The spec is a hard freeze — every caller is
denied and no group can override it through the agent. If you need a break-glass
finance role, add a separate
allow ifbranch gated oninput.subject.claims(groups) rather than editing this policy; group names would be placeholders to replace with your IdP's group at import time. - Tool names are unverified for two builds. The Claude-connector tool names are
not published, and the QBO report tool-name strings were not verified from source.
The
delete_verb match assumes the connector reuses the officialverb_entityvocabulary. Capture the livetools/listand confirm before production use. - Destructive-verb word-boundary match. Matching
delete,void, ordeactivateat a boundary (followed by a_or-separator) over-blocks any future or third-party tool whose name contains adelete_<x>/void-<x>/deactivate_<x>segment (e.g. a hypotheticalsoft_delete_noteon another server on the same gateway). The failure mode is over-blocking, never under-blocking. Restore-styleundelete_*/undelete-*andreactivate_*names are not matched (the boundary requires a non-letter before the verb), a read tool whose name merely contains "deleted"/"voided" (e.g.get_deleted_invoices,get_voided_invoices) is not matched (the char after the verb is a letter, not a separator), and an unrelatedavoid_*is not matched (a letter precedesvoid). Thevoid/deactivateverbs are matched on the name symmetrically with theoperation-argument rule: no surveyed build ships a dedicatedvoid_*/deactivate_*tool, but the connector build's tool names are unverified, so the name rule blocks them pre-emptively rather than relying on the argument rule alone. - Verb/entity separator must be
_or-. The name match requires a_or-between thedeleteverb and the entity (delete_invoice,delete-invoice); a hypothetical concatenated name with no separator (deleteinvoice) is not matched by the name rule. This is not a real evasion on a correctly configured gateway — it routes on the exact server-registered tool name, and every surveyed QBO build uses an underscore-separateddelete_<entity>name — but if you cannot rely on that invariant, compose the PF-28default-deny-unknown-toolsallowlist. - Reversible deactivation via
update_*is not blocked. In QBO, deactivating a name entity (customer/vendor/employee/item) can be done either by the dedicateddelete_<entity>tool (blocked here) or by an ordinaryupdate_<entity>write that setsactive: false— and that update passes through, because this policy deliberately allows non-destructive create/update writes. The gap is bounded to reversible deactivations (they can be re-activated by another update); the irreversible concern — hard deletes and voids of transactions — has noupdate_*equivalent and is fully covered by thedelete/operationrules. If you need to freezeactive: falsetoggles too, compose PF-12 role-gate-writes (or a dedicated update-guard policy) that inspectsupdate_*argument bodies. - The
operationrule keys on exact verbs. It denies onlydelete,void, anddeactivate. A parameterized server that uses a different destructive verb, or a mega-tool call that omitsoperationentirely, is not caught by the argument rule (and, having nodelete_tool name, would pass). Unlike a fail-closed allowlist, this follows the spec's explicit-deny model — confirm your server'soperationvocabulary and extenddestructive_operationsif it uses other verbs. A non-stringoperationvalue (e.g. an array["delete"]) is stringified bysprintf("%v", …)to"[delete]", which is not in the destructive set and so is not caught by the argument rule. This is not a real evasion: a typed mega-tool server rejects a non-stringoperation, so the call never reaches a destructive code path — but if you cannot rely on server-side type validation, add the PF-28default-deny-unknown-toolsallowlist. - Name matching is normalized apart from non-whitespace padding. The tool name
is lowercased and
trim_spaced before the boundary match, so casing and leading/trailing spaces, tabs, and newlines are handled, and bothresource.nameandpayload.nameare checked so a missingresourceblock cannot fail open. It does not normalize other trailing/embedded characters: a name padded with a non-whitespace code point (e.g. a zero-width space) or built from Unicode homoglyphs ofdelete_would not match. These are not real evasions on a correctly configured gateway — it routes on the exact server-registered tool name, so a padded/homoglyph name does not resolve to the real destructive tool — but if you cannot rely on that invariant, compose the PF-28default-deny-unknown-toolsallowlist alongside this policy. - MCP path only. Deletes performed in the QuickBooks Online web UI, via the QBO REST API directly, or by another integration are outside the gateway's reach.
Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.
Policy source (Rego)
package quickbooks.ingress.freeze_destructive_ops
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# Destructive verbs that can appear in the `operation` argument of a
# parameterized (mega-tool) QuickBooks MCP server such as the archived
# hvkshetry build — transaction(operation="delete"|"void"), etc. Compared
# case- and whitespace-insensitively.
destructive_operations := {"delete", "void", "deactivate"}
# Case-insensitive tool name, read from BOTH the PARC field (resource.name) and
# the legacy alias (payload.name). name_of coerces to a lowercased,
# whitespace-trimmed string: a missing OR non-string value (number, null, array,
# object) resolves to "" rather than leaving the rule undefined (an undefined
# name would make the regex check undefined and skip matching entirely —
# fail-open). trim_space strips leading/trailing whitespace so a padded name like
# "quickbooks-delete_invoice\n" cannot slip past the boundary match.
name_of(key) := trim_space(lower(v)) if {
v := object.get(object.get(input, key, {}), "name", "")
is_string(v)
}
name_of(key) := "" if {
v := object.get(object.get(input, key, {}), "name", "")
not is_string(v)
}
resource_name := name_of("resource")
payload_name := name_of("payload")
# Display name for the deny reason: the PARC name when present, else the legacy
# payload name (so the message is meaningful even if the resource block is absent).
display_name := resource_name if resource_name != ""
display_name := payload_name if resource_name == ""
# Dedicated destructive tools. The gateway prefixes the tool with the configured
# server name, which is not standardized, so we match a destructive verb
# (`delete`, `void`, or `deactivate`) at a word boundary (start of the name, or
# preceded by a non-letter such as the `-`/`_` gateway separator) followed by an
# entity separator (`_` or `-`), rather than an exact name. This covers every
# `delete_<entity>` tool in the official inventory without enumerating each one,
# and — because the separator class is `[_-]` — also catches a hyphen-cased
# `delete-<entity>` name from a server or gateway build that renders the
# verb/entity boundary with `-` instead of `_`. The `void`/`deactivate` verbs are
# matched on the NAME too (symmetric with the `operation`-argument rule below) so
# a dedicated `void_<entity>`/`deactivate_<entity>` tool — plausible on the
# Claude-connector build whose tool names the landscape note flags as unverified —
# cannot slip past a delete-only name match. It does NOT match a restore-style
# `undelete_*`/`undelete-*` or `reactivate_*` name (a letter precedes the verb),
# nor a read tool that merely contains the word "deleted"/"voided" such as
# `get_deleted_invoices`/`get_voided_invoices` (the verb is followed by a letter,
# not a separator), nor an unrelated `avoid_*` (the `void` there is preceded by a
# letter). Both names are checked independently, so a malformed value in one field
# cannot suppress a real destructive verb carried in the other.
is_destructive_tool_name if {
regex.match(`(^|[^a-z])(delete|void|deactivate)[_-]`, resource_name)
}
is_destructive_tool_name if {
regex.match(`(^|[^a-z])(delete|void|deactivate)[_-]`, payload_name)
}
# All `operation` argument values, read from both the generic gateway args key
# and the alternate params.arguments container, normalized (stringified,
# lowercased, whitespace-trimmed). Empty/missing values are dropped.
operation_values contains v if {
op := object.get(object.get(object.get(input, "payload", {}), "args", {}), "operation", "")
v := lower(trim_space(sprintf("%v", [op])))
v != ""
}
operation_values contains v if {
params := object.get(object.get(input, "payload", {}), "params", {})
arguments := object.get(params, "arguments", {})
op := object.get(arguments, "operation", "")
v := lower(trim_space(sprintf("%v", [op])))
v != ""
}
# A parameterized call whose `operation` verb is destructive.
destructive_operation_arg if {
some v in operation_values
destructive_operations[v]
}
# A destructive request: a dedicated delete_/void_/deactivate_ tool, or any tool
# carrying a destructive `operation` argument.
is_destructive if {
is_destructive_tool_name
}
is_destructive if {
destructive_operation_arg
}
# Allow anything that is not destructive (reads, reports, create/update writes,
# and non-QuickBooks tools).
allow if {
not is_destructive
}
reasons contains msg if {
is_destructive_tool_name
msg := sprintf("The tool '%s' performs a destructive QuickBooks operation. In QuickBooks Online most transaction deletes are hard deletes, recoverable only from the audit log, so agent-initiated deletes and deactivations are blocked on this channel. A person must perform the deletion in the QuickBooks Online UI. Contact your finance or InfoSec team if this block is a false positive.", [display_name])
}
reasons contains msg if {
destructive_operation_arg
some v in operation_values
destructive_operations[v]
msg := sprintf("This QuickBooks tool call requests a destructive operation ('%s') through its 'operation' argument. Delete, void, and deactivate are blocked on the agent channel because QuickBooks transaction deletes are hard deletes, recoverable only from the audit log. Perform the change in the QuickBooks Online UI instead. Contact your finance or InfoSec team if this block is a false positive.", [v])
}
reason := joined if {
count(reasons) > 0
reason_list := sort([r | some r in reasons])
joined := concat("; ", reason_list)
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa