Custom DLP Patterns
Define organisation-specific detection rules that run on top of ShieldAgent's built-in PII, credential, and financial data coverage.
What Custom Patterns Are
Custom DLP patterns let your team define additional sensitive-data detection rules specific to your organisation — for example, internal project codes, proprietary identifiers, or domain-specific data formats. Each pattern is a RE2-compatible regular expression paired with an action (block, redact, alert, or shadow). Custom patterns are evaluated in sequence after built-in rules, so they extend rather than replace the standard coverage.
Plan Comparison
Custom pattern limits and workflow features depend on your ShieldAgent plan.
| Feature | Pro | Business |
|---|---|---|
| Maximum custom patterns | 20 | 20 |
| Available actions | block, redact, alert | block, redact, alert |
| Activation approval | Single approver | 4-eye (two approvers required) |
| Webhook delivery on match | — | Yes, HMAC-signed |
RE2 Syntax Limits
Patterns must be valid RE2 expressions. The following are not supported:
- Back-references (\1, \2, …)
- Lookahead and lookbehind assertions
- Backreference-based recursion
- PCRE possessive quantifiers
Maximum compiled pattern size is 256 KB. Overly broad patterns (e.g. .* with no anchors) are rejected at save time.
Test Before You Deploy
Use the dry-run endpoint to validate a pattern against sample traffic before activating it. Dry runs evaluate the pattern on up to 50 recent tool-call payloads captured in shadow mode. No action is taken — results are returned immediately.
- Open the dashboard, go to Security → DLP → Custom Patterns, then select New Pattern.
- Enter your RE2 expression and choose an action.
- Click Test Pattern to run a dry run against recent shadow-mode traffic.
- Review matched excerpts (values are partially masked) and adjust the pattern if needed.
- Click Save to submit the pattern for activation (subject to approval on Business plans).
Dry-run via SDK
You can also drive dry runs programmatically:
const result = await client.dlp.patterns.dryRun({
expression: '\\bPROJ-[0-9]{4,6}\\b',
action: 'alert',
sampleSize: 50,
});
// result.matches — array of masked excerpts
// result.matchCount — total hits across sampled payloads
console.log(`Matched ${result.matchCount} times across ${result.sampleSize} payloads`);Action Options
Each custom pattern is paired with one of five actions:
| Action | When to use |
|---|---|
| block | Reject the tool call immediately. Use for patterns that must never leave your perimeter. |
| redact | Replace the matched value with a redaction token and forward the sanitised request. Use when the downstream tool still needs to receive a request but must not see the sensitive value. |
| alert | Allow the request and generate an audit event. Use for monitoring when blocking would break a workflow you are still evaluating. |
| notify | Allow the request and deliver a signed webhook or Slack event to your configured endpoint (Business). On Pro, writes an in-app notification instead. No raw match content is transmitted — only a SHA-256 hash. |
| shadow | Record the match silently with no modification or alert surfaced to the agent. Business plan only. Use during rollout to validate pattern accuracy before switching to a more aggressive action. |
4-Eye Approval (Business)
On Business plans, any new or modified custom pattern enters a pending state before it goes live. A second named approver — different from the pattern author — must review and approve it. This prevents a single administrator from activating an overly broad or incorrect pattern unilaterally.
Webhook Delivery
When a custom pattern with the notify action matches on a Business plan, ShieldAgent delivers a signed event to your configured endpoint. Each delivery carries an HMAC signature using your webhook secret. No raw match content is transmitted — only a SHA-256 hash of the matched value.
Webhook payload schema
{
"event": "dlp.notify",
"version": 1,
"patternId": "550e8400-e29b-41d4-a716-446655440000",
"patternName": "internal-project-codes",
"action": "notify",
"actionTaken": "notify",
"tenantId": "550e8400-e29b-41d4-a716-446655440001",
"agentId": "550e8400-e29b-41d4-a716-446655440002",
"mcpServerId": "550e8400-e29b-41d4-a716-446655440003",
"interactionId": "550e8400-e29b-41d4-a716-446655440004",
"contentHashSha256": "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3",
"contentHash": "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3",
"severity": "high",
"gdprClassification": "credential",
"timestamp": "2026-05-30T14:22:00.000Z"
}Field reference
| Field | Type | Nullable | Example | Description |
|---|---|---|---|---|
| event | string | no | "dlp.notify" | Always "dlp.notify". Use this to route the event in your handler. |
| version | number | no | 1 | Schema version. Increments on breaking changes. Current version: 1. |
| patternId | string (UUID) | no | "550e8400-…" | ID of the custom pattern that triggered this event. |
| patternName | string | no | "internal-project-codes" | Human-readable name of the pattern, as configured in your dashboard. |
| action | string | no | "notify" | Always "notify" in this payload. |
| actionTaken | string | no | "notify" | Alias for action. Included for schema symmetry with other event types. |
| tenantId | string (UUID) | no | "550e8400-…" | Your ShieldAgent workspace identifier. |
| agentId | string (UUID) | no | "550e8400-…" | Agent session that made the tool call triggering this match. |
| mcpServerId | string (UUID) | no | "550e8400-…" | The MCP server the call was routed to. |
| interactionId | string (UUID) | no | "550e8400-…" | Unique per tool-call interaction. Use as your idempotency key — retries carry the same ID. |
| contentHashSha256 | string (hex) | no | "a665a459…" | SHA-256 hash of the matched text. The raw value is never transmitted. |
| contentHash | string (hex) | no | "a665a459…" | Alias for contentHashSha256. |
| severity | "critical" | "high" | "medium" | "low" | no | "high" | Severity as classified by the matching pattern. |
| gdprClassification | "pii" | "credential" | "financial" | "health" | "none" | no | "credential" | GDPR data category of the matched content. |
| timestamp | string (ISO 8601) | no | "2026-05-30T14:22:00.000Z" | UTC timestamp when the event was generated. |
HMAC verification
The signature is sent in the X-ShieldAgent-Signature header as sha256=<hex>. Always verify it before processing the event. Use a constant-time comparison to prevent timing attacks:
Node.js
import crypto from 'node:crypto';
export function verifySignature(rawBody: string, secret: string, header: string): boolean {
if (!header.startsWith('sha256=')) return false;
const expected =
'sha256=' +
crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(header, 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// Express: capture the raw body before JSON parsing
// app.use('/shieldagent-notify', express.raw({ type: 'application/json' }));
// app.post('/shieldagent-notify', (req, res) => {
// if (!verifySignature(req.body.toString('utf8'), process.env.SHIELDAGENT_WEBHOOK_SECRET!, req.header('x-shieldagent-signature')!)) {
// return res.status(401).end();
// }
// const payload = JSON.parse(req.body.toString('utf8'));
// res.status(200).json({ ok: true });
// });Python
import hmac, hashlib
def verify_signature(raw_body: bytes, secret: str, header: str | None) -> bool:
if not header or not header.startswith('sha256='):
return False
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
# FastAPI example:
# @app.post('/shieldagent-notify')
# async def handle(request: Request):
# raw = await request.body()
# if not verify_signature(raw, os.environ['SHIELDAGENT_WEBHOOK_SECRET'],
# request.headers.get('x-shieldagent-signature')):
# raise HTTPException(401)
# payload = json.loads(raw)Test vector
Run your verifier against these values before going to production. Any difference means your implementation is incompatible with the production signing path:
secret = "shieldagent-test-secret-32ch-pad"
body = '{"event":"dlp.notify","version":1,"hash":"deadbeef"}'
header = "sha256=8834a0520425c5b374ab3d444d3c0368cb6eb1af1a1678a2ed5e8937e8bcb06f"Manage and rotate your webhook secret in Dashboard → Security → DLP → Notification Targets. Old signatures are invalidated immediately on rotation.
Slack Payload Schema
When your notification target is a Slack incoming webhook, ShieldAgent sends a Block Kit message instead of the canonical JSON payload. The X-ShieldAgent-Signature header is still present and is computed over the Block Kit body bytes — not the canonical payload. If your Slack receiver also verifies the signature, hash the raw request body exactly as received.
Block Kit example
{
"text": ":warning: ShieldAgent DLP notify: `internal-project-codes` (high)",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":warning: ShieldAgent DLP notify: `internal-project-codes` (high)"
}
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Pattern*\ninternal-project-codes" },
{ "type": "mrkdwn", "text": "*GDPR class*\ncredential" },
{ "type": "mrkdwn", "text": "*Agent*\n`550e8400-e29b-41d4-a716-446655440002`" },
{ "type": "mrkdwn", "text": "*MCP server*\n`550e8400-e29b-41d4-a716-446655440003`" },
{ "type": "mrkdwn", "text": "*Interaction*\n`550e8400-e29b-41d4-a716-446655440004`" },
{ "type": "mrkdwn", "text": "*Content SHA-256*\n`a665a45920422f9d…`" }
]
},
{
"type": "context",
"elements": [
{ "type": "mrkdwn", "text": "event=dlp.notify version=1 ts=2026-05-30T14:22:00.000Z" }
]
}
],
"metadata": {
"event_type": "dlp.notify",
"event_payload": { "...": "full canonical payload — same fields as webhook schema above" }
}
}Top-level fields
| Field | Type | Description |
|---|---|---|
| text | string | Plain-text summary shown in Slack notifications and link unfurls. |
| blocks | array | Block Kit sections: a summary header, a detail fields section with pattern name, GDPR class, agent, MCP server, interaction ID, and content hash prefix, plus a context footer with event metadata. |
| metadata.event_type | "dlp.notify" | Structured event type for SIEM ingestion. |
| metadata.event_payload | object | Full canonical DLP notify payload (same fields as the webhook schema above). Lets SIEM tooling index structured fields from a Slack-delivered event. |
What if I paste a Slack URL into a generic webhook target?
The API recognises Slack incoming-webhook URLs and automatically corrects the target kind from webhook to slack. The create or update call succeeds, and the response includes two extra fields:
| Response field | Type | Description |
|---|---|---|
| autoCorrectedToSlack | boolean | true when the kind was automatically changed from webhook to slack. |
| warnings | string[] | Contains "url_resembles_chat_platform_consider_dedicated_kind" when the URL was auto-corrected. |
If you configure kind: webhook with a URL that resembles Discord or Microsoft Teams, the API returns the same warning without auto-correcting — the target is saved as webhook and delivery may fail. Set the correct kind explicitly at create time.