Skip to main content

GDPRLint logo

GDPRLint

Technical privacy and security guardrail for coding agents, enforced at the Git commit boundary.

version ci license python tests status local-first

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

GDPRLint scan output

GDPRLint blocked commit

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.4)

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 (pip dependency laya). 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.4.0
    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 explain PII.EMAIL   # what a rule detects and how to suppress it
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
gdprlint scan --all --update-baseline # accept current findings (ratchet baseline)
gdprlint scan --all --no-baseline     # ignore the baseline for this run

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 explain PII.* --format json   # rule docs (schema gdprlint/rule/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:

  1. git diff --cached → only added lines staged for commit
  2. Scanners produce Findings (evidence already redacted)
  3. Excludes, config exceptions, inline gdprlint:ignore directives and the baseline drop ignored/accepted findings (each counted in the report)
  4. Optional Laya gate (local System 1 decisor) annotates TP/FP, risk, remediation family
  5. 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,
  "inline_suppressions": true,
  "baseline": ".gdprlint-baseline.json"
}
  • mode: block (enforce) | off (report only, never block) — per-rule tuning is done via rules
  • 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 [+ optional line])
  • exclude: path globs (added to built-in defaults)
  • exclude_defaults: false disables built-in lockfile/min.js excludes
  • laya.enabled: false → rules-only (no model); same effect as scan --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: false disables third-party scanners from entry points
  • inline_suppressions: false disables gdprlint:ignore directives
  • baseline: path to the accepted-findings snapshot (default .gdprlint-baseline.json next to the config); false disables it

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.

Suppressions — inline directives and baseline

Two explicit, auditable escape hatches keep adoption practical without turning the guardrail off. Both are opt-in: no directive or baseline file means nothing changes.

Inline gdprlint:ignore directives

Suppress a specific finding from a comment on the same line, or on the line above (for formats where inline comments are awkward):

token = os.environ["LEGACY"]  # gdprlint:ignore SECRET.API_KEY
# gdprlint:ignore PII.*
query = load_legacy_email()
  • The marker must sit inside a comment (#, //, --, ;, <!--…), so prose that merely mentions it is ignored.
  • Rule lists accept globs: gdprlint:ignore PII.*, SECURITY.EVAL. A bare gdprlint:ignore (or gdprlint:ignore-all) suppresses all rules.
  • In staged mode the directive must be staged together with the code it suppresses (it is read from the scanned input, not from disk).
  • Directives that match nothing are reported as unused so stale suppressions do not accumulate silently.
  • Disable with "inline_suppressions": false.

Baseline (ratchet for legacy repositories)

Snapshot the findings you have already accepted so only new ones block:

gdprlint scan --all --update-baseline   # accept the current state
gdprlint scan                           # only new findings can block
gdprlint scan --all --no-baseline       # see everything, once
  • The snapshot lives in .gdprlint-baseline.json (auto-detected next to the config file; change with "baseline": "path" or disable with false).
  • Entries are fingerprints (rule + file + redacted evidence, no line numbers), so moving code does not invalidate the baseline while changing a secret does.
  • --update-baseline merges — a narrow scan never wipes a broad snapshot. To reset, delete the file.
  • Matched findings are dropped from the report and counted as baseline_suppressed (JSON) / shown as a summary line (text).

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): uppercase VENDOR.RULE_NAME; the SECRET. / PII. / SECURITY. namespaces are reserved for built-ins
  • pattern (required): Python regex; invalid patterns fail at load time with exit code 2 · flags accepts i, m, s, x
  • Defaults: category: secret, action: warn, severity: medium, confidence: likely
  • Action precedence: exact rules entry → built-in exact → longest rules glob → the rule's own action → 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 → suppressions (excludes · exceptions · inline · baseline)
                        → LayaAnalyzer (optional gate) → DecisionEngine → ALLOW/BLOCK
                                 │
                                 └── future: richer risk assessment / explanations
  • scanners/ — SecretScanner, PIIScanner, SecurityScanner, CustomRuleScanner share one Finding schema; third-party scanners load via entry points
  • rules.py — central rule catalog (kept in sync with scanners by tests) + RuleDoc docs for gdprlint explain
  • suppress.py / baseline.py — inline directives and the accepted-findings snapshot
  • analyzer.py — LayaAnalyzer (Router, batched typed questions) or RulesOnlyAnalyzer
  • engine.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, with docs via gdprlint explain VENDOR.RULE_NAME.

For a built-in rule:

  1. Add a pattern + rule id in src/gdprlint/scanners/*.py
  2. Register the id in the catalog src/gdprlint/rules.py (kept in sync by tests)
  3. Attach related_articles only as contextual tags
  4. Add positive and false-positive tests under tests/
  5. 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 + inline directives 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; --all also skips binaries and files over 1 MiB
  • --all honors .gitignore, .git/info/exclude and core.excludesFile (plus its XDG defaults) — but not .ignore/.rgignore or other non-git ignore files
  • Inline directives are read from the scanned input: in staged mode they must be staged together with the finding they suppress
  • 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.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gdprlint 0.4.0
File Size Uploaded
gdprlint-0.4.0.tar.gz 92.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gdprlint 0.4.0
File Interpreter ABI Platform
gdprlint-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 160.7 kB

Release files / gdprlint-0.4.0.tar.gz

Download URL gdprlint-0.4.0.tar.gz
Size 92.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e0ea278dc34e6654b55c06bce5d5371923a28bb2a63c355ad18b6f1d063a8b41
BLAKE2b-256 checksum
How to use checksums
1f129d6e3b4e4bb8d6b669113454c649ac355b16bf57fd1a6ca5968ca4d4ebdf
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 8, 2026.

Transparency log

Release files / gdprlint-0.4.0-py3-none-any.whl

Download URL gdprlint-0.4.0-py3-none-any.whl
Size 68.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8683fe84951ecae33d05cccb1872c584256b20a771ab745ac2ebf467ef39d15d
BLAKE2b-256 checksum
How to use checksums
a9c4d71e88a032f405bf6810061747d7226b9d465246e3565f08dbde7a33f6f1
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 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page