For rule creation, actions, and pattern syntax basics, see Guardrails Configuration.
What your patterns match against
mongosh and Compass are JavaScript interpreters. They evaluate your expression locally and send only the resulting command document — the shell text is never transmitted.
\{"drop": a reliable signature for this command is a drop, rather than the word drop appears somewhere in the payload.
$db names the target database, on every command.
Operators keep their names as JSON keys, at any depth. A pattern for "\$out" finds it inside a nested $lookup sub-pipeline just as easily as at the top level.
Webapp access: raw text, not command documents
Everything above assumes a native connection — a realmongosh or Compass process, proxied by Hoop, evaluating your JavaScript locally and sending only the resulting command document. Web App and one-off access work differently.
When a resource role permits Web App / one-off access — the query box in the Hoop Web Console, or hoop exec <role> -i "..." from the CLI — there is no local shell to evaluate your input first. What you type is what the guardrail sees, verbatim:
{"drop":"users","$db":"..."}. Recipes written for the command grammar (\{"drop":) never fire here — that exact string never appears, because the payload is shaped like JavaScript, not JSON.
Three things follow from the text being literal and unevaluated:
Quoting is optional and inconsistent. mongosh accepts unquoted object keys ({drop: "users"}), single quotes, double quotes, and whitespace anywhere. A pattern anchored to "drop": misses {drop:"users"} and { drop : "users" } alike — anchor on the method call instead (\.drop\s*\(), and treat a quote around a key as optional (["']?) rather than assumed.
There is no reliable command-position anchor. The wire JSON always puts the command name first, which is what makes \{"drop": safe — an ordinary document can never open with that. Raw text has no such guarantee: $where can appear inside a live query, a comment, or a string a user is inserting, and a pattern can’t tell those apart. Anchor as tightly as the syntax allows (a leading . for a method call, db. for a namespace), and accept that some raw-text recipes are blunter than their command-document equivalents.
Variable indirection defeats literal matching. A native connection always sends the final evaluated command, however it was built locally. Web App / one-off input is matched as typed, before anything would be evaluated — var f = {}; db.users.deleteMany(f) never contains the text deleteMany({}), so it slips past a pattern written for the literal call. There is no fix at the pattern layer; where this risk matters, pair the rule with Require Approval or restrict the resource role to native access only.
Database context comes from a separate statement. There is no $db field to anchor on. use reporting or db.getSiblingDB("reporting") sets the database for whatever follows, but arrives as its own line — a rule scoped to one database needs to match that statement directly, not assume the name travels with every command.
Which rule type to use
Guardrail rules come in two types: a Pattern (pattern_match, a regex) and a word list (deny_words_list, a list of whole words).
Every recipe on this page is a Pattern, and that is not incidental. MongoDB signatures identify a command by its JSON punctuation — \{"drop":, "q":\{\},"limit":0 — and a word list matches whole words, so it cannot express a signature that begins or ends on a brace, quote or colon.
Use a word list when the thing you want to block genuinely is a whole word:
A word list is the blunter of the two.
salaries as a word list also fires when a document merely mentions the word, so prefer a Pattern whenever you need the match tied to a specific place in the command.
The same logic carries over to raw text: punctuation is still what ties a match to a method call (\.drop\() or an operator (\$where\b) rather than incidental text, so the recipes below stay Pattern rules in both forms.
Recipes
Each recipe gives the Block rule for the same operation in both forms — switch the tab to match how the resource role is accessed: Native for the command-document Pattern (validated against real payloads), Raw Text for the Web App / one-off Pattern written againstmongosh syntax. A role that allows both access modes needs both rules assigned. Shell syntax has more valid spellings than the wire protocol has encodings — run every raw-text rule through Test before you deploy below before assigning it widely.
block-collection-drop
Blocks dropping a collection.- Native
- Raw Text
The leading
\{" ties the match to the command position, so an ordinary document containing the word drop does not trip it. dropDatabase and dropIndexes are separate commands with their own names — use the next recipe to cover all of them.
block-destructive-ddl
Blocks every irreversible namespace or index operation.- Native
- Raw Text
block-writes
A read-only connection needs two rules. This one covers write commands, and the other covers writes hidden inside an aggregation pipeline:- Native
- Raw Text
The second rule is not redundant. A pipeline write looks like this:
aggregate — a read command — so rule 1 does not match it, but $out writes a collection. A read-only policy with only the first rule has this hole. Assign both to the same resource role.
block-delete-all
Blocks deleting every document in a collection, while allowing scoped deletes.- Native
- Raw Text
MongoDB puts the delete scope on the wire, which makes this precise rather than approximate:
limit: 0 means all matches, limit: 1 means one, and "q":{} is an empty filter.
The pattern has two alternatives because deleteMany({}) has two wire encodings, depending on your driver version. Both empty the collection, so both must be blocked:
The second alternative requires
multi to follow filter immediately, which is what keeps it from firing on a bulkWrite update — that op carries updateMods between the two fields.
Where that gap matters, put the resource role behind Action Access Requests and add a second rule on \{"delete": with the Require Approval action. Every delete then waits for an approver from the groups you set with --reviewers, and the approver sees the command — including the filter this pattern cannot judge — before it runs.
deleteMany with any predicate, deleteOne, and the 8.0 bulkWrite delete op. Two things to weigh before assigning it:
db.users.deleteOne({_id:1})waits for approval too. On an interactive connection that is real friction, so scope it to the resource roles that need it rather than all of them — see Read-Only with Approval for that shape, or Sensitive Operations - Dual Approval to require two groups.- It does not cover
db.users.findOneAndDelete({_id:1}), which the server receives asfindAndModifywithremove: true. Add"remove":trueas a second pattern if single-document deletes matter.
\{"delete":.*"limit":0. Do not use "limit":0 alone — in a find command limit: 0 means no limit, so it blocks the ordinary read db.users.find({}).limit(0).
block-update-all
Blocks updating every document in a collection.- Native
- Raw Text
multi: true marks a multi-document update and "q":{} an empty filter. This is also the technique for requiring two conditions together — a single pattern with .* spans between them.
This pattern depends on field order (
q, then u, then multi), which is driver behaviour rather than a protocol guarantee. Prefer order-independent patterns where you can, and revisit this one after a major driver upgrade.block-server-side-js
Blocks server-side JavaScript execution.- Native
- Raw Text
A
$where clause is arbitrary JavaScript evaluated by the server:
$code is how any JavaScript value renders, which is why the same rule also covers mapReduce’s map and reduce functions. That command sends:
map or reduce identifier is not defined in a fresh shell and throws ReferenceError before anything reaches the wire:
out: { inline: 1 } returns results instead of writing a collection, so a failed block leaves nothing behind. Map-reduce has been deprecated since MongoDB 5.0; if your server rejects the command outright, that error is not a guardrail result.
block-restricted-collection
Blocks one collection by name, without matching documents that merely mention it.- Native
- Raw Text
A query on a permitted collection can still reach a restricted one through a join. Add a second rule for
"from":"salaries" to cover $lookup and $graphLookup.
Test before you deploy
Never test a blocking rule with a command that would be destructive if the block fails. A typo in the pattern, or a guardrail assigned to the wrong resource role, means the command runs. The commands below are safe. They target collections that do not exist (gr_test_zz), so even if a rule fails to match, MongoDB performs a no-op instead of destroying data. The “must pass” commands are filtered reads against the same non-existent collection, which return an empty result.
no such command: bulkWrite and changes nothing.
db.users.countDocuments({})must not trigger a rule targetingcount.countDocumentssends anaggregatecommand; onlyestimatedDocumentCountsendscount.- An explained command must trigger the same rule as the command itself:
db.gr_test_zz.find({$where:"1"}).explain()blocks, becauseexplainis unwrapped to the command it wraps.
Test in
mongosh before Compass. Compass issues its own listCollections, $collStats and schema-analysis aggregate calls alongside your actions, so a broad rule can block Compass’s own metadata queries and make the session look broken rather than guarded.What patterns cannot catch
Which command produced a match. A pattern for an SSN fires identically on a query searching by an SSN and a write storing one:analytics database” is not expressible — enumerate what to deny, with patterns like "\$db":"production".
The size of a bulk operation. A pattern cannot count documents.
Commands that are not evaluated. A pattern targeting these never fires: listCollections, listIndexes, reIndex, compact, shardCollection, user management such as createUser, cursor iteration (getMore), and heartbeats (hello, ping).
For what patterns cannot express, layer your defenses:
- Require Approval on the broadest rules, so a human sees the operation regex cannot judge.
- Database-level controls: give the resource role’s MongoDB user a read-only role, or scope it to specific collections. The guardrail blocks the text; the database enforces the permission.
- Runbooks: parameterized operations for day-to-day work, instead of free-form shell access.
Related
Guardrails Configuration
Rule creation, actions, pattern syntax, and troubleshooting
SQL Guardrail Recipes
Patterns for tautologies, subqueries, CTEs, and missing WHERE clauses
Guardrails Overview
What guardrails do and how they fit with other features
Runbooks
Parameterized operations instead of free-form shell access