Skip to main content

prepublish-guard

Check a repository for secrets and personal data before you push it or make it public. It checks the current files and the whole git history.

prepublish-guard scan . --history

CI deps python

Why

Most leaks are accidents. Someone hard-codes a token while testing, puts a CSV of clients in data/, or leaves a notebook output showing C:\Users\<name>\.... Deleting the line in a later commit does not fix it: the value stays in the git history, and anyone who clones the repository can read it.

This tool was built after a portfolio audit found exactly that: a national ID number that had been removed from a README but was still in the history. General-purpose secret scanners miss that kind of finding, because they know little about personal data formats, and even less about Colombian ones (cédula, NIT, mobile numbers).

What it finds

Category Examples Severity
Secrets AWS, GitHub, GitLab, Slack, Google, Stripe, OpenAI, Anthropic, Twilio, SendGrid, npm, PyPI and Telegram tokens; private key blocks (including encrypted ones); passwords inside connection URLs; literal values assigned to password, token, api_key, client_secret… high
Personal data national IDs with context (cédula, C.C., NIT, DNI, SSN, passport); payment cards that pass the Luhn check; CSV/TSV columns of IDs, phones or e-mails high
personal e-mail addresses; Colombian mobile numbers and other phone numbers medium
Local paths /Users/<name>/, /home/<name>/, C:\Users\<name>\ with a real user name low
Confidential terms employer, client or project names you list in the config medium
Sensitive files id_rsa, credentials.json, .env, key stores, .npmrc/.pypirc, database dumps low–high
Hygiene no LICENSE file low

It is precise first. A tool that cries wolf gets switched off. So it ignores placeholders (<your-key>, ${VAR}, changeme, AKIAIOSFODNN7EXAMPLE), example, test and noreply e-mail domains, environment lookups (os.environ[...], ${{ secrets.X }}), documented test card numbers, the fictional 555-01xx phone range, generic home folders (/home/user, /home/runner) and its own config file.

Guarantees

  • It never prints what it finds.
    • Personal data and local paths appear only as their type and length ([hidden, 28 chars]).
    • Secrets show at most their first two characters, usually the vendor prefix (gh************ (40 chars)).
    • The fingerprint that identifies a finding (for baselines and code scanning) is derived with scrypt, a deliberately slow, memory-hard function. Recovering an ID or phone number from it by trying candidates would take years on one core.
    • You can paste a report into an issue or a CI log without leaking the values again.
  • It is read-only and offline. It modifies nothing, sends nothing and never rewrites history. When something is only in the history, it explains what to do (rotate first, then git filter-repo), and leaves the decision to you.
  • It has no dependencies. It uses the Python 3.11+ standard library and git.

Quick start

pip install "git+https://github.com/s-osorio-gom/prepublish-guard@v0.1.1"   # or: pipx install .
prepublish-guard scan .                      # files that a push would publish (tracked files)
prepublish-guard scan . --history            # ...and every line ever added, on every branch and tag
prepublish-guard scan . --format sarif --output results.sarif   # for GitHub code scanning
prepublish-guard rules                       # what is detected

Exit codes:

  • 0: nothing at or above --fail-on (default medium).
  • 1: findings at or above it.
  • 2: a usage or configuration error.

Report formats: text, markdown (for a PR comment), json and sarif (SARIF 2.1.0).

Example

examples/demo.sh builds a throwaway repository with typical mistakes (every value is fabricated), and then "fixes" them in a second commit. The working-tree scan looks clean:

prepublish-guard 0.1.1: 0 finding(s) [high 0, medium 0, low 0] in 3 file(s)

The history scan shows that nothing was actually removed:

prepublish-guard 0.1.1: 7 finding(s) [high 5, medium 1, low 1] in 3 file(s) and 2 commit(s)

== secret (3)
  [HIGH  ] HISTORY settings.py:1 (first added in 68dd34f53d)  url_with_password: password embedded in a connection URL  -> Su********* (11 chars)  #c9c46fe878bed13f
  [HIGH  ] HISTORY settings.py:2 (first added in 68dd34f53d)  github_token: GitHub token committed in plain text  -> gh************ (40 chars)  #bb89af0ac19d744a
  [HIGH  ] HISTORY settings.py:3 (first added in 68dd34f53d)  aws_access_key_id: AWS access key id committed in plain text  -> AK************ (20 chars)  #72e2ef3d5fdcfa37
  what to do: Revoke or rotate it at the provider first: once pushed, deleting it from the code does not make it safe. ...

== personal data (3)
  [HIGH  ] HISTORY clientes.csv (first added in 68dd34f53d)  pii_column: column `cedula` holds 2 personal value(s)  -> cedula  #2a32f4428b31a0ff
  [HIGH  ] HISTORY clientes.csv (first added in 68dd34f53d)  pii_column: column `celular` holds 2 personal value(s)  -> celular  #71d34175afada5ed
  [MEDIUM] HISTORY README.md:3 (first added in 68dd34f53d)  email_address: personal e-mail address  -> [hidden, 28 chars]  #e42afd2dffdbc515
  what to do: Remove it or replace it with fictional data. ...

== local path (1)
  [LOW   ] HISTORY README.md:2 (first added in 68dd34f53d)  personal_path: absolute path with a personal user name (reveals who and where)  -> [hidden, 16 chars]  #2036ac579c3a4920
  what to do: Replace the absolute path with a relative one, a setting or an environment variable.

Found only in the git history. Removing it from the current files is not enough: (1) rotate it if it is a
credential; (2) rewrite the history, e.g. `git filter-repo --replace-text replacements.txt` ...

A value repeated in many places counts as one finding, listed with all its lines. For example, a local path that appears in 105 notebook cells is reported once, not 105 times.

Use it before every commit, and in CI

pre-commit. Add this to .pre-commit-config.yaml:

repos:
  - repo: https://github.com/s-osorio-gom/prepublish-guard
    rev: v0.1.1
    hooks:
      - id: prepublish-guard

The hook scans the whole working copy, including untracked files that are not git-ignored, so a stray file with a token blocks the commit even if you did not stage it.

GitHub Actions. The results appear in the repository's Security → Code scanning tab:

- uses: actions/checkout@v5
  with: { fetch-depth: 0 }          # full history
- uses: s-osorio-gom/prepublish-guard@v0.1.1
  with: { history: "true" }

A complete workflow is in examples/github-workflow.yml. Uploading to code scanning needs permissions: security-events: write, and on private repositories GitHub Advanced Security; without it, pass upload-sarif: "false" and the job still fails on findings.

Configuration

.prepublish-guard.toml at the repository root is optional:

confidential_terms = ["Acme Bank", "Project Falcon"]   # names that must never be published
allowed_email_domains = ["mycompany.com"]              # e-mails you publish on purpose
allow_paths = ["tests/fixtures/*"]                     # glob patterns to skip
allow_regex = ["support@mycompany\\.com"]              # matches (or whole lines) never reported
fail_on = "medium"                                     # low | medium | high | never

prepublish-guard baseline . --history accepts the current findings. It writes only their fingerprints to ignore, never the values. After that, only new findings fail the check. Accepting a CSV column finding accepts the values the column holds now: a new value in that column reports the column again.

How well it works

Labelled evaluation. The tool was evaluated on 24 hand-labelled publication bundles: dev 6, validation 10, test 8. The cases include real-looking leaks and tricky negatives (placeholders, example addresses, relative paths, generic homes). They live outside this repository, but they were visible while the tool was developed, so the figures below are agreement on a known suite, not a held-out estimate (details).

Issue type Precision Recall
secrets 1.00 1.00 (4/4)
personal data 1.00 0.83 (5/6)
local paths 1.00 1.00 (4/4)
confidential terms 1.00 1.00 (3/3)
missing license 1.00 1.00 (3/3)
overall 1.00 0.95 (19/20)

The only miss was a phone number in the fictional 555-01xx range. The suite used it to stand for a real number; the tool ignores that range on purpose, because such numbers are never real. Details: docs/EVALUATION.md.

On a real repository. On the author's own portfolio repository (98 files, 7 commits) it reported 26 findings, which the author then cleaned from the files and the history:

  • a national ID that survives only in the git history, with the commit that added it;
  • e-mail addresses in exported datasets and notebook outputs;
  • a local user path repeated in 105 notebook cells (reported as 4 findings, one per notebook);
  • the missing license.

Limits

  • It uses pattern matching, not live verification. It does not check whether a token still works. For exhaustive secret coverage, also run a dedicated scanner (gitleaks, TruffleHog). This tool's strengths are personal data, publication hygiene and reports you can share.
  • It does not read images, PDFs, spreadsheets or other binaries. Their file names are still checked.
  • Text files larger than max_file_bytes (default 2 MB) are scanned up to that size, and the report says so.
  • A shallow clone has an incomplete history. The report says so; in CI use actions/checkout with fetch-depth: 0.
  • In a table already reported for its personal columns (pii_column), the individual values of those columns are not listed one by one. Accepting a column accepts the values it holds now; removing values from it also changes its fingerprint, so take the baseline again after cleaning a table.
  • In history, rows added to a CSV in a commit that does not touch its header line are checked line by line only. A bare ID number (no "cédula" or similar context) added that way and later removed is not reported; the pre-commit hook catches it when it is added. Planned for 0.1.2.
  • On very large histories, --max-commits N limits the scan to the newest commits.
  • E-mails in datasets you publish on purpose need allowed_email_domains, allow_paths or a baseline.

Development

pip install -e ".[dev]"
pytest -q            # 72 tests
ruff check .
bash examples/demo.sh /tmp/pg-demo

The project has no runtime dependencies. Each detector has positive cases and look-alike negative cases in tests/test_detectors.py, and end-to-end tests on real temporary git repositories are in tests/test_scan_and_cli.py. Those tests also assert that no report contains a raw value. tests/test_release_review.py reproduces each finding of the independent pre-release review.

En español (resumen)

prepublish-guard revisa un repositorio antes de publicarlo. Revisa los archivos y todo el historial de git, y busca:

  • credenciales;
  • datos personales: cédulas, NIT, celulares colombianos, correos, tarjetas y columnas de datos personales en CSV;
  • rutas locales con tu usuario;
  • nombres confidenciales;
  • la falta de licencia.

Nunca muestra los valores encontrados: los datos personales aparecen solo como tipo y longitud. No modifica nada y no envía nada. Cuando algo solo está en el historial, explica cómo revocarlo y limpiarlo.

License

MIT (see LICENSE).

Metadata

Release files for prepublish-guard 0.1.1

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

Source distribution (sdist)

Source distribution for prepublish-guard 0.1.1
File Size Uploaded
prepublish_guard-0.1.1.tar.gz 39.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for prepublish-guard 0.1.1
File Interpreter ABI Platform
prepublish_guard-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 70.4 kB

Release files / prepublish_guard-0.1.1.tar.gz

Download URL prepublish_guard-0.1.1.tar.gz
Size 39.8 kB
Tags Source
SHA-256 checksum
How to use checksums
d61e624c55057b4929fc483a97bd3fb9f203cbc0c692b58db3548355f1910c7c
BLAKE2b-256 checksum
How to use checksums
ab10aebd07efd73b4a0dcd84d30c6d28ad252034fc9504b5f53057138e4a5efb
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 11, 2026.

Transparency log

Release files / prepublish_guard-0.1.1-py3-none-any.whl

Download URL prepublish_guard-0.1.1-py3-none-any.whl
Size 30.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4e5a62a87355585d8d0b5dfbc10de265d1fb15fe9c7b292e453f0c8582d843f0
BLAKE2b-256 checksum
How to use checksums
4a6965a1ab134b5eb3596cafe7eb54aa04b90d449854e8324d5f6397b967a11a
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 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

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