Skip to main content

Writing Rules

A well-written rule is specific, testable, and accompanied by a clear remediation message. This guide walks through the process of designing rules from scratch.

Step 1 — Identify the constraint

Before writing a pattern, articulate the constraint in plain English:

  • "We do not allow eval() in production code."
  • "All React components must be functional, not class-based."
  • "The moment package is banned; use date-fns instead."
  • "Passwords must not be hardcoded as string literals."

A constraint that is hard to articulate clearly will produce a rule that is hard to maintain.

Step 2 — Choose the right category

Pick the category that best fits:

  • Code quality / UI patterns → frontend or backend
  • Security vulnerabilities → security
  • AI model integration → llms
  • PII, schemas, migrations → data
  • Infrastructure, containers → deployment
  • Performance, caching → scaling
  • npm packages → dependencies

Step 3 — Write the pattern

Patterns are ECMAScript regular expressions. The engine tests each added or modified line against the pattern.

Simple literal match

pattern: "console\\.log"

Matches any line containing console.log.

Alternation

pattern: "eval\\(|Function\\("

Matches lines with either eval( or Function(.

Word boundaries

Use \\b to avoid partial matches:

# Matches: eval(
# Does NOT match: preeval(, evaluator
pattern: "\\beval\\("

Character classes

# Matches: document.getElementById, document.querySelector, document.querySelectorAll
pattern: "document\\.(getElementById|querySelector)"

Common pitfalls

ProblemWrongRight
Dots match any char"import.moment""import\\.moment"
Greedy matching"secret.*value""secret\\s*=\\s*['\"][^'\"]{6,}"
Case sensitivity"eval""eval" + test with actual code
Escaping in YAMLSingle backslashDouble backslash: "\\b"

YAML escaping

In YAML double-quoted strings, backslashes must be doubled. The regex \beval\( becomes "\\beval\\(" in YAML:

pattern: "\\beval\\s*\\(" # regex: \beval\s*\(

In YAML single-quoted strings, no escaping is needed — but you cannot use special YAML characters:

pattern: '\beval\s*\(' # works, but harder to read for complex patterns

Step 4 — Set the severity

Ask: "If this pattern appears in a merged PR, how bad is it?"

  • Would it introduce a security vulnerability?block
  • Would it break a team convention but not cause a bug?warn
  • Is it a style suggestion?info

When in doubt, start at warn. You can upgrade to block later (the ratchet allows upgrades).

Step 5 — Write the remediation message

The remediation message appears in the check output next to the violation. It should:

  1. Explain why the pattern is banned
  2. Show what to do instead with a concrete example
  3. Be brief — 2-4 sentences max
remediation: >
eval() allows arbitrary code execution and is a critical
XSS vulnerability. Use JSON.parse() for deserializing JSON,
or a safe expression library (e.g. expr-eval) for computed
expressions.

Step 6 — Add exclusions

Most rules have legitimate exceptions. Add them as glob patterns:

exclude:
- "**/*.test.ts" # test files may use eval for mocking
- "**/*.spec.tsx"
- "**/fixtures/**"
- "**/scripts/migrate-*"

Use ** for recursive matching. The glob is matched against the file path relative to the project root.

Step 7 — Add compliance mappings

If your organization reports against SOC 2, EU AI Act, HIPAA, or DORA, map each rule to the relevant controls:

maps-to:
- framework: soc2
control: CC6.1
- framework: hipaa
control: "164.312(a)(2)(iv)"

See Framework Mappings for the full list of available controls.

Step 8 — Test the rule

Use the dry-run mode to test before committing:

vyb check --dry-run

Or use the MCP server to test a proposed rule interactively:

proposeRule → dryRunRule → (review output) → commitRule

See Dry Run for the complete testing workflow.

Full example: a complete security rule

- id: sec-007
name: no-prototype-pollution
description: >
Detect patterns that may enable prototype pollution —
assigning to __proto__, constructor.prototype, or
Object.setPrototypeOf with user-controlled input.
severity: block
pattern: "__proto__|constructor\\.prototype|Object\\.setPrototypeOf"
pattern-mode: line
remediation: >
Prototype pollution allows attackers to inject properties
into base objects. Use Object.create(null) for data maps,
validate input schema with Zod, and never merge untrusted
objects without explicit key allowlisting.
exclude:
- "**/*.test.ts"
- "**/polyfills/**"
enabled: true
maps-to:
- framework: soc2
control: CC7.1
- framework: eu-ai-act
control: Art.9.5

Tips for teams using AI IDEs

When your team uses Cursor or Claude Code, some AI-generated patterns may be subtle. Consider:

  • Semantic rules are hard: Regex matches text, not semantics. A rule banning eval won't catch window["ev"+"al"](). Use multiple patterns for known evasions.
  • Test with real AI output: Run a sample generation in your IDE and check the output against your rules. AI tools sometimes choose unexpected idioms.
  • Use the MCP server for iteration: The Claude Desktop MCP lets you dryRunRule against your actual codebase before committing.

Next: Dry Run Mode