Diagram

I live in Singapore. My blog is hosted on Cloudflare Pages, served globally. If I accidentally publish a post containing someone’s NRIC number, a Singapore phone number, my own API keys, or a client’s email address — I’ve violated the Personal Data Protection Act. The fines start at S$1 million.

The obvious fix: don’t publish PII. The engineering fix: make it impossible to publish PII by putting a scanner in the pipeline.


What pdpa-sg-clj detects

pdpa-sg-clj is a Clojure/Babashka toolkit I built that wraps ripgrep with Singapore-specific detection rules:

Severity What it detects Example
critical Valid NRIC/FIN numbers S0100000J (Mod-11 checksum verified)
critical SG phone numbers with +65 +65 9123 4567
high AWS access keys AKIA...
high Stripe live keys sk_live_...
high GitHub personal access tokens ghp_...
high PEM private key blocks -----BEGIN RSA PRIVATE KEY-----

Additional lower-severity detections:

Severity What it detects Example
medium Hardcoded passwords password = "supersecret"
medium Generic API secrets API_KEY = "sk-..."
low Real email addresses [email protected] (not example/test domains)

The NRIC detection is particularly careful — it doesn’t just regex-match the S1234567A pattern. That would generate false positives on hex strings, git hashes, and BSV transaction IDs (all of which appear in my blog). Instead, it runs the Singapore ICA Modulo-11 checksum algorithm to verify the check digit is mathematically valid. A hex string like deadbeefF matches the structural pattern but fails the checksum — so it’s ignored.


The pipeline integration

The blog’s publish script (scripts/publish.bb) already had a validate step that checks frontmatter, diagram syntax, and paragraph length. I inserted the PDPA scan as Step 1.5 — after validation but before the expensive build:

Diagram

The critical design decision: the scan runs on a single post during publish, not the entire repo. When I run bb scripts/publish.bb check-suspend-fix-automation-fish, the scanner only opens that one .md file. It takes ~0.2 seconds instead of 3 seconds for a full repo scan. The full-repo scan is available as bb pdpa-scan (no argument) for periodic audits.

The blocking rule

Only critical and high findings block publishing:

(def clean? (and (zero? (:critical counts))
                 (zero? (:high counts))))

Medium and low findings are printed as warnings but don’t block. This is a deliberate trade-off: my blog uses fake email addresses in code examples ([email protected]), which the scanner correctly ignores. But if I accidentally paste a real client email into a post, I want to know — without the scanner blocking every post that contains an @ sign.


How it’s wired

Diagram

Three files make it work:

  1. pdpa-sg-clj/ — a git submodule pointing at the toolkit repo
  2. scripts/pdpa-scan.bb — a 70-line wrapper that calls pdpa.scan/scan on a single post and formats the output
  3. scripts/publish.bb — one new step (1.5) that calls the wrapper and blocks on critical/high findings

The bb.edn was updated to include pdpa-sg-clj/src in its classpath and add cheshire as a dependency (needed by pdpa.scan for ripgrep JSON parsing).

Why a submodule and not a library dependency

I chose a git submodule over a Maven/Clojars dependency for one reason: the scanner rules need to evolve with Singapore PDPA amendments. The Safe NRIC rule, for example, changes on 31 December 2026. When I update the detection rules in pdpa-sg-clj, a git submodule update --remote pulls the latest rules into every project that uses it. No version bump, no release cycle — the scanner is always current with the law.


What happens when the scanner finds something

If I accidentally write S1234567A in a post body (a syntactically valid NRIC — the checksum matches), the pipeline stops:

🛡️  PDPA SG Compliance Scan
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Scanning: draft-post.md
  Findings: critical=1 high=0 medium=0 low=0

  [CRITICAL] draft-post.md:42 — Live Singapore NRIC / FIN (Mod-11 valid)

  ❌ PDPA scan BLOCKED — fix findings above before publishing
     Use: bb redact <file>   (from pdpa-sg-clj submodule)
     Or manually remove PII from the post.

The build never runs. The git push never happens. The PII never leaves my machine.

The fix is one of: - Redact itbb redact draft-post.md replaces the NRIC with [REDACTED_NRIC] and creates a .redact.bak backup - Replace it — substitute a fake NRIC like S0000000Z (structurally valid but checksum-fails — the scanner ignores it) - Remove it — just delete the line


The 11-obligation checklist

The scanner handles obligation #5 (Protection — prevent unauthorized disclosure). But pdpa-sg-clj also ships a full 11-obligation checklist covering every requirement of the Singapore Personal Data Protection Act:

# Obligation How my blog addresses it
1 Consent No PII collected — no consent needed
2 Purpose Limitation Contact form states purpose explicitly
3 Notification Privacy policy published at /privacy
4 Accuracy Static content, reviewed before publish
5 Protection PDPA scanner in pipeline — this post
6 Retention Limitation No user data stored (static site)

Cross-border and governance obligations:

# Obligation How my blog addresses it
7 Transfer Limitation (§26) Cloudflare Pages serves globally; no user data transferred
8 Access & Correction Contact DPO for any concerns
9 Withdrawal of Consent No consent collected — no withdrawal needed
10 Data Breach Notification 3-day PDPC window; monitoring via Cloudflare analytics
11 Accountability / DPO DPO contact published, internal audit trail

Most obligations are design-satisfied. For a static blog that doesn’t collect user data, most obligations are satisfied by design. The scanner covers the one vector where data could leak: my own writing.


The bottom line

The PDPA scan adds 0.2 seconds to my publish pipeline and prevents an entire class of compliance failures. It costs nothing to run. It blocks publishing when it finds something. It works because of three decisions:

  1. Run on single post during publish — fast enough to be in the critical path
  2. Block only on critical/high — doesn’t create noise from false positives
  3. Submodule, not library — scanner rules stay current with the law without release cycles

If you publish content in Singapore, the same pattern works for any static site generator. The scanner is a Babashka script that reads text files — it doesn’t care what generated them.


Built with pdpa-sg-clj v0.2.0 (MIT), Babashka 1.4, ripgrep. Singapore PDPA rules verified against 21 June 2026 amendments. Not legal advice — consult your DPO.