GDPR / CCPA / HIPAA pre-generation gate
gdpr-gate is an advisory code-level gate that fires inline during /soleur:plan Phase 2.7 and at /soleur:work Phase 2 exit. It scans plan prose, schema migrations, and diffs for regulated-data gaps under GDPR (Articles 5/6/9/17/20/25/30/32/33/35), with secondary coverage for CCPA / CPRA and HIPAA. It never blocks. Critical findings (Article 9 special-category data) prompt operator acknowledgment + GitHub issue creation; the gate never auto-writes to compliance-posture.md.
This is not legal review. Output is a heuristic, machine-generated checklist meant to compress regulator-shaped surprises out of the normal feature-design loop. Consult the clo agent and legal-compliance-auditor before merging anything load-bearing.
When to invoke
- Plan time — automatic at
/soleur:planPhase 2.7 when the plan touches regulated-data surfaces (perhr-gdpr-gate-on-regulated-data-surfacescanonical regex). - Work time — automatic at end of
/soleur:workPhase 2, single pass after all per-task RED/GREEN/REFACTOR loops complete (token budget ≤4k per invocation). - Manual —
/soleur:gdpr-gate "<scope>"where<scope>is a file path, glob, or one-line description. - Manual repo-scan —
/soleur:gdpr-gate --repo-scanruns the gate against the whole working tree (operator-initiated only). Defenses, batching, and output contract documented in## --repo-scan modebelow. - Hook (advisory only) —
lefthook.ymlgdpr-gate-advisoryprints a stderr breadcrumb when staged paths match the canonical regex. The hook always exits 0 — it never blocks the commit. Blocking enforcement lives in/soleur:shipPhase 5.5 (post-PR).
The gate is read-only with respect to the canonical /soleur:plan template. It audits the plan; it never injects its own checklist into the plan body. Architectural invariant per ADR-026.
Disclaimer (always first)
Every gate output begins, as the first non-blank line, with the literal:
**This is not legal review. Findings are heuristic. Consult `clo` + `legal-compliance-auditor` before merging.**
The disclaimer is hardcoded. A test in plugins/soleur/test/gdpr-gate.test.ts asserts the literal appears as the first non-blank line of every fixture output.
--repo-scan mode
/soleur:gdpr-gate --repo-scan runs the gate against the whole working tree. Operator-initiated only — never auto-fires from /soleur:plan, /soleur:work, or lefthook. Token budget remains ≤4k per Haiku call (ADR-026 TR3) via 25-files-per-batch fan-out.
Sole-arg sentinel. When invoked with $ARGUMENTS, trim leading/trailing whitespace (including \t and \n). If the trimmed value equals exactly --repo-scan (no spaces, no quotes, no additional tokens), enter repo-scan mode. Any other value — including --repo-scan apps/web-platform, repo scan, or "--repo-scan section of the repo" — falls through to the v1 scope-string mode and is forwarded verbatim to the prompt.
File source. git ls-files -c -o --exclude-standard (cached + untracked, respecting .gitignore). Submodules excluded; symlinks not followed. Pre-filtered through the canonical regex (see ## Path globs (canonical) below) so only regulated-data paths enter the candidate pool.
Deny-list (D1). Single source of truth: [scripts/path-denylist.txt](./scripts/path-denylist.txt). Line-oriented file with extended-regex patterns (one per line; # for comments). Bash [[ =~ ]] semantics. Any candidate path matching at least one deny-list pattern is excluded from the scan corpus and a # blocked: <path> line is emitted to stderr (audit trail).
Allow-list bypass (D3). GDPR_GATE_REPO_SCAN_ALLOW_PATHS=path1:path2:... — colon-separated literal paths (no globs accepted; bash strict equality after split). Default unset → all deny-list patterns enforced. Each entry must satisfy two clauses or the script exits 1: (1) the path must match at least one deny pattern (otherwise the bypass is meaningless), and (2) the path must exist in git ls-files -c -o --exclude-standard (otherwise the operator probably typo'd the path; see Sharp edges).
CI refusal (D3). If both $CI and $GDPR_GATE_REPO_SCAN_ALLOW_PATHS are set, the script exits 1 with allow-list bypass refused in CI environment. Operator-only by construction — runbooks that set the bypass var get caught at the script boundary, not by docs alone.
Batching. 25 files per Haiku call. The dispatching agent collects per-batch outputs, dedups by (check_id, path, line), and summarises inline.
Inline-only output (D4). Findings emitted to stdout / conversation only. The repo-scan walker NEVER writes to compliance-posture.md, __goldens__/, test/fixtures/, or any disk path under ~/.claude/. The v1 critical-finding flow (operator-acknowledged write to compliance-posture.md Active Items + GitHub compliance/critical label) still applies for Art. 9 column-name matches discovered during repo-scan.
Schema-only invariant (D5). The repo-scan prompt template inherits v1's DO NOT INCLUDE COLUMN VALUES directive verbatim. The directive is the load-bearing assertion in plugins/soleur/test/gdpr-gate.test.ts.
Canonical-regex source-of-truth. [scripts/repo-scan.sh](./scripts/repo-scan.sh) extracts the canonical regex from this file's ## Path globs (canonical) heading via awk at runtime — it does NOT redefine the regex. Editors of SKILL.md must keep that heading and the first fenced regex line stable; if extraction fails the script exits 1 with canonical regex not found in SKILL.md. Asserted by AC-PARITY-1 and gdpr-gate-repo-scan.test.ts.
Path globs (canonical)
The single source of truth for "what counts as a regulated-data path" is the regex below. lefthook.yml mirrors it verbatim; the skill's prompt template references it; AGENTS.md hr-gdpr-gate-on-regulated-data-surfaces cites it.
^(apps/web-platform/supabase/migrations/|apps/web-platform/lib/auth/|apps/web-platform/server/.*auth.*\.(ts|tsx|js)|apps/web-platform/app/api/.*\.(ts|tsx)$|.*\.sql$)
Each component matches at least one file in the current repo (verified at plan time, 2026-05-10). forms/ and /*.prisma from earlier drafts are dropped — both matched zero paths in this repo and a glob that matches nothing is a workflow violation per hr-when-a-plan-specifies-relative-paths-e-g.
5 mandatory v1 checks (FR4)
Each check has a stable check_id for cross-referencing in compliance-posture.md and GitHub issue labels.
| check_id | Article | Trigger | Default severity | |---|---|---|---| | GDPR-Art-6 | Art. 6 lawful basis | New schema column without -- LAWFUL_BASIS: <basis> annotation | Important | | GDPR-Art-5e | Art. 5(1)(e) retention | New PII table without retention metadata | Important | | GDPR-Art-17 | Art. 17 erasure | New FK to users without ON DELETE CASCADE or anonymisation migration | Important | | GDPR-Art-17-caller | Art. 17 erasure (caller-site) | Diff adds an ON DELETE RESTRICT FK to public.users AND an anonymise_* RPC, AND git grep -n '<rpc_name>' returns zero matches outside supabase/migrations/, knowledge-base/, and plan files | Important (see [[2026-05-16-migration-mandates-must-have-wired-call-sites-in-same-pr]]) | | GDPR-Chapter-V | Art. 44–49 cross-border | New non-EEA vendor env var / SDK without compliance-posture.md Vendor DPA row | Important | | GDPR-Art-9 | Art. 9 special-category | New column matching the Art. 9 list in [fields.md](./references/fields.md) | Critical |
Critical is reserved for Art. 9 column-name matches in v1. Demoting the other four to Important keeps the noise floor low and preserves compliance/critical issue labels as a load-bearing signal. See [non-negotiables.md](./references/non-negotiables.md) for the regulatory rationale.
Reference layers:
Active layers (with check_id markers):
- [layers/api-layer.md](./refere