publish-guard
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
Python 3.11 or later. No dependencies.
git clone https://github.com/lon-coeng/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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file publish_guard-0.1.0.tar.gz.
File metadata
- Download URL: publish_guard-0.1.0.tar.gz
- Upload date:
- Size: 17.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20675c70fb38ca7ad80945b25ecb6f92d3d47d7a2892e15bd56884db9564d7ba
|
|
| MD5 |
833b5d7644473a70b0bac416856ba19f
|
|
| BLAKE2b-256 |
f1aa61d4e9f27eb2a66c787fd04c1e3b7741a752968738c67f56fe2e6ce087e7
|
Provenance
The following attestation bundles were made for publish_guard-0.1.0.tar.gz:
Publisher:
publish.yml on lon-coeng/publish-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
publish_guard-0.1.0.tar.gz -
Subject digest:
20675c70fb38ca7ad80945b25ecb6f92d3d47d7a2892e15bd56884db9564d7ba - Sigstore transparency entry: 2683433456
- Sigstore integration time:
-
Permalink:
lon-coeng/publish-guard@292a65d423b84b95397d10cfc2a00fa15066802a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/lon-coeng
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@292a65d423b84b95397d10cfc2a00fa15066802a -
Trigger Event:
push
-
Statement type:
File details
Details for the file publish_guard-0.1.0-py3-none-any.whl.
File metadata
- Download URL: publish_guard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8985fdc1d70be4f0565ec21a0a04683bc9ff28095dc308699a6ca76d7723f135
|
|
| MD5 |
e55e73e305e683b19fe326d599ef7199
|
|
| BLAKE2b-256 |
0cd771a82ef71914ac9f9c5215159ce95d38c82346059a00faa1856594dccfd3
|
Provenance
The following attestation bundles were made for publish_guard-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on lon-coeng/publish-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
publish_guard-0.1.0-py3-none-any.whl -
Subject digest:
8985fdc1d70be4f0565ec21a0a04683bc9ff28095dc308699a6ca76d7723f135 - Sigstore transparency entry: 2683433477
- Sigstore integration time:
-
Permalink:
lon-coeng/publish-guard@292a65d423b84b95397d10cfc2a00fa15066802a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/lon-coeng
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@292a65d423b84b95397d10cfc2a00fa15066802a -
Trigger Event:
push
-
Statement type: