Skip to content

feat(redact): path-ignore for the pre-push credential scan#2328

Open
frederik-kaster-noygear wants to merge 6 commits into
garrytan:mainfrom
frederik-kaster-noygear:claude/redact-prepush-path-ignore
Open

feat(redact): path-ignore for the pre-push credential scan#2328
frederik-kaster-noygear wants to merge 6 commits into
garrytan:mainfrom
frederik-kaster-noygear:claude/redact-prepush-path-ignore

Conversation

@frederik-kaster-noygear

Copy link
Copy Markdown

Problem

Pushing large generated data files (e.g. prospecting/exports/**/*.csv, 4–8 MB each) is blocked by the pre-push credential guard with a HIGH engine.input_too_large finding — a false positive that has nothing to do with credentials. The scan engine fail-closes on input over the 1 MiB cap, and the hook concatenates all added-line text before scanning, so one large generated file oversizes the whole push. The only escape is GSTACK_REDACT_PREPUSH=skip on every such push, which defeats the guard entirely (it also skips real code in the same push).

Change

Add an opt-in, auditable path-ignore to bin/gstack-redact-prepush. Generated data files can be exempted from the pre-push scan without weakening credential scanning on code.

Ignore globs merge two sources:

  • .gstack/redact-prepush-ignore committed at the repo root (one glob per line, # comments, blank lines skipped) — the recommended mechanism: versioned, reviewable in git history, shared across contributors.
  • redact_prepush_ignore_globs config key (gstack-config, comma-separated) — a machine-local supplement in ~/.gstack (never committed).

Globs match repo-root-relative paths via Bun.Glob (so ** spans directories, e.g. prospecting/exports/**/*.csv). Matched paths are dropped from the scanned diff via a git :(exclude) pathspec, so excluded content never reaches scan(). Every exemption prints a one-line stderr notice reporting how many paths / bytes were skipped and appends an audit record to prepush-skip.jsonl — an ignore is never silent.

Fail-closed invariant preserved (#1946)

An ignore glob is an explicit, auditable opt-in. Anything not matched by an ignore glob still goes through scan(), so:

  • a bare oversize non-ignored file still fail-closed-blocks (unchanged);
  • a real secret in a non-ignored file still blocks, even when ignore rules are present;
  • with no ignore rules configured, behavior is byte-for-byte unchanged.

The accepted tradeoff: a secret inside an explicitly ignored path is exempted — but that path is a versioned opt-in and the skip is always reported.

Tests

Six new cases in test/redact-prepush-hook.test.ts (all green, plus the existing 15):

  • (a) ignored large file passes, skip reported on stderr
  • (b) non-ignored large file still blocks fail-closed
  • (c) real secret in a non-ignored file still blocks with ignore rules present
  • (d) explicit ignore glob exempts its file (auditable opt-in)
  • (e) machine-local config key is an additional ignore source
  • (f) no ignore rules → default behavior unchanged

Docs

  • bin/gstack-config: new redact_prepush_ignore_globs default + annotated header entry.
  • CLAUDE.md: redaction-guard section documents the config key and the committed-file mechanism.

Config bump / CHANGELOG

None here — VERSION and CHANGELOG are branch-scoped and land at /ship time per repo convention, not during development.

CI note

This PR comes from a personal fork by a non-collaborator, so the eval/E2E jobs won't receive base-repo secrets and will fail with empty-env auth. Per CONTRIBUTING / CLAUDE.md, a maintainer can move the branch to a base-repo branch to run those jobs. Free tests (bun test) pass; the two unrelated failures observed locally (sidepanel-terminal.js forceRestart, parent-process watchdog) reproduce on a clean tree with these changes stashed, so they are pre-existing and unrelated.

Context: hit while shipping noygear/medical-mockup PR #282, which commits prospecting/exports/<version>/<STATE>/suspects-*.csv (4–8 MB each). That repo will set redact_prepush_ignore_globs = prospecting/exports/**/*.csv (or the committed file) once this lands.

🤖 Generated with Claude Code

@trunk-io

trunk-io Bot commented Jul 22, 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

@frederik-kaster-noygear

Copy link
Copy Markdown
Author

/review pass — one fail-open found and fixed

Ran a pre-landing review including an independent adversarial subagent. It caught a reproduced fail-open in the initial implementation, now fixed (commits d6a474d, f00d3c1, 7b49a2e):

Fail-open: the ignored paths were excluded from the scan diff with a plain :(exclude)<name> pathspec, which is glob-enabled in git. A legal on-disk filename holding a ?/[/] metacharacter (e.g. report[2024].csv, data?.csv) was read as a pattern, so git also dropped OTHER, non-ignored sibling files from the scan. Verified end-to-end: an ignored export?.csv plus a sibling exportX.csv holding a live AKIA/PEM key exited 0 (push allowed, credential unscanned) — a violation of the #1946 fail-closed invariant.

Fix: excludes now use :(top,exclude,literal) (and the skip-accounting diff uses :(top,literal)). literal disables glob interpretation so only the exact ignored path is exempted; top anchors excludes at the repo root so they line up with the root-relative --name-only output regardless of cwd (which also closes a latent subdirectory divergence). Regression test (h) asserts the sibling secret still blocks.

Two smaller items also handled:

  • core.quotePath=false on the name-only diff so non-ASCII filenames (café.csv) match their globs instead of silently no-opping. Test (g).
  • Documented that the redact_prepush_ignore_globs config value must be comma-separated with no spaces (config reads are whitespace-delimited); steered multi-glob users to the committed .gstack/redact-prepush-ignore file.

Full prepush suite: 23 pass. gstack-config redact-keys suite: 6 pass.

frederik-kaster-noygear and others added 6 commits July 22, 2026 23:20
Machine-local, comma-separated supplement to the committed
.gstack/redact-prepush-ignore file. Empty by default (no behavior change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Large generated data files (multi-MB CSV/Parquet exports) oversize the 1 MiB
scan cap and trip a false-positive HIGH engine.input_too_large, forcing a full
GSTACK_REDACT_PREPUSH=skip that also skips real code in the same push.

Add an opt-in, auditable path-ignore. Ignore globs merge two sources: the
committed .gstack/redact-prepush-ignore file (one glob per line, # comments —
reviewable in git history, shared across contributors) and the machine-local
redact_prepush_ignore_globs config key. Matched repo-root-relative paths (Bun.Glob,
so ** spans directories) are dropped from the scanned diff via a git :(exclude)
pathspec, and a one-line stderr notice always reports how many paths / bytes were
skipped — an exemption is never silent.

Preserves the garrytan#1946 fail-closed invariant: an ignore glob is an explicit opt-in;
anything NOT matched still goes through scan(), so a bare oversize non-ignored
file still blocks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ignored large file passes with a skip notice; non-ignored large file still
fail-closed blocks; a real secret in a non-ignored file still blocks even with
ignore rules present; an explicit ignore glob exempts its file (auditable
opt-in); the machine-local config key is an additional source; no ignore rules
leaves default behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review found a reproduced fail-open: a plain `:(exclude)<name>`
git pathspec is glob-ENABLED, so a legal on-disk filename holding a `?`/`[`/`]`
metacharacter was interpreted as a PATTERN and could drop OTHER, non-ignored
files from the scan — a real credential in a sibling file (verified with a live
AKIA/PEM key) slipped past with exit 0.

Use `:(top,exclude,literal)` for excludes and `:(top,literal)` for the
skip-accounting diff. `literal` disables glob interpretation so only the exact
ignored path is exempted; `top` anchors at the repo root so excludes line up
with the root-relative names from `--name-only` regardless of cwd (the positive
pathspec is `:/`, also root-anchored). Also pass `core.quotePath=false` on the
name-only diff so non-ASCII filenames arrive as raw UTF-8 and the glob matches
(otherwise git C-quotes them and the user's ignore rule silently no-ops).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(g) a non-ASCII filename is exempted by its glob (core.quotePath=false).
(h) a `?`-bearing ignored filename does NOT drag a sibling holding a real
    secret out of the scan — the sibling credential still blocks (exit 1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gstack-config reads values whitespace-delimited, so a space after a comma
silently drops the trailing globs. Document it and steer multi-glob users to
the committed .gstack/redact-prepush-ignore file (one glob per line).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@frederik-kaster-noygear
frederik-kaster-noygear force-pushed the claude/redact-prepush-path-ignore branch from 7b49a2e to 7cce208 Compare July 23, 2026 03:21
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