Skip to content

feat(redact-prepush): repo-level .gstack-redact-allowlist for known-benign values - #2598

Closed
boyard wants to merge 1 commit into
garrytan:mainfrom
boyard:redact-prepush-allowlist
Closed

boyard wants to merge 1 commit into
garrytan:mainfrom
boyard:redact-prepush-allowlist

Conversation

@boyard

@boyard boyard commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Why (in your own words)

The pre-push hook calls the scan engine with its allowlist option permanently empty — there is no config surface anywhere for a known-benign value (a public dev-compose password, a documented sample URL). So every push touching such a value warns or blocks, forever, with no legitimate resolution. We watched what that trains people to do on a real repo: rather than bypass the hook, an AI worker restructured a dev-only credential string across variables until the scanner stopped matching. Detector evasion — the finding is silenced AND no record exists anywhere. A reviewable allowlist entry is strictly better than both a bypass and a reshape: it leaves the decision in the repo, in a diff, with a comment.

This PR adds that surface: a repo-root .gstack-redact-allowlist file (one exact string per line, # comments), passed as the engine's existing allowlist scan option (the engine already honors allow.has(span); the hook just never fed it).

Design points:

  • Entries match the captured span, not a substring — for db.url_with_password that is the whole scheme://user:pass@host:port URL, so an entry clears one specific benign URL, never every use of that password.
  • Suppression is reported, never silent — the hook prints how many HIGH/MEDIUM findings the allowlist suppressed on each push (a silent suppression would be the same sin as the reshaping it replaces).
  • The HIGH block message now names the allowlist as the recorded resolution path for known-benign values, and explicitly warns against restructuring code to dodge the pattern — discoverability at the exact moment someone is deciding between the honest fix and the evasive one.
  • Fail-strict in every failure mode — absent, unreadable, or oversized (>64 KiB) file all mean "scan without an allowlist": the file can make the scan stricter or record a reviewed suppression, but can never block a push or fail open.

Live evidence

Before/after on a real repo, driving the hook with the git pre-push stdin protocol (a docker-compose dev URL, public in the repo it came from):

=== BEFORE (no allowlist) ===

⛔ gstack-redact-prepush BLOCKED the push — credential(s) in the pushed diff:

  HIGH  db.url_with_password  post********…

Rotate the credential (a pushed secret is compromised) and remove it from the diff.
If this is a KNOWN-BENIGN value (a public dev-compose password, a documented sample),
add the exact matched string to .gstack-redact-allowlist at the repo root instead — that
records the decision as a reviewable commit. Do NOT restructure code to dodge the
pattern: that silences the finding while leaving no record.
This is a guardrail: `git push --no-verify` or `GSTACK_REDACT_PREPUSH=skip git push` bypass it.
exit=1

=== AFTER (echo 'postgres://hospitality:hospitality@db:5432' >> .gstack-redact-allowlist) ===
gstack-redact-prepush: 1 finding(s) suppressed by .gstack-redact-allowlist (entries there are trusted as reviewed-benign).
exit=0

Test file (7 new cases alongside the existing hook tests):

$ bun test test/redact-prepush-hook.test.ts
 29 pass
 0 fail
 63 expect() calls
Ran 29 tests across 1 file. [8.51s]

Mutation check — drop allowlist from the scan opts in the hook and the new tests go RED (real output, then reverted):

(fail) repo allowlist (.gstack-redact-allowlist) > an allowlisted HIGH span passes instead of blocking [203.29ms]
(fail) repo allowlist (.gstack-redact-allowlist) > suppression is REPORTED, never silent [208.68ms]
(fail) repo allowlist (.gstack-redact-allowlist) > comments and blank lines are ignored; entries are trimmed [242.27ms]
(fail) repo allowlist (.gstack-redact-allowlist) > pushing the allowlist file itself does not block on its own entries [289.44ms]
 25 pass
 4 fail

bun run test (free tier): all shards pass except 7 pre-existing failures in browse/test/bun-polyfill.test.ts, verified identical on pristine upstream/main on the same machine — cause is node not installed on this host, unrelated to this change.

Scope

  • Changed: bin/gstack-redact-prepush (allowlist load + wiring + suppression notice + block-message hint), test/redact-prepush-hook.test.ts (7 new tests).
  • Verified live by: the before/after transcript above; the full hook test file (29 pass); a documented mutation check (4 RED when the wiring is removed).
  • Did NOT test: Windows paths (the file read uses path.join on git rev-parse --show-toplevel output, same as the rest of the hook); the engine's span normalization for non-ASCII allowlist entries (entries are compared against the engine's normalized span — exotic Unicode values may need their NFKC form in the file).

Liveness proof (required)

The repo operator (boyard) will attach the GSTACK PR liveness screenshot and tick the checklist before marking this ready for review — draft until then. (This PR was prepared by an AI agent in the operator's environment; the screenshot requirement exists precisely to put a human on record, so it is his to provide, not mine to generate.)

Screenshot 2026-08-18 at 22 16 07

Checklist

  • Liveness screenshot attached: GSTACK PR typed live into a real surface (not edited onto the image)
  • This is not a generated-file-only diff (I edited the source/template and regenerated)
  • No ETHOS.md edits, and no changes to voice / founder perspective / YC references
  • New public command / external service / host adapter has an accepted issue linked (or N/A) — N/A: no new command/service/host; a config file read by an existing hook
  • Linked issue or reproduction: reproduction transcript included above

…enign values

The pre-push hook scans with the engine's allowlist option permanently
empty — there was no config surface at all for a known-benign value (a
public dev-compose password, a documented sample URL). The observed
failure mode: rather than bypassing, a worker restructured a credential
string across variables until the scanner stopped matching — detector
evasion that silences the finding while leaving no record anywhere. A
recorded, reviewable suppression surface is strictly better.

- bin/gstack-redact-prepush reads .gstack-redact-allowlist at the repo
  root (one exact string per line, # comments) and passes entries as the
  scan allowlist (the engine already honors allow.has(span)).
- Entries match the CAPTURED SPAN, so for db.url_with_password an entry
  clears one specific URL, never every use of a password.
- Suppression is reported, never silent: the hook prints how many
  HIGH/MEDIUM findings the allowlist suppressed on each push.
- The HIGH block message names the allowlist as the recorded resolution
  path and explicitly warns against restructuring to dodge the pattern.
- Fail-strict: absent/unreadable/oversized (>64KiB) file → scan runs
  WITHOUT an allowlist; it can never block or loosen a push by failing.

Tests: 7 new cases in test/redact-prepush-hook.test.ts (HIGH cleared,
suppression notice, exact-span scope, comment parsing, self-bootstrap
push of the allowlist file, oversize fallback, block-message hint).
Mutation check: dropping the allowlist from the scan opts sends 4 of
them RED.
@trunk-io

trunk-io Bot commented Aug 16, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@capy-ai

capy-ai Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Landed in v1.91.66.0 (#3087): the repo-root .gstack-redact-allowlist, rewritten on current main with credit to you (Co-authored-by). Thank you @boyard!

@capy-ai capy-ai Bot closed this Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant