feat(classify): case-fold blocked/sensitive matching in risk-paths.yml#114
Merged
Conversation
minimatch defaults to case-SENSITIVE, so a lowercase-authored pattern
silently misses real-world case variants. '**/secrets*' in wxa-jake-ai's
`blocked:` matched docs/secrets.md but NOT docs/SECRETS.md — so the
production secrets ROTATION RUNBOOK, whose shell snippets are pasted against
prod hosts, fell through to `docs/**` and classified risk:trivial ("zero
runtime impact"), auto-merge-eligible. wxa-jake-ai#875 had to be opened as a
draft to dodge it. Repo-side exact-case gate: wxa-jake-ai#877.
The invariant: folding case may only ever ADD gating, never remove it.
That is why the fold is restricted to blocked/sensitive rather than applied
uniformly. Folding the safe/trivial classes is a fail-OPEN: a path matching
nothing gets the deliberately-strict `standard` fallback, and folding would
hand it to an auto-merge-eligible class instead. With `safe_test:
['tests/**']`, a PR adding 'Tests/release.py' would classify safe_test rather
than standard — and on GitHub's case-sensitive filesystem that is a genuinely
DISTINCT path, not the same file recased. Class precedence does not help:
it only breaks ties when a blocked/sensitive pattern also matches, and there
none does. Folding the safe classes has no upside either — its only effect is
to make them more lenient. (Codex round-1 P2; the fleet audit below could not
have caught it, since it scans files that already exist and this vector is
about files a future PR introduces.)
Negation now fails closed in the folded classes, mirroring the bracket guard
in #107: '!' inverts the match, so folding REMOVES gating — minimatch('FOO',
'!foo') is true but false under nocase; 'src/!(*.md)' vs 'src/A.MD' likewise.
Zero fleet repos use negation in a gating class, so — exactly as with brackets
— strictness costs nothing today and stops the footgun being introduced.
(Codex round-2 P2.)
Fleet audit before shipping, 2026-07-14: every blob in all 45 repos carrying a
risk-paths.yml (18,604 files) classified twice, fold off vs on. ZERO
downgrades. Exactly 2 upgrades, both real secrets docs, both trivial ->
blocked: wxa-jake-ai docs/SECRETS.md and inbox_superpilot
docs/SECRETS_ROTATION.md. Re-verified under the narrowed fold: same 2
upgrades, still zero downgrades.
Does NOT fix .github/CODEOWNERS, which GitHub matches itself and also
case-sensitively. Repos relying on a lowercase glob to own an uppercase path
still need an exact-case CODEOWNERS line (see wxa-jake-ai#877).
selftest/test_classify_nocase.sh pins the fix, the safe-class asymmetry, and
the negation guard; verified to fail without the change.
Auto-Merge-Risk: high
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Coverage Floor — mode:
|
topcoder1
marked this pull request as ready for review
July 15, 2026 07:10
Codex reviewno regressions found |
The `prettier (markdown)` check formats *and* to _and_. Caught by CI on #114. Auto-Merge-Risk: high Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex reviewregression: .github/scripts/classify.mjs:170 - no test exercises case-folded matching for the sensitive class |
|
No issues found. The case-fold is applied only to the two highest-priority classes (blocked/sensitive); since nocase only ever adds matches and classify() returns the first match in priority order, the change can only upgrade gating, never downgrade — the invariant holds. Negation guard is correctly scoped and tests cover both halves plus fail-closed negation. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
classify.mjsnow matchesblocked:/sensitive:patterns case-insensitively. Companion to whois-api-llc/wxa-jake-ai#877.Why
minimatch defaults to case-sensitive, so a lowercase-authored pattern silently misses real-world case variants:
docs/SECRETS.mdtrivial← auto-merge eligibledocs/secrets.mdblocked**/secrets*in wxa-jake-ai'sblocked:never matcheddocs/SECRETS.md, so the production secrets rotation runbook — shell snippets pasted against prod hosts — fell through todocs/**and landed intrivial:("zero runtime impact"). wxa-jake-ai#875 had to be held as a draft to dodge it.The invariant
Everything below follows from that one line.
Why the fold is asymmetric (blocked/sensitive only)
Applying
nocaseuniformly is a fail-open, and this is the part worth reading. A path matching nothing today gets the deliberately-strictstandardfallback. Folding the safe classes hands it to an auto-merge-eligible class instead: withsafe_test: ['tests/**'], a PR addingTests/release.pywould classifysafe_testrather thanstandard. On GitHub's case-sensitive filesystem that's a genuinely distinct path, not the same file recased — a lowercase pattern has no business claiming it.Class precedence does not protect against this: it only breaks ties when a blocked/sensitive pattern also matches, and here none does.
And there's no upside being given up — folding the safe classes only makes them more lenient, which is precisely the direction we don't want. The asymmetry buys the entire safety benefit at zero cost.
Caught by codex round-1 (P2). Worth being explicit: the fleet audit below could not have found this. It scans files that already exist; this vector is about files a future PR introduces. Measuring the static tree and concluding "safe" was the flaw.
Why negation now fails closed
!inverts the match, so folding removes gating. Verified directly:This mirrors the bracket guard from #107 exactly, including its rationale: zero of the 45 fleet repos use negation in a gating class, so strictness costs nothing today and stops the footgun from ever being introduced. Caught by codex round-2 (P2).
Fleet evidence
Every blob in all 45 repos carrying a
risk-paths.yml— 18,604 files — classified twice, fold off vs on:Zero downgrades. Both upgrades are real secrets docs currently sitting at
risk:trivial. Re-verified under the narrowed fold after the codex fix: same 2 upgrades, still zero downgrades — the narrowing costs nothing.Enumeration used the REST contents API per repo across both orgs (
topcoder1is a user account, not an org), not code search — a fine-grained PAT returns silently-empty search results, which would have produced a falsely-clean verdict. Zero unreadable repos.Latent (no live hit, but present fleet-wide):
Dockerfilein 44/45 repos and the.envfamily — a committedDOCKERFILEwould classifystandardtoday. This closes those too.Scope limit — read this
This does NOT fix
.github/CODEOWNERS.GitHub matches that itself, and also case-sensitively:So
**/secrets*in CODEOWNERS misseddocs/SECRETS.mdtoo, and the file had no code owner — a second dead gate that no upstream change can reach. Repos relying on a lowercase glob to own an uppercase path need an exact-case CODEOWNERS line. wxa-jake-ai#877 does this;inbox_superpilotstill needs it.Tests
selftest/test_classify_nocase.sh— 21 cases pinning the fix, the safe-class asymmetry, and the negation guard. Registered intest_workflow_guards.pysotests-runner.ymlenforces it in CI.Verified the test actually tests something: with the fold reverted, 6 of 12 original cases fail, including
docs/SECRETS.md → trivial. The existingtest_classify_bracket_guard.shstill passes (5/5).Rollout
pr-classify.ymlfetchesclassify.mjsfrommainat run time, so this takes effect fleet-wide on merge. The #783/#107 merge-order trap does not apply: #107 needed ordering because it fails closed; this doesn't, and either order lands in the same state. wxa-jake-ai#877 is independent — it fixes that repo's CODEOWNERS half, which this PR cannot.Auto-merge rationale: MANUAL MERGE — PRs to
topcoder1/ci-workflowsare always manual-merge per policy, and this one changes the risk classifier for all 45 consuming repos. Opened as a draft deliberately.Codex pre-review:
codex review --uncommitted(v0.144.1). Round 1 → P2 (uniform fold fail-open) fixed. Round 2 → P2 (negation non-monotonic) fixed. Rounds 3 and 4 → zero findings, two consecutive clean rounds. Codex rounds: 4.HUMAN_READABLE_SUMMARY: Our PR risk classifier compared file paths letter-for-letter including capitals, so a rule written as
secretsfailed to catch a file namedSECRETS.md— and a production secrets runbook was being treated as a harmless doc that could merge itself. This makes the safety rules ignore capitalization, but only for the rules that restrict things, never the ones that wave things through — so the change can only ever make the gate stricter, never looser.🤖 Generated with Claude Code