decoyrail

Sensitive-data filtering (DLP)

The vault protects known secrets. DLP detectors catch structured sensitive data that was never vaulted, such as a card number in a prompt or a bank account in an uploaded log. They scan every outbound request. A blocking hit overrides any policy allow, like a decoy tripwire.

Watch it work live with decoyrail log -t in a second terminal: blocks show up as [DENY] events with dlp: notes, warnings as [ALRT].

The detectors

Each detector validates a checksum or strict structure. Well-known test values from payment-provider documentation pass through to avoid blocking test suites.

Detector Matches Validation
pan payment card numbers, plain or 4242 4242 / 4242-4242 styled Luhn checksum plus a real issuer prefix (Visa, Mastercard, Amex, Discover, JCB, Diners)
ssn US Social Security numbers in the dashed form 123-45-6789 area/group/serial validity rules
iban international bank account numbers, compact or print-spaced ISO 13616 mod-97 check, known country lengths
aba US bank routing numbers ABA checksum plus valid Federal Reserve prefix
email email addresses structural

Names, street addresses, and free-text health data are out of scope for rules: that is judgment-call territory and waits for the local judge model. No detector here supports a HIPAA claim.

Digit runs glued to identifiers stay quiet: UUIDs, timestamps, version strings, and order_4539... style IDs don't fire, because a card number inside a longer token is not a standalone card number.

Modes

Each detector has one of four modes:

Mode What a hit does
block the request is rejected with a 403 naming the detector and offset
mask the value is replaced with [decoyrail:masked:<detector>] and the request goes through
warn the request goes through untouched; an alert lands in the audit log
off detector disabled

Defaults: pan, ssn, iban, and aba warn; email is off, because ordinary developer traffic (commits, package metadata) is full of email addresses. Warn-first is deliberate: the detectors are new, so out of the box a hit shows up in decoyrail log -t as an [ALRT] event but nothing breaks. Run like that for a while; when the alerts you see are values you genuinely don't want leaving the machine, upgrade those detectors to block:

decoyrail dlp show              # current modes
decoyrail dlp set pan block     # upgrade one; a running proxy picks it up
decoyrail dlp set ssn mask      # or replace values instead of rejecting
decoyrail dlp set aba off       # the local override when you need one

The settings live in the [dlp] section of policy.toml, next to the egress rules, and hot-reload the same way. The shipped defaults:

[dlp]
pan = "warn"
ssn = "warn"
iban = "warn"
aba = "warn"
email = "off"

A tightened setup, with fixture values excused, looks like this:

[dlp]
pan = "block"    # reject requests carrying a real card number
ssn = "block"
iban = "mask"    # replace with [decoyrail:masked:iban] and forward
aba = "warn"     # keep watching before deciding
email = "off"
# Org-specific fixture values, compared ignoring case and separators:
allow = ["4111 1111 1111 1111", "GB82 WEST 1234 5698 7654 32"]

What a block looks like

The error body is machine-readable so a coding agent can fix its own request and retry:

{
  "decoyrail": true,
  "blocked": true,
  "reason": "dlp",
  "detectors": [{"detector": "pan", "location": "body", "offset": 17}],
  "message": "decoyrail blocked this request: sensitive data detected. ..."
}

The audit event records the detector, the location, and a salted fingerprint of the value. The value itself is never written anywhere. The fingerprint salt is local to the machine (~/.decoyrail/dlp.salt), so you can tell "the same card number tried to leave three times" from your own log, but a log consumer can't reverse or cross-reference it.

Debug mode: seeing what actually matched

Fingerprints tell you the same value fired repeatedly, but not what it was. When a block surprises you and you suspect a false positive, turn on debug mode:

decoyrail dlp set debug on      # or debug = true under [dlp] in policy.toml

While it is on, every request that carries a DLP hit is written in full to a file under ~/.decoyrail/dlp-debug/, one file per request, readable by your user only. The file starts with a header naming each hit and showing the matched value in its surrounding text, with the match marked:

# decoyrail dlp debug dump  2026-07-09T14:21:19.064Z
# POST api.anthropic.com/api/event_logging/v2/batch
# hits:
#   block aba@body:base64 off=306874 fp=6d313032fc2e9609
#     ..."routing_number": ">>>061000052<<<", "account": ...

For hits inside base64, hex, or percent encoded blobs, the snippet comes from the decoded text, so you see what the detector saw. The rest of the file is the request headers and body exactly as they would have left the machine, with one exception: any real secret the vault swapped in is scrubbed back to a named placeholder before the file is written. The audit log gains only a payload=<file> pointer in the event note, so decoyrail log -t tells you where to look; the log itself never holds the value, debug mode or not.

Once you know what fired, either add the value to allow (fixtures and test data), change the detector's mode, or take the block as the save it was. Then turn debug off and delete the dumps; they exist to answer a question, not to accumulate:

decoyrail dlp set debug off
rm -r ~/.decoyrail/dlp-debug

Scope and bounds