GDPRLint
Technical privacy and security guardrail for coding agents, enforced at the Git commit boundary.
GDPR Lint for the commit boundary — block secrets, PII leaks, and dangerous patterns before they enter history.
GDPRLint is a technical privacy and security guardrail. It does not certify legal or GDPR compliance.
Introduction
Coding agents (OpenAI Codex, Claude Code, OpenCode, Cursor, and others) modify repositories at high velocity. GDPRLint inspects staged Git changes before they enter history and can block commits that introduce secrets, personal data exposures, or basic dangerous code patterns.
Coding Agent
│ modifies files
▼
git add .
│
▼
Git staged changes
│
▼
GDPRLint (pre-commit)
├── Secrets / credentials
├── PII / personal data
└── Dangerous code patterns
│ (+ optional Laya local decisor gate)
▼
ALLOW / BLOCK
Screenshots
Why it exists
- Agent-agnostic: the only integration point is Git. Any tool that changes a repo can use it.
- Privacy by design: analysis runs locally. No code, PII, or secrets are sent to external APIs. Reports redact sensitive values.
- Fail safely: high-confidence secrets block by default; false positives are configurable.
- Open source: Apache-2.0, auditable, extensible rules.
What it detects (v0.2)
| Category | Examples | Default action |
|---|---|---|
| Secrets | AWS keys, GitHub/OpenAI/Google/Slack/Stripe tokens, private keys, JWT, connection strings, hardcoded passwords/API keys, credentials in URL query, weak compose/env passwords | block (high confidence); SECRET.HARDCODED_WEAK_PW → warn |
| PII | email (placeholder domains ignored), phone (context required; lockfiles skipped), IBAN (ES + international), Spanish DNI/NIE, credit cards (Luhn), SSN/NHS/CPF with context, public IPs, MAC, GPS, DOB, license plates, URL/query PII (incl. password/username params), logging PII (real interpolation only), special-category signals | warn |
| Security | eval/exec, os.system, shell=True, child_process.exec, dangerouslySetInnerHTML, SQL concat, pickle/unsafe YAML, TLS verify off, CORS * (cors() / origin: "*"), weak RNG for secrets, credential logging, password/token in localStorage |
warn; SECURITY.LOG_CREDENTIAL + SECURITY.LOCALSTORAGE_SECRET → block |
PII confidence tiers: possible · likely · high. Names are not detected (too many false positives).
Installation
Requires Python ≥ 3.10 and Git.
pip install gdprlint
From source (development):
git clone <this-repo>
cd gdprlint
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
gdprlint --version
First Laya load: the decisor uses local weights from Hugging Face (
pipdependencylaya). The first predict may download a checkpoint (~hundreds of MB). If the network or model is unavailable, GDPRLint falls back to rules-only mode and still blocks high-confidence secrets.
As a pre-commit hook
With the pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pablorivast/GDPRLint
rev: v0.3.1
hooks:
- id: gdprlint
Or manage the hook yourself: gdprlint init && gdprlint install.
Usage
# In your project repository
gdprlint init # optional: write a starter .gdprlint.json
gdprlint list-rules # inspect the 64 built-in rules and their actions
gdprlint install # writes .git/hooks/pre-commit (idempotent)
git add .
git commit -m "my change"
# → GDPRLint runs automatically and may block the commit
Manual scan:
gdprlint scan
gdprlint scan --quiet # only print when the commit would block
gdprlint scan --no-laya # rules-only, skip the model gate
gdprlint scan --config path/to.json # explicit config file
gdprlint scan --all # whole worktree, .gitignore honored (works without git too)
gdprlint scan --history main~3..main # lines added across a git revision range
Machine-readable reports (CI / code scanning):
gdprlint scan --format json # stable JSON schema (gdprlint/report/v1)
gdprlint scan --format sarif # SARIF 2.1.0 (GitHub code scanning)
gdprlint scan --format json --output gdprlint-report.json
gdprlint list-rules --format json # rule catalog (schema gdprlint/rules/v1)
gdprlint install --force # refresh an outdated managed hook block
Exit codes:
| Code | Meaning |
|---|---|
0 |
Commit allowed (including warnings-only) |
1 |
Commit blocked |
2 |
Usage / config / git error |
gdprlint uninstall removes the managed hook block.
Scan modes:
| Mode | Input |
|---|---|
| (default) | added lines of git diff --cached |
--all |
every non-ignored file in the worktree; root and nested .gitignore honored, works without git (binary and >1 MiB files are skipped) |
--history RANGE |
added lines across a revision range, e.g. main~3..main |
--all and --history are mutually exclusive; the selected mode is reported as
mode in the JSON/SARIF report.
--all does not require a git repository: it starts at the nearest ancestor
directory containing .git (or the current directory when there is none) and
walks it with gitignore rules. Outside a repository only --all works; the
default staged mode and --history still exit 2.
How the Git hook works
gdprlint install appends a delimited block to pre-commit (creating the file if needed). Existing hooks are preserved. On each commit:
git diff --cached→ only added lines staged for commit- Scanners produce Findings (evidence already redacted)
- Optional Laya gate (local System 1 decisor) annotates TP/FP, risk, remediation family
- Config resolves
block/warn/off→ ALLOW or BLOCK
Directories like node_modules/ are only touched if you explicitly staged files from them.
Configuration — .gdprlint.json
Create one with gdprlint init (writes a starter file that mirrors the
built-in defaults):
{
"mode": "block",
"min_block_confidence": "high",
"rules": {
"SECRET.*": "block",
"SECRET.HARDCODED_WEAK_PW": "warn",
"PII.*": "warn",
"PII.SPECIAL_CATEGORY": "block",
"SECURITY.*": "warn"
},
"exclude": ["fixtures/sanitized/**"],
"exclude_defaults": true,
"exceptions": [
{ "rule": "PII.EMAIL", "file": "docs/examples.md", "line": 12 }
],
"laya": {
"enabled": true,
"device": "cpu",
"min_confidence": 0.7,
"suppress_fp_below": 0.85,
"preload": false,
"show_gdpr_context": true,
"flag_special_category": true,
"flag_dpia_signal": true
},
"gdpr_context": "informational",
"custom_rules": [
{
"id": "CORP.INTERNAL_TOKEN",
"pattern": "tok_[A-Za-z0-9]{32}",
"category": "secret",
"action": "block",
"severity": "high",
"confidence": "high"
}
],
"plugins": true
}
- mode:
block(enforce) |off(report only, never block) — per-rule tuning is done viarules - min_block_confidence: minimum tier (
possible·likely·high) to block - rules: glob →
block|warn|off(exact ids win over globs; longest glob wins) - exceptions: drop a specific finding (
rule+file[+ optionalline]) - exclude: path globs (added to built-in defaults)
- exclude_defaults:
falsedisables built-in lockfile/min.js excludes - laya.enabled:
false→ rules-only (no model); same effect asscan --no-laya - laya.device:
cpu(default) |cuda - laya.min_confidence / suppress_fp_below: gate thresholds (0–1)
- laya.preload:
true→ load model weights eagerly at startup - laya.show_gdpr_context / flag_special_category / flag_dpia_signal: privacy-review signals
- gdpr_context:
off→ hide article references - custom_rules: project-specific regex rules (see below)
- plugins:
falsedisables third-party scanners from entry points
See examples/.gdprlint.json for a full inventory
of keys, and gdprlint list-rules for every rule id.
Built-in excludes (v0.1.1+): package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock, uv.lock, Cargo.lock, composer.lock, *.min.js, *.min.css.
Blocking credential rules (v0.2.0): SECRET.CREDENTIALS_IN_URL, SECURITY.LOG_CREDENTIAL, SECURITY.LOCALSTORAGE_SECRET.
Custom rules and plugins
Custom rules — add project-specific detections in .gdprlint.json without
touching Python:
{
"custom_rules": [
{
"id": "CORP.INTERNAL_TOKEN",
"pattern": "tok_[A-Za-z0-9]{32}",
"flags": "i",
"category": "secret",
"action": "block",
"severity": "high",
"confidence": "high",
"message": "Corp internal token committed",
"remediation": "Rotate the token and read it from the environment.",
"articles": ["32"]
}
]
}
id(required): uppercaseVENDOR.RULE_NAME; theSECRET./PII./SECURITY.namespaces are reserved for built-inspattern(required): Python regex; invalid patterns fail at load time with exit code 2 ·flagsacceptsi,m,s,x- Defaults:
category: secret,action: warn,severity: medium,confidence: likely - Action precedence: exact
rulesentry → built-in exact → longestrulesglob → the rule's ownaction→ built-in globs →warn - Evidence is redacted like every other finding; custom ids appear in
gdprlint list-rules
Plugins — packages can contribute scanners through the
gdprlint.scanners entry-point group:
# pyproject.toml of the plugin package
[project.entry-points."gdprlint.scanners"]
my_scanner = "my_package.scanner:MyScanner"
MyScanner must subclass gdprlint.scanners.Scanner (or be a factory
returning one). Findings flow through the same exclude/exception/gate
pipeline; a broken plugin is reported on stderr and skipped, never fatal.
Set "plugins": false to disable loading.
Architecture (prepared for growth)
Detector → Finding → LayaAnalyzer (optional gate) → DecisionEngine → ALLOW/BLOCK
│
└── future: richer risk assessment / explanations
scanners/—SecretScanner,PIIScanner,SecurityScanner,CustomRuleScannershare oneFindingschema; third-party scanners load via entry pointsrules.py— central rule catalog (kept in sync with scanners by tests)analyzer.py—LayaAnalyzer(Router, batched typed questions) orRulesOnlyAnalyzerengine.py— orchestration + technical decisions (not legal ones)- MVP works even if Laya weights cannot load (fail-safe fallback)
RGPD / GDPR positioning
GDPRLint maps some patterns to contextual article tags (e.g. Art. 5(1)(f), 32, 33, 9, 25, 35) as risk indicators for humans.
| GDPRLint does | GDPRLint does not |
|---|---|
| Detect technical exposures in staged code | Certify GDPR/RGPD compliance |
| Recommend remediation | Replace a DPO or legal review |
| Flag “privacy review suggested” signals | Issue DPIAs or notify authorities |
| Redact secrets in output | Send findings to external LLM APIs by default |
GDPRLint is a technical privacy and security guardrail. It does not certify legal or GDPR compliance.
Adding a rule
No code needed for a line-based regex: use custom_rules (see
Custom rules and plugins) — it is validated, redacted and listed by
gdprlint list-rules like a built-in.
For a built-in rule:
- Add a pattern +
ruleid insrc/gdprlint/scanners/*.py - Register the id in the catalog
src/gdprlint/rules.py(kept in sync by tests) - Attach
related_articlesonly as contextual tags - Add positive and false-positive tests under
tests/ - Keep evidence redacted via
gdprlint.redact
Rule ID shape: SECRET.* · PII.* · SECURITY.*.
Development
pip install -e ".[dev]"
pytest # full suite + coverage (fails below 78%)
pytest -m "not laya_e2e" # skip tests that load real weights (what CI runs)
pytest -m laya_e2e # optional: run against the real Laya checkpoint
ruff check . # lint
mypy # type check (src/)
See CONTRIBUTING.md for the full workflow, commit style and how to add a rule. Notable changes are tracked in CHANGELOG.md.
Limitations
- Pattern-based, line-oriented — not a full SAST or data-flow analyzer
- Special-category and phone/DNI heuristics can false-positive; confidence tiers + exceptions mitigate
- Laya base checkpoints are not magic: gated by
min_confidence; uncertain findings become review flags, not silent passes - Binary staged files are not content-scanned;
--allalso skips binaries and files over 1 MiB --allhonors.gitignorefiles only — not.git/info/exclude, global git excludes or.ignorefiles- No name detection (deliberate)
- Does not implement multi-jurisdiction privacy law automatically
Contributing
Issues and PRs welcome. Keep the project local-first, deterministic by default, and free of hard dependencies on any single coding agent.
To report a vulnerability, see SECURITY.md — please use private reporting, not public issues.
License
Apache-2.0 — see LICENSE.
Metadata
Release files for gdprlint 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gdprlint-0.3.1.tar.gz | 76.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gdprlint-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 133.6 kB
Release files / gdprlint-0.3.1.tar.gz
| Download URL | gdprlint-0.3.1.tar.gz |
|---|---|
| Size | 76.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8468c6686e9b09d927f1cdc146731442c9763d5b3c6964fa3f10e6c8cb108976
|
|
BLAKE2b-256 checksum How to use checksums |
28ad55563b54965d6aae9c9eacb44ca915233afc261c0b675e97c265f6357095
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / gdprlint-0.3.1-py3-none-any.whl
| Download URL | gdprlint-0.3.1-py3-none-any.whl |
|---|---|
| Size | 57.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
633419a4a2223ab1810b602f3ede64e1309517aca462036ad254f343d8954dfa
|
|
BLAKE2b-256 checksum How to use checksums |
db06f746953cf46f09aaac1a83266d1eaeca4d78cd238602c48a008515133ec6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log