Skip to main content

publish-guard

test PyPI

日本語版 / Japanese version

Find what you forgot to remove before you publish a repository — including the parts that are only in the history.

Built after publishing two repositories derived from client work, and discovering three separate ways the sanitization had failed. Each of those is now a feature.


The problem

You built something under contract. You want to show it. So you replace the client's name, the credentials, the resource IDs — and publish.

Three things go wrong, and none of them are visible when you look at the files.

Editing a file does not remove what it contained. Every earlier commit still holds the original. git log -p prints it. Anyone who clones gets it.

The same value appears in more than one form. A Japanese sheet name is percent-encoded inside a URL. A filename has its dots escaped inside a regex literal. An organisation name shows up as ACME, Acme and acme. Non-ASCII text written by json.dumps becomes \uXXXX and is invisible to the eye. Replacing the plain string leaves every one of these behind.

git push --force does not delete anything on GitHub. The commits become unreachable, but the objects stay, and a direct SHA still fetches them — while the Actions run history publishes those SHAs for anyone to read.

What this does

publish-guard scan   <repo>                    surface candidates you may have missed
publish-guard verify <repo> -c <config.toml>   confirm the terms you chose are really gone

verify walks every blob in every commit, not the working tree. It expands each term into the forms it might actually appear as. It checks commit messages and author metadata too. It exits 1 when anything is found, so it works in CI.

scan does not decide anything. Whether 売上高 is a client-specific metric or a generic word is a judgement only someone who knows the project can make. What a machine does better is spotting a 40-character opaque identifier or a username buried in a home directory path — the things that survive a careful read.

Install

pip install publish-guard

Python 3.11 or later. No dependencies — tomllib landed in the standard library at 3.11.

publish-guard scan /path/to/repo

To run it straight from a clone instead:

git clone https://github.com/loncoeng/publish-guard.git
cd publish-guard
python -m publish_guard.cli --help

Use

1. Find candidates.

python -m publish_guard.cli scan /path/to/repo
[opaque-id] identifiers 20 characters or longer
  1Kx9mQ2vTpL7rB4nW8sJfD6yHcE3aZgUo
    config/settings.json, docs/setup.md

[home-path] home directory paths, often containing a username
  /home/acme-operator  (history only)
    systemd/worker.service

2. Decide what to remove, and record the decision.

# publish-guard.toml
[forbidden]
terms = [
  "AcmeCorp",
  "1Kx9mQ2vTpL7rB4nW8sJfD6yHcE3aZgUo",
  "/home/acme-operator",
  "売上高",
]

Write the plain string. The other forms are derived for you.

3. Remove them however you like, then confirm.

python -m publish_guard.cli verify /path/to/repo -c publish-guard.toml
scanned: 42 commits / 310 blobs / 4 terms

found: 3 results / 1 term

  売上高
    %E5%A3%B2%E4%B8%8A%E9%AB%98  ← other form
      test/import-verification.test.js  (still present)

Some results are history only. Editing the file will not remove them.
git push --force does not remove the objects on GitHub either.
To be certain, recreate the repository.

In CI

- name: publish-guard
  run: python -m publish_guard.cli verify . -c publish-guard.toml

The point is not to catch it once. It is to keep catching it after you have stopped thinking about it.

What it does not do

It does not replace anything. Removal is a judgement call, and a tool that guessed would guess wrong in the direction of leaking. Remove things your own way — this confirms the result.

It cannot tell you what is sensitive. scan offers candidates by shape. Only you know which of them matter.

verify has no exclusions. --exclude belongs to scan, where leaving a file out costs you nothing but the quality of a suggestion. verify is the gate that stops a publish, and a gate with a bypass is not a gate. Lockfiles are no exception there: their contents are mostly checksums, but a private registry package name like @acmecorp/internal-ui, or an internal mirror URL, lands in them — as ways a client gets identified go, that one is fairly typical.

It cannot clean history for you. If something is only in the history, no command here fixes that. Recreating the repository is the reliable answer, and verify tells you when you need to.

Options

--no-history Scan only the HEAD tree. Fast, and misses exactly what this tool exists to find. For a pre-commit look, not a pre-publish check.
--current-branch Walk only the current branch's history instead of every ref. Note this still walks ancestors — it is about refs, not depth.
--limit N How many candidates to print per category in scan (default 20).
--exclude GLOB Leave a path out of scan. Repeatable. Matched against the file name as well as the full path, so package-lock.json also covers web/package-lock.json.
--include-lockfiles Scan dependency lockfiles too. They are left out by default.

Exit codes

0 Nothing found
1 Something found
2 Not a git repository, or a bad configuration

Tests

python -m unittest discover -s tests -t .

No dependencies required. The tests build real git repositories in a temporary directory and verify the behaviour that matters: that a value removed by a later commit is still found, and that every derived form is caught.

A note from writing this

The first version of this README used real values as examples — a real resource identifier, a real internal term — on the reasoning that examples from experience beat invented ones. They went into the README and the tests of a tool built to find exactly that. I caught it and rebuilt the repository before anyone had cloned it.

The part worth writing down comes after. scan had already flagged one of them. It printed the identifier as a candidate. I read that output and decided it was fine, because it was "just a test fixture."

The tool worked. The person reading its output was the one who got it wrong. That is why verify exits non-zero and belongs in CI — not because you will forget, but because you will see the warning and talk yourself out of it.

Later, running scan over a JavaScript repository before publishing it returned 197 candidates. 155 of them were integrity hashes out of package-lock.json. I did not read the list; I audited that repository by hand instead. A finding you cannot find is not much better than a miss. That is when lockfiles became a default exclusion. The same repository now reports one candidate, and it is one worth a decision.

Whatever gets excluded is printed back with a count and the file names. Skipping quietly would only trade one problem for another: believing you looked at everything.

License

MIT. See LICENSE.

Metadata

Release files for publish-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 publish-guard 0.1.1
File Size Uploaded
publish_guard-0.1.1.tar.gz 18.2 kB Details

Built distribution (wheel)

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

Total release size: 36.8 kB

Release files / publish_guard-0.1.1.tar.gz

Download URL publish_guard-0.1.1.tar.gz
Size 18.2 kB
Tags Source
SHA-256 checksum
How to use checksums
2b315a5b7dd4dba5051fc4f304cc3a2ecad9d8a4e948cfa9416604a83a93dc88
BLAKE2b-256 checksum
How to use checksums
6534648971d641bc8d9fc2f180173f67c23862b2b469ac7bcaf6b892b2ea2f76
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 Sep 23, 2026.

Transparency log

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

Download URL publish_guard-0.1.1-py3-none-any.whl
Size 18.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b9962a4a79ab47bccf78bb9e3368d92923674a188577300266d74714730481e6
BLAKE2b-256 checksum
How to use checksums
34f54e5aa7a571b379695a70e1ceee825c4878dd02beaf81613a77307c91adf7
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 Sep 23, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

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