git-filter-repo: limit a history scrub to your own commits with negative refs, or it rewrites upstream too
To scrub leaked strings from a fork without changing upstream commit SHAs:
1. Compute the rewrite set: the leaky commits plus all their descendants.
2. Find the boundary parents just outside that set.
3. Run filter-repo with --refs <every ref pointing into the set> ^<boundary1> ^<boundary2> ....
Without the negative refs, filter-repo re-exports everything reachable from those refs, including thousands of upstream commits. GPG-signed upstream commits lose their signatures and change SHA, which breaks merging with upstream. Verify afterwards that the merge-base with upstream/main is unchanged and every upstream ref still has its old SHA.
Gotchas:
(1) --replace-text skips binary blobs. Compiled artifacts such as .wasm or .so can embed build paths (e.g. /tmp/<user>/...), so scan binaries with strings and patch them in a blob callback with a same-length replacement, then validate the binary still loads.
(2) The rewrite is deterministic: running the same filter on a bundle of local branches gives shared commits the same new SHAs as the remote rewrite, so local branches can be reset without a manual rebase.
(3) Push with --force-with-lease=<ref>:<old-sha> per ref, and push CI-triggering branches last.
(4) Old SHAs stay readable on GitHub until GC, and across the fork network's shared storage. Full removal needs a GitHub Support sensitive-data request.
Follow-up for rewriting local or unpushed branches after the remote rewrite: don't re-run the same filter over remote plus local commits together. filter-repo also translates abbreviated SHAs in messages, so a shared commit whose message cites a local-only commit gets a different SHA than the remote rewrite gave it. Instead, export only the local-only commits with ^boundary negatives, and use --commit-callback to map the excluded parents through the first run's commit-map: commit.parents = [M.get(p,p) if isinstance(p,bytes) else p for p in commit.parents]. fast-export emits the diff against the old parent and fast-import applies it on the new one, so this is a rebase. Add --prune-empty never so commit counts don't change. Simulated on 581 commits: trees, authors and parents were identical to the single-pass result. Also: re-running with --sensitive-data-removal (plus --no-fetch on a full mirror) gave a byte-identical commit-map, and filter-repo/first-changed-commits is what GitHub Support asks for.