Skip to main content

publish-guard

test

日本語版 / 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

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

publish_guard-0.1.0.tar.gz (17.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

publish_guard-0.1.0-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

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

Hashes for publish_guard-0.1.0.tar.gz
Algorithm Hash digest
SHA256 20675c70fb38ca7ad80945b25ecb6f92d3d47d7a2892e15bd56884db9564d7ba
MD5 833b5d7644473a70b0bac416856ba19f
BLAKE2b-256 f1aa61d4e9f27eb2a66c787fd04c1e3b7741a752968738c67f56fe2e6ce087e7

See more details on using hashes here.

Provenance

The following attestation bundles were made for publish_guard-0.1.0.tar.gz:

Publisher: publish.yml on lon-coeng/publish-guard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for publish_guard-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8985fdc1d70be4f0565ec21a0a04683bc9ff28095dc308699a6ca76d7723f135
MD5 e55e73e305e683b19fe326d599ef7199
BLAKE2b-256 0cd771a82ef71914ac9f9c5215159ce95d38c82346059a00faa1856594dccfd3

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 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