DLP Rules Editor
Supported modes: Hosted Available in: Solo Teams -- View pricing
DLP rules are the core admin lever for blocking sensitive content
from leaving developer machines, the LLM gateway, and (soon) the
browser extension. This guide walks through the dashboard
workflow end-to-end. For the API contract see the
/api/orgs/{id}/dlp/rules family of endpoints documented in the
backend OpenAPI spec.
What a DLP rule is
A DLP rule has six fields:
| Field | Type | Notes |
|---|---|---|
name | string | Human-readable label, shown in audit logs. Required. |
pattern | string (RE2) | Compiled at insert time. PCRE features rejected. Required. |
category | enum | pii / secret / ip / financial / compliance / custom. Required. |
action | enum | detect / mask / block. Required. |
scopes | string[] | Any combination of sdk, gateway, browser_ext, scout. Required. |
enabled | bool | Defaults to true. A disabled rule is invisible to enforcement. |
A rule lives in a lifecycle state machine:
draft -> tested -> impact_previewed -> [pending_approval] -> live
|
+-> disabled
A new rule starts in draft. You can move it to live directly,
or route through the optional approval queue for high-impact rules.
Only live rules are enforced by the SDK / gateway / browser
extension.
Writing your first rule
Open Governance → DLP Rules in the dashboard. Click New rule.
Pattern syntax
Patterns must be RE2-compatible. Most regular regexes work, but the following PCRE features are NOT supported:
- Lookaheads:
(?=foo),(?!foo) - Lookbehinds:
(?<=foo),(?<!foo) - Backreferences:
\1,\2 - Possessive quantifiers:
*+,++
If you paste a PCRE pattern, the test affordance immediately shows the compile error inline. Fix it before saving.
Common patterns
US Social Security Number:
\b\d{3}-\d{2}-\d{4}\b
Email address (loose):
[\w._%+-]+@[\w.-]+\.[A-Za-z]{2,}
AWS access key:
AKIA[0-9A-Z]{16}
Credit card (Visa):
\b4\d{3}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b
Internal project code names:
\b(Manhattan|Apollo|Voyager|Falcon)\b
Test before saving
The form has a Test pattern affordance. Type a sample input that contains the thing you want to catch:
Customer record:
name: Alice Johnson
ssn: 123-45-6789
cc: 4111-1111-1111-1111
Click Test pattern. The backend compiles your regex and reports:
compiles: true | false— if false, the error message points to the exact column.matched: true | false— whether the sample triggered.matches: [...]— up to 10 matched substrings, in order.
Iterate until the matches look right. Remember:
- Greedy is the default. Use
?after a quantifier (.*?) for non-greedy. \bis your friend. Word boundaries prevent4111from matching inside4111111111(which would be a 16-digit card).- Anchor when you can.
^and$are cheap and prevent false positives.
Action
Pick one:
- detect — Log the match in audit. Don't change anything. Use this when you want visibility but the rule is too noisy or too new to enforce.
- mask — Replace the matched span with
[REDACTED:CATEGORY:LEN]before forwarding to the LLM. The user's original prompt is NOT changed in their UI; only the network payload is masked. - block — Reject the entire request with a 403 (gateway) or a
PolicyDeniedError(SDK). The reason includes the rule name so the user knows what triggered.
Scopes
A rule with scopes: ["sdk"] only fires inside the on-machine SDK
guard call. A rule with scopes: ["sdk", "gateway"] fires both
on-machine AND in front of the LLM provider.
For maximum coverage, set ["sdk", "gateway", "browser_ext"]. The
scout scope is reserved for the discovery agent (Phase 11).
Publishing
Click Publish on a draft rule. The rule transitions to live
and the org's policy_version counter bumps by 1. Within 5 minutes
(the next SDK policy poll) every enrolled machine sees the new rule.
The gateway picks it up immediately because the gateway has its
own custom rules cache that hot-reloads from
/etc/controlzero/custom_dlp_rules.json on file change. See
Phase 9 gateway integration for the deploy
mechanism.
Two-person approval (high-impact rules)
If a rule could break production (e.g., a block action with the
gateway scope and a permissive pattern), you may want a second
admin to sign off. The flow:
- Admin A creates the rule, tests it, then clicks Request approval instead of Publish.
- The rule transitions to
pending_approvalstate. Admin A's user ID is recorded aspending_approval_by. - Admin B (a different user) opens the rule and clicks
Approve. The rule transitions to
liveandpolicy_versionbumps.
If admin A tries to approve their own request, the API returns
403 SAME_ACTOR. The two-person check is enforced server-side
so a malicious actor with a stolen session can't bypass it from
the dashboard.
If 48 hours pass without approval, the rule auto-rejects and
falls back to draft (background expiry job, runs nightly).
Editing a live rule
Live rules are immutable by direct edit. To change a live rule:
- Click Disable to take it out of enforcement.
- Click Edit (the rule is now back in
draft). - Change the pattern / action / scopes.
- Click Test pattern to verify.
- Click Publish to put it back into enforcement.
This dance forces you to think about the change rather than silently mutating something developers depend on.
The edit reason field is required on every patch and surfaces in the version history.
Version history
GET /api/orgs/{id}/dlp/rules/{rid}/versions returns every prior
state of a rule (newest first). The dashboard shows this as a
sidebar drawer when you click a rule's "Versions" button.
Each version row has:
version(monotonic per rule)pattern,category,action,scopesat that point in timeedit_reasonandedited_byadminedited_attimestamp
Versions are immutable — they are append-only snapshots
written by the storage layer's UpdateDLPRule transaction. You
cannot delete a version, even by deleting the rule.
Rollback
Find the version number you want to restore (from the sidebar
drawer or the API). Click Rollback to v3 (or call
POST /dlp/rules/{rid}/rollback with target_version: 3).
The rule's pattern, category, action, and scopes are restored to the v3 values. Two important behaviors:
- Rollback creates a NEW version. History is never deleted.
The new version's
edit_reasonis auto-filled with"ROLLBACK to v3: {your reason}". - The result lands in
draft, notlive. Even if you're rolling back from a regression, you have to explicitly re-publish. This avoids accidentally pushing a half-tested rollback into production.