Smart Rules · node9 documentation

Smart Rules

Condition-based policy that matches raw tool arguments — not just extracted tokens.

Smart rules operate on the raw JSON arguments the AI sends, before any tokenization. Unlike dangerousWords (which scan extracted tokens), smart rules let you express structural conditions like "block DELETE only when no WHERE clause is present" — directly in your config. They are the primary way to write custom policy in Node9.

Built-in default
Node9 ships with one default smart rule active out of the box: no-delete-without-where. It blocks any SQL DELETE or UPDATE that lacks a WHERE clause — without requiring any config changes.

Anatomy of a Smart Rule

Full smart rule example
{
  "name": "block-prod-kubectl",
  "tool": "bash",
  "conditions": [
    {
      "field": "command",
      "op": "matches",
      "value": "kubectl.*--namespace[= ]production",
      "flags": "i"
    }
  ],
  "conditionMode": "all",
  "verdict": "block",
  "reason": "Production kubectl operations require a manual release process"
}

Fields

toolrequired
The tool name to match. Supports glob patterns: "bash", "mcp__postgres__*", "*" (all tools).
conditionsrequired
Array of conditions evaluated against the raw args object. All must pass by default (conditionMode: "all").
conditionModeoptional
"all" — every condition must be true (AND logic). Default.
"any" — at least one condition must be true (OR logic).
verdictrequired
What happens when the rule matches:
"review" — show the human approval prompt
"block" — hard deny immediately, no prompt shown
"allow" — skip all further policy checks
reasonoptional
Human-readable explanation shown in the approval prompt, negotiation message, and audit log.
nameoptional
A label for the rule, shown in node9 explain output and audit entries.

Condition Operators

Available op values
opMeaning
matchesField value matches a regex pattern (use flags: "i" for case-insensitive)
notMatchesField value does NOT match the regex pattern
containsField value contains the given substring
notContainsField value does NOT contain the substring
existsField is present and non-empty
notExistsField is absent or empty
matchesGlobField value matches a glob pattern (e.g. "**/node_modules/**")
notMatchesGlobField value does NOT match the glob pattern
Dot-notation field paths
The field key supports dot-notation to access nested args: "params.query.sql" navigates into { params: { query: { sql: '...' } } }. Field values are whitespace-normalized before matching, so multi-space SQL won't bypass regex rules.

Examples

Block DELETE/UPDATE without WHERE (built-in default)
{
  "name": "no-delete-without-where",
  "tool": "*",
  "conditions": [
    { "field": "sql", "op": "matches",    "value": "^(DELETE|UPDATE)\\s", "flags": "i" },
    { "field": "sql", "op": "notMatches", "value": "\\bWHERE\\b",        "flags": "i" }
  ],
  "conditionMode": "all",
  "verdict": "review",
  "reason": "DELETE/UPDATE without WHERE clause — would affect every row in the table"
}
Block writes outside the project directory
{
  "name": "sandbox-writes",
  "tool": "write_file",
  "conditions": [
    { "field": "path", "op": "notMatches", "value": "^/home/user/my-project" }
  ],
  "verdict": "review",
  "reason": "writing outside project directory"
}
Block wget piped to shell (extends the built-in curl coverage)
Node9 ships with review-curl-pipe-shell built in, which catches curl ... | bash. The default does not cover wget, so the rule below adds hard-block coverage for the same attack pattern using a different fetcher:
{
  "name": "block-wget-pipe-shell",
  "tool": "bash",
  "conditions": [
    { "field": "command", "op": "matches", "value": "wget.+\\|.*(bash|sh)", "flags": "i" }
  ],
  "verdict": "block",
  "reason": "wget piped to shell — remote code execution risk"
}
Note that built-in defaults are evaluated first in the smart-rules waterfall, so a project rule cannot downgrade or upgrade a verdict that a default already produced — it can only cover cases the default misses.

Merge Behaviour

Smart rules from all layers are concatenated in order: built-in defaults → active shields → global config → project config. First match wins. You cannot remove a built-in or shield rule from a lower layer.

Debugging with node9 explain

Dry-run the policy engine
Use node9 explain to see exactly which smart rule (or other tier) would trigger for any tool call:
# Check if a DELETE would be flagged
node9 explain execute_sql '{"sql":"DELETE FROM users"}'

# Check a safe scoped DELETE
node9 explain execute_sql '{"sql":"DELETE FROM orders WHERE id=1"}'
The output shows the full waterfall evaluation step-by-step, including which smart rule matched and its verdict.