docs(changelog): say what the hook scans, and name what it does not

The entry promised "close four bypass classes completely", "the whole
reachable history is scanned" and "everything reachable is scanned". The
hook scans the diff of the range it computes, which is the content of the
pushed tip, not each commit along the way, so a credential added and later
removed inside one push is not reported. Four specific paths are closed;
the scan's shape is unchanged, and the entry now has a Known limitation
section saying so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lubos Buracinsky
2026-09-22 19:12:15 +02:00
co-authored by Claude Opus 5
parent 19642b74bf
commit bd073182c6
+10 -6
View File
@@ -2,10 +2,10 @@
## [1.87.7.0] - 2026-09-22 ## [1.87.7.0] - 2026-09-22
**Pre-push credential checks close four bypass classes completely.** **Pre-push credential checks close four ways a push could carry a key out.**
**A remote the hook cannot see is never assumed to already have your history.** **A remote the hook cannot see is never assumed to already have your content.**
This release closes four ways the pre-push credential hook exited 0 on a push that carried a live key. Each is a class rather than a single input: pushing somewhere other than `origin`, a remote tip the local clone does not have, a proximity pattern cut in half by a scan slice, and invisible padding that moved that cut. The gate that ships with it exercises nine shapes across those four classes, each against real git repositories. This release closes four ways the pre-push credential hook exited 0 on a push that carried a live key: pushing somewhere other than `origin`, a remote tip the local clone does not have, a proximity pattern cut in half by a scan slice, and invisible padding that moved that cut. The gate that ships with it exercises nine shapes across those four paths, each against real git repositories. What the hook scans is unchanged: the diff of the range it computes, which is the content of the pushed tip rather than each commit along the way — see the note at the end of this entry.
### The numbers that matter ### The numbers that matter
@@ -22,19 +22,23 @@ The nine fail-open scenarios are the four classes above. Pushing to a remote the
### What this means for developers ### What this means for developers
Your pre-push hook now blocks a credential on its way to a mirror, a staging remote or a push URL, including a credential that `origin` already has but the destination does not. Force-pushes where the local clone is missing the remote's tip are scanned across the whole reachable history instead of a guessed range — slower on that rare path, and correct. A clean first push in a SHA-256 repository is no longer hard-blocked. Your pre-push hook now blocks a credential on its way to a mirror, a staging remote or a push URL, including a credential that `origin` already has but the destination does not. When the local clone is missing the remote's tip, the range is no longer guessed from stale tracking refs: the full content of the pushed tip is scanned instead — slower on that rare path, and no longer dependent on a guess. A clean first push in a SHA-256 repository is no longer hard-blocked.
### Itemized changes ### Itemized changes
#### Fixed #### Fixed
- **Push target scoping:** The default-branch probe and the "already on the remote" exclusion now follow the remote the push actually targets. A push to a configured remote other than `origin` is anchored on that remote. A push to a URL is described by no remote-tracking ref at all, so nothing is excluded and everything reachable is scanned; previously it borrowed `origin`'s base and excluded every commit any remote had seen. - **Push target scoping:** The default-branch probe and the "already on the remote" exclusion now follow the remote the push actually targets. A push to a configured remote other than `origin` is anchored on that remote. A push to a URL is described by no remote-tracking ref at all, so nothing is excluded and the range covers every commit reachable from the pushed tip; previously it borrowed `origin`'s base and excluded every commit any remote had seen.
- **Absent remote tips:** When git names a remote tip the local object database does not have, the local tracking refs are demonstrably stale for that ref, so no local narrowing is applied and the whole reachable history is scanned. A guessed range is never trusted in that case, whether it comes back empty or not. - **Absent remote tips:** When git names a remote tip the local object database does not have, the local tracking refs are demonstrably stale for that ref, so no local narrowing is applied and the range is anchored below every reachable commit. A guessed range is never trusted in that case, whether it comes back empty or not.
- **Slice boundary overlap for proximity rules:** Added lines are scanned in 768 KiB slices that now overlap by 16 KiB, so a pattern that needs its label nearby cannot lose it at a seam. The overlap is filled from the seam backwards to its exact budget, taking a partial line where a whole one no longer fits, so a line longer than the overlap still contributes its end. - **Slice boundary overlap for proximity rules:** Added lines are scanned in 768 KiB slices that now overlap by 16 KiB, so a pattern that needs its label nearby cannot lose it at a seam. The overlap is filled from the seam backwards to its exact budget, taking a partial line where a whole one no longer fits, so a line longer than the overlap still contributes its end.
- **Zero-width characters on ingest:** Zero-width characters are stripped before slices are budgeted, so the budget measures the same text the detection engine inspects and invisible padding cannot move a seam between a label and its secret. - **Zero-width characters on ingest:** Zero-width characters are stripped before slices are budgeted, so the budget measures the same text the detection engine inspects and invisible padding cannot move a seam between a label and its secret.
- **Empty-tree object resolution:** The fallback range's empty-tree object id is obtained from `git hash-object -t tree --stdin` instead of a hardcoded SHA-1 value that does not exist in a SHA-256 repository. - **Empty-tree object resolution:** The fallback range's empty-tree object id is obtained from `git hash-object -t tree --stdin` instead of a hardcoded SHA-1 value that does not exist in a SHA-256 repository.
- **Over-budget single lines:** A minified file whose single line exceeds the slice budget is sliced with overlap and actually read, so the finding names the credential instead of reporting an unscanned size error. - **Over-budget single lines:** A minified file whose single line exceeds the slice budget is sliced with overlap and actually read, so the finding names the credential instead of reporting an unscanned size error.
### Known limitation
The hook scans the **diff** of the range it computes, so it sees the content of the pushed tip, not each commit on the way there. A credential added in one commit and removed in a later one within the same push is absent from that diff and is not reported, although git delivers both commits. This is unchanged by this release and applies to every range the hook computes, ordinary pushes included; catching it means scanning each commit's own diff, which is a separate change.
### For contributors ### For contributors
- **Pre-push fail-open gate:** `test/redact-prepush-fail-open.sh` (32 scenarios) and `test/redact-prepush-fail-open.test.ts`, which runs it as part of the free suite. - **Pre-push fail-open gate:** `test/redact-prepush-fail-open.sh` (32 scenarios) and `test/redact-prepush-fail-open.test.ts`, which runs it as part of the free suite.