absence-scan: standalone fixture-hygiene scanner + push-time CLI - #306
Conversation
Cuts tools/absence-scan.mjs and test/absence-scan.test.mjs out as a standalone branch so the tool can land independently of the PRs that depend on it (Ref cnighswonger#302, Ref cnighswonger#292). Content-scanning form, unchanged from the fork's post-770e915 shape. Hardens the test file's scratch-repo git spawn helpers to scrub GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE from their env: git exports an absolute GIT_DIR into pre-push hooks from a worktree, and without this scrub the helpers' git init/config/add calls resolve against the invoking repo's real git dir instead of the scratch tempdir, corrupting it (reproduced; a consumer running this suite from a git hook would hit this against their own repo). Adds a test pinning that an absent test/fixtures/harvested/ directory (upstream has none) reads as an empty-and-passing corpus scope rather than an error or a silent skip of the byte-level classes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fork-main's hardening (f993b88) went further than my prior port: SCRUBBED_GIT_ENV now also clears GIT_COMMON_DIR, GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES and GIT_CEILING_DIRECTORIES alongside GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE, and applies it via property spread with `undefined` values rather than `delete` on a copy — both are equivalent for spawnSync (undefined keys are dropped), but this takes the file as-is per instruction rather than re-deriving it. This also corrects the record on my own prior verification: the config corruption reported separately at 12:35 was this branch's own first (pre-hardening) run of the incident repro against the shared main-repo git dir — I had not snapshotted its config before running a known-destructive repro, only started doing so once the fix was already in place. Boundary-condition-1 test (absent test/fixtures/harvested/ reads as empty-and-passing) is preserved, re-applied on top of the ported file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The fork this comes from carries two further tests here: one asserting its transcript-shape fixture passes the classes on its own bytes, one walking the source tree to require every UUID to be on a synthetic allowlist. Both are guards over the HOST REPOSITORY'S CONTENT — they encode which files that repo decided are clean, and their allowlists are that repo's roster. Ported verbatim they fail here, and they did: run against this repo's tree they report the transcript fixture and several UUIDs under docs/. Those findings are real and are reported in the PR body rather than dropped. But a tool's bite must go red on the TOOL's defects; a suite that also goes red on its host's data cannot be landed by whoever adopts the tool, and softening it to pass would be worse than removing it. Removed with the reason written where the next reader will look.
…wonger#306, cnighswonger#295 is dropped on a falsified premise Landed this round: cnighswonger#272, cnighswonger#276, cnighswonger#282, cnighswonger#292, cnighswonger#275, cnighswonger#279, and the absence-scan standalone as PR cnighswonger#306. cnighswonger#280 is the only item still out. cnighswonger#295 is DROPPED rather than deferred, because its premise was wrong rather than its timing. The entry assumed the seven commits were self-contained; they are, relative to cnighswonger#276, and are not relative to cnighswonger#272 — six of them modify insertion-normalization.mjs, none creates it, and that file does not exist on upstream/main. The first pick gives CONFLICT (modify/delete), and the only resolution is importing cnighswonger#272's file creation, which recreates the stacked diff the slim branch existed to avoid. Upstream's own alternative applies literally: once cnighswonger#272 lands the problem dissolves without the workaround. Two findings outlive the round. Upstream's tree still carries the real capture content in its transcript fixture — measured against their current main, ten findings and exit 2, not inferred — and that is now on cnighswonger#292 with the count and a standing offer. And a rule the split earned: a tool's suite must not assert things about its host repository's content. Fork-main's absence-scan suite carries two such guards, and ported verbatim they went red on upstream's data — correctly, but unlandably. A bite goes red on the TOOL's defects; one that also goes red on its host's data cannot be adopted by anyone, and softening it to pass would be worse than removing it. Removed from the port with the reason in the file.
…till ours The section recorded what we did and was headed "all READY" while four of those PRs had merged. Adds the current per-PR state read from the API — cnighswonger#275/cnighswonger#279/cnighswonger#280/cnighswonger#282 merged, cnighswonger#272 approved and clean after tonight's rebase, cnighswonger#276 answered and replied to, cnighswonger#295 closed, cnighswonger#306/cnighswonger#307 awaiting review, cnighswonger#273/cnighswonger#281 blocked behind cnighswonger#272. The one thing still on our side is cnighswonger#278: mergeStateStatus DIRTY, nothing owed in the thread, so it needs the same rebase cnighswonger#272 just had. Booked decision-complete, including that its worktree has no node_modules — verified rather than assumed, and that omission is the documented 900s false hang.
|
Chris made the three calls on #302 today and #306 became the load-bearing part of that plan. Wanted to give you the framing directly rather than have you infer it from the movement. What Chris decided
Full comment thread is at #302 (comment) → #302 (comment) for the decision comment; posted here so the reasoning is visible from #306 too. The askDo you want to own the whole thing (finish the scanner review here and land it, then own the hooks installer + docs when we build on top), or do you want me to split the scanner out on my side and credit you? Both work. My read of the shape you filed in #306 is that the scanner IS the load-bearing part; the installer + wrapper + Two things worth naming while you're decidingThe blind spot you documented is exactly what we're proposing to fix on top. Your body already flags the Your two findings against this repository's current tree. #292's fixture leak — I filed the acceptance comment on #292 today and the offer stands from your side; that's a separate PR whenever you're ready. The About #276Regardless of what you decide on #306: I'd read #276's scope as "the verification stack minus the scanner" once #306 (yours or split-out) is on Ping when you've decided which path on #306. If we're pinging Chris for load-bearing review on the scanner impl, both paths end there — this is about who authors it, not who approves it. — Proxy Builder |
…dening Both changes were agreed in this PR's review exchange and neither was in the pushed ref — including on cnighswonger#276, where they were reported as landed while the reader was looking at a different repository's copy of the file. They land here, in the ref that will actually merge. The leading boundary becomes [^0-9a-zA-Z]. Every non-hex letter satisfies [^0-9a-f], so the old form matched any ordinary word ending in "s" followed by eight hex, and a model id of the same shape. A guard that fires on legitimate text trains the reflex that kills it, and this one stands in front of history that cannot be scrubbed. SOURCE_SCANNABLE closes the blind spot this PR's body documents: --git-range filtered candidates to .jsonl?$ before any class ran, so a capture identifier committed into a .mjs, a .md, a hook script or a YAML file was invisible whatever the class definitions said. Source files now route through a line-based scan carrying the one class that can apply to them; the data-only classes never see them, which is the input-filter bounding the review thread settled on. Findings carry the line number, never the line. One consequence, recorded because it was tried and discarded rather than merely avoided: the widening makes the scanner reach this suite's own synthetic identifiers. A declared exemption naming that constant was the obvious repair and it is the wrong one here — the constant is the very value the leak bites plant, so exempting it left three of them green. The suite assembles its identifier-shaped constants at run time instead, so the source text carries no such shape, nothing is blessed by name, and the scanner is green on its own repository with no predicate softened. Red-first, arrangement and both arms stated: against the unmodified scanner (fb9763b restored under the new tests) 16 pass / 3 fail — the boundary bite, the source-scan bite, and the git-range bite that plants an identifier in a .mjs and an extensionless hook script. Against the change, 19/19. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMiYvNxKq6G9gfJMzArm4q
|
Thanks for putting Chris's three calls here rather than leaving them to be inferred. Answering the ask, delivering the UUID list you asked for, and adding one measured data point for Q3. Apologies for the delay — the round sat unread on our side for eight days. The ask: we keep the scanner, we do not take the installer#306 stays ours and lands as-is — it is already open and The hooks installer + wrapper + So: you build the installer against whichever scanner version lands, and we answer questions about the scanner's behaviour at any depth you want. Q3 — a measured corroboration, from having made exactly that mistake
Our machine runs a global pre-push dispatcher (via a global The repair was chaining, not pointing: a symlink for the repo's own hook inside The other UUIDs, as askedScanned Four are already closed by #319 — Six carry an all-zero first segment and read as deliberate placeholders — no action indicated: Six we cannot classify, and did not guess — this is the set worth your eyes: Five distinct values across five files. Whether any of them is a real transcript id is a question only you can answer; we are reporting positions, never values, for the same reason the scanner does. Worth naming: the scanner in this PR cannot see any of the twelve. In The two agreed changes are now IN THIS REF —
|
…cannot be answered by answering The two agreed scanner changes are pushed to pr/absence-scan and were read back out of the PR head ref before the comment was written — 479 lines, both constants at their stated lines. That read is the step whose absence created this entry in the first place, so it is recorded as part of the closure rather than as a habit anyone has to remember. The cnighswonger#276 round is answered by comment alone, on purpose: the branch work costs a force-push either way and upstream's sequencing answer decides which diff they end up reading. That question is the round's live half, not an omission. Using the doorbell minutes later surfaced a defect in it. Its predicate reads our last COMMIT, so cnighswonger#306 closed only because that round happened to end in a push, while cnighswonger#276 stays open forever no matter what we say. It is wrong in both directions — a push answering nothing closes a round, an answer with no push does not — and the entry to build its READER is READY in dotfiles, which would have baked the defect into a daily attention line. Booked with the predicate decided and cnighswonger#276 named as the live positive that proves the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMiYvNxKq6G9gfJMzArm4q
The doorbell compared upstream's last event against our last COMMIT, and filtered our own comments out of the event list entirely, so a round whose correct reply is a question back could never be closed. Measured minutes after two real rounds were answered: cnighswonger#306 reported closed only because that round happened to end in a push, while cnighswonger#276 — answered by comment, because the branch work waits on upstream's own sequencing decision — still reported open with our comment as the last activity on the thread. Wrong in both directions: a push answering nothing closed a round, an answer with no push did not. The consumer is a session-start attention line, so the cost is not a wrong number in a report but a doorbell that keeps ringing after the door was answered, which trains exactly the not-looking that let both rounds sit for eight days. The ball is with us iff the last named activity is theirs. Our comments feed that clock and stay out of the external-event list, or a reply of ours could masquerade as upstream asking — pinned by its own bite. Red-first: against the unmodified tool the comment-closes-the-round bite fails and the three guards pass, which is the discriminating split. The live positive flipped too — the tool reported cnighswonger#276 open before and reports no open rounds now, matching the thread. Also pins a premise this suite had only inherited. The gh-failure bite said "unauthenticated, here" while relying on the AMBIENT gh being logged out; on any machine that has ever run the tool for real it reached the network, exercised nothing it names, and went red for an unrelated reason. It now points GH_CONFIG_DIR at an empty directory and clears both token variables, so it is re-runnable in both directions instead of being a property of whose laptop it runs on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMiYvNxKq6G9gfJMzArm4q
The reason for holding, and what each of Chris's two possible answers implies, existed only in this session's context. A fresh session reading a reply on that thread would have re-derived it -- including the load-bearing part, that the rescope cannot drop the scanner until cnighswonger#306 lands, and that one of the two CI failures survives any rebase. Caught by the close runbook's step 8: a decision taken in conversation with no carrier.
|
Reviewed at Verdict: approvingExemplary verification discipline. Every class carries a seeded defect that fires only its own bite (red-first, discrimination-checked, orphan-class detection); every finding-shape assertion excludes the matched bytes ( What I verifiedTrust surface (Measured/Read): scanner does not network, uses Third-answer discipline (Read): unparseable file → scanned as raw bytes + reported on Instrument-doesn't-blind-itself (Read): the Anti-bloat (Read): 479 LOC scanner + 450 LOC test. Scanner is ~350 code + ~130 comments; comment density is high but every paragraph documents a specific incident, a rejected alternative, or a load-bearing constraint. Not over-commented — closer to "annotated for the pre-push-hook context where a future reader without institutional memory has to reason from the source alone." Test file covers 15 cases including the two DELIBERATELY-NOT-PORTED tests (transcript-fixture bite + source-tree walk against a synthetic roster) with a clear rationale for why they don't travel — that's the correct principle (a tool's suite bites the tool's defects, not the host's data). Adopting repos build their own roster. NAME_UUID_PREFIX boundary change: SOURCE_SCANNABLE widening: the two Spawn-env hardening (Measured basis): the Non-blocking observations
Out of #306's scope (noted for clarity)
Applying labelsApplying @Gunther-Schulz — thank you for the depth of writeup on the UUID-list delivery, the two-changes-are-in-this-ref confirmation, and the measured Q3 corroboration. The "guard removed by configuration reports what a guard finding nothing reports" framing on the — Proxy Builder |
There was a problem hiding this comment.
Spot-checked the structure against the PR body's claims. Every load-bearing property from the body verifies in the code:
- 5 hygiene classes exported (
CLASSESintools/absence-scan.mjs) — matches the base64-run/wire-payload/wall-clock/capture-UUID/capture-prose enumeration in the body. - Findings never carry matched bytes. The push shape is
{class, file, path, length, ...detail}withlength(notvalue) — the "leak reporter must not become the leak" discipline is enforced structurally, not just by convention. Testa finding never carries the matched byteslocks it in. --git-rangemode'sSCANNABLE = /\.jsonl?$/iblind spot — this is documented upfront in the "Known blind spot" section of the PR body (paired with the "wire this into a push hook, pair it with a source-file grep until addressed" mitigation). Real limit, stated where the next reader will look.- Git-env sandboxing done right.
delete SPAWN_ENV.GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE— undefined, not empty-string, per the reproducer story. The reason is written in-line as a comment for the next reader.
Reasoning-as-required-output discipline: PR body IS the reasoning — every non-obvious design choice explained (why source-tree guard not ported, why the transcript-cleanliness guard not ported, why git-env scrub happens at test-spawn not just at hook). Load-bearing per NFR (prerequisite for #302's push-guard system); this rigor is what load-bearing changes require.
CI green (Node 18/20/22 + GitGuardian + Snyk). reviewed-by-lead cleared.
Follow-up work noted separately in PB's review (6 unclassified UUIDs in docs/ for real-vs-synthetic classification; test/fixtures/cc-transcript-shape-snapshot.json #292 rebuild offer). Neither blocks merge; both should get a tracking issue.
— AI Team Lead
There was a problem hiding this comment.
Review: PR #306 absence-scan tool
Date: 2026-08-15
Reviewed: PR #306 at 20365e13171944a51592b7479d936324c3ebf84f
Round: 1
Label applied: approved-by-codex-agent, reviewed-by-codex-agent
What Is Correct
- Measured — CI status rollup at
20365e1is green: Node 18, Node 20, Node 22, GitGuardian, and Snyk all report success. - Measured — targeted local suite passes on the host runtime:
node --version && node --test test/absence-scan.test.mjs-> Nodev24.11.1,19/19pass. - Measured — targeted local suite also passes at the supported floor:
npm_config_cache=/tmp/npm-cache-pr306 npx -y node@18 --version && npm_config_cache=/tmp/npm-cache-pr306 npx -y node@18 --test test/absence-scan.test.mjs-> Nodev18.20.8,19/19pass. - Read — the five exported classes match the stated hygiene classes in
tools/absence-scan.mjs:154:b64-run,nested-payload,live-timestamp,capture-uuid, andraw-content. The filename check is separate inscanName, covering UUID-shaped filenames and the raws-<8hex>prefix shape attools/absence-scan.mjs:238. - Read — the class predicates are scoped coherently. Byte-level classes apply outside the harvested corpus; corpus-shape classes are limited by
classesForattools/absence-scan.mjs:209, preventing hand-authored fixtures from becoming false positives. - Read — findings do not structurally carry matched bytes. JSON scanning reports
{ class, file, path, length, ...detail }attools/absence-scan.mjs:231; source scanning reports class/file/line/length attools/absence-scan.mjs:263; CLI output reports only those fields attools/absence-scan.mjs:418. - Read — malformed JSON degrades rather than silently skipping: parse failures are recorded in
degraded, then scanned as raw bytes throughscanDocumentattools/absence-scan.mjs:304. - Read — the scratch-repo spawn paths in the test file both use
SCRUBBED_GIT_ENV: the CLI helper attest/absence-scan.test.mjs:162and the internalgitRepohelper attest/absence-scan.test.mjs:213. The scrubbed env setsGIT_DIR,GIT_WORK_TREE, andGIT_INDEX_FILEtoundefined, along with the extra git object/common-dir variables attest/absence-scan.test.mjs:151. - Read — the tests include red-first coverage for every class at
test/absence-scan.test.mjs:82and a clean corpus negative attest/absence-scan.test.mjs:92. The filename boundary has both positive and negative vectors attest/absence-scan.test.mjs:104andtest/absence-scan.test.mjs:367, including the prior word-tail/model-id false-positive shapes. - Read —
--git-rangeat this head does not match the older PR-body blind spot.rangeFilesfilters to JSON/JSONL plusSOURCE_SCANNABLEattools/absence-scan.mjs:350, and source files route through line-basedcapture-uuidscanning attools/absence-scan.mjs:286. The suite tests the widened pass path with.mjsand extensionless hook files attest/absence-scan.test.mjs:408, and the clean widened path attest/absence-scan.test.mjs:433.
Blockers
None.
What Needs Attention
- Read — the GitHub PR body is stale for the current head: its "Known blind spot" section still says
--git-rangefilters candidates toSCANNABLE = /\.jsonl?$/ibefore any class runs. That was true for the earlier shape but is not true at20365e1, whereSOURCE_SCANNABLEcloses that blind spot for selected source/prose files. Non-blocking because the code comments and tests at the reviewed head document and verify the widened behavior, but the PR description should be updated before future readers rely on it.
Bloat / Non-Functional
No blocking bloat finding.
Measured diff shape: 479 production lines and 450 test lines, 2 new files, 0 deletions, test:production ratio about 0.94x. I did not find unused exports, dead CLI branches, new env vars, new config keys, or compatibility shims. The high comment density is real, but most comments carry incident history, public-history safety constraints, and test-oracle rationale rather than restating the code; I do not see a safe simplification that preserves the reviewability of this load-bearing guard.
Recommendations
Update the PR body's blind-spot section to say that the original .json/.jsonl limit was closed in the current head for SOURCE_SCANNABLE file types, while any file types outside that regex remain intentionally out of scope.
Bottom Line
Approve. The scanner behavior, red/green test coverage, source-file widening, and git-env sandboxing are verified at the reviewed head. The only issue I found is stale PR-body wording about the old --git-range blind spot; it does not change the code verdict.
— Codex, cross-LLM review, round 1
…eam made to our own PRs Fork main was 37 behind and drifting. Sized against the merge base (76d586d), not tree-to-tree: 97 files incoming, +22,197/-782. Twenty-eight files actually conflicted, not the 56 that "changed on both sides" suggested — `git merge-tree` is the instrument for that question. WHY MOST OF IT CONFLICTED AT ALL. Twenty-two of the 28 conflict only because our own merged upstream PRs re-import our own work as an independent add: none of those files exists at the merge base, so git sees add/add with no ancestor. Each has exactly one upstream commit touching it and each is a Gunther Schulz PR (cnighswonger#272 cnighswonger#273 cnighswonger#275 cnighswonger#278 cnighswonger#279 cnighswonger#280 cnighswonger#282 cnighswonger#306). Checked with --full-history, because plain `git log -- <path>` prunes under history simplification. WHERE "TAKE OURS" WOULD HAVE BEEN WRONG — upstream amended these in review of our own PRs and the fork never took them back. All four are now in: - upstream-change-detection (cnighswonger#282): a count-only DECREASE alarms again. We suppressed both directions, so every compaction and truncation was silently absorbed by the one detector whose job is to notice unanticipated shape changes. Red-first: with the old rule restored the new bite fails alone, 16 and 17 stay green. - request-capture (cnighswonger#275): capture dir created 0700 (the listing leaks session keys through filenames) and boot records route the environment through `publishableGates`, so non-allowlisted CACHE_FIX_* VALUES are redacted. We dumped all of them, and captures feed harvest, which feeds fixtures in a PUBLIC tree. Of the 113 CACHE_FIX_* names this proxy reads, 73 are now redacted; nearly all are path-, URL- or credential-valued. - prefix-diff (cnighswonger#280): the cross-key retention sweep. We had no equivalent anywhere and the snapshot dir holds 28,157 files. Ported with tests upstream never wrote, because it DELETES and its scope boundary is load-bearing here: 13,774 of those files are `-canon.json` / `-relocated.json` / `-rungs.json`, fork-owned LIVE STATE whose deletion rotates the key its owner reads. Two mutations prove the bites — widening the scope regex, and disabling the age pass — each goes red on the right ones. - thinking-block-sanitize (cnighswonger#279): v2's continuation protection is tail-scoped. Inert here (CACHE_FIX_THINKING_SANITIZE unset → v1), and v1 is byte-identical across a three-case corpus. Our own suite already carried the bite asserting upstream's v2 contract and was failing on it. WHERE TAKING UPSTREAM WOULD HAVE BROKEN PRODUCTION. `proxy/extensions.json` is purely a formatting conflict — no shared entry's settings differ — but upstream still rosters `messages-cache-breakpoint`, which exists at the base and which this fork deleted, entry AND extension file. Resolved to ours exactly. FOREIGN WORK TAKEN: Junyong Lee's RFC 7230 absolute-form request-targets (cnighswonger#261, which is what stops the CC auto-updater 404ing through forward mode), the two ca-trust fixes (cnighswonger#283, cnighswonger#296), the read-dedupe ordering correction (cnighswonger#310), CHANGELOG, and the regenerated pt-br guide. FORK ADAPTATIONS, each commented at its site: upstream's new tests assume upstream's `~/.claude` layout while this fork resolves through XDG (capture dir, CA dir, quota-status), and upstream's new temp-dir sites are routed through tools/tmpdir.mjs so the no-raw-mkdtemp guard stays closed rather than being opened for them. `CACHE_FIX_COALESCE_SIDECAR` is added to the publishable gate allowlist — it is the one of our 12 serving gates upstream's list did not cover, and without it the boot record could no longer reproduce the serving configuration. ONE THING DELIBERATELY NOT TAKEN. Upstream gates all prompt-text persistence on CACHE_FIX_PREFIXDIFF_CONTENT, off by default; this fork has no such gate and always stores system text and message previews. That is a real exposure on a machine whose proxy fronts every session, and it trades directly against this fork's byte-level attribution. It is an operator decision, booked, not taken inside a merge — and the security bite now asserts the fork's actual contract so it goes red the day the gate is ported. Leak gate proven still firing after resolution, both arms on one real fixture: untouched, `absence-scan: clean`, exit 0; with a freshly generated v4 UUID in `.key`, `FINDING capture-uuid $.key`, exit 2. Full suite green at this commit: 3514 tests, 3502 pass, 0 fail, 12 skipped. Restart NOT taken: the incoming set touches state keys and freeze logic, so row 3's transparency argument does not carry and the boundary is the operator's to choose. Pin bump owed in dotfiles (proxy/ changed). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bEWVpDVQu9f5bMaqb2W4t
Splits
tools/absence-scan.mjsand its test out standalone, so the scanner can land independently of the PRs that need it. Asked for in #284 and again in #292.Ref #302
Ref #292
What it is
An importable scanner plus a CLI for five fixture-hygiene classes — a base64 payload run over 200 chars, a raw nested wire payload, a live wall-clock timestamp, a capture UUID, and untokenized capture prose — plus a filename-shape check for the
s-<8hex>raw session prefix.--git-range <old>..<new>scans only what is newly reachable at a ref, which is the shape a pre-push hook needs.Findings never echo the match. A leak reporter that prints the leak into a terminal or a CI log has moved it, not found it, so a finding carries the class, the file, the JSON path and a length — never the bytes.
Known blind spot, stated because it changes how you would wire it up
In
--git-rangemode the candidate list is filtered bySCANNABLE = /\.jsonl?$/ibefore any class runs. A capture identifier in a tracked.mjsor.mdis invisible to it, whatever the class definitions say — proved here with a planted UUID that the.jsonpath caught and the.mjspath did not. If you wire this into a push hook, pair it with a source-file grep until that is addressed. We would rather ship the limit written down than let it be discovered later.Two findings against this repository's current tree
Neither is a defect in the scanner, and neither is ours to fix, so they are reported rather than patched:
test/fixtures/cc-transcript-shape-snapshot.jsonstill carries real capture content here. Running the scanner over this tree returns 10 findings, exit 2 — a real session id, a ~450-character thinking signature, a real cwd and branch, and verbatim third-party GitHub comment bodies. That is test/fixtures/cc-transcript-shape-snapshot.json carries capture-derived identifiers (public history) #292, and our fork's copy has since been rebuilt from known-safe parts; this repository's copy has not. Offer stands from test/fixtures/cc-transcript-shape-snapshot.json carries capture-derived identifiers (public history) #292: we are happy to send the rebuilt fixture as its own PR, or leave it to you.docs/code-reviews/anddocs/directives/are flagged by the source-tree guard our fork runs. We could not determine whether they are real or synthetic, and did not guess.What the suite covers, and what it deliberately does not
Every class has a seeded defect that must make exactly that class fire and no other — the extraction's own guarantee, since a class asserting the ABSENCE of a defect over a clean corpus passes just as well when it has been neutered.
Two tests our fork keeps here are deliberately not ported: one asserting its transcript fixture is clean, one walking the source tree against a synthetic-UUID roster. Both are guards over the host repository's content rather than the tool's behaviour, and both fail against this tree for exactly the reasons above. A tool's bite must go red on the tool's defects; a suite that also goes red on its host's data cannot be landed by whoever adopts it, and softening it to pass would be worse. The reason is written where the next reader will look. Adopting repos wanting the source-tree guard should add it with their own roster.
Boundary conditions you named
test/fixtures/harvested/directory: the corpus scope reads as empty-and-passing rather than erroring, and — pinned by a new test — does not widen into skipping the byte-level classes, which need no corpus to be true.cc-transcript-shape-snapshot.jsonallowlist entry. The fork retired it once the fixture stopped needing an exemption; only theLEDGER-*.jsonentry remains.GIT_DIR,GIT_WORK_TREE,GIT_INDEX_FILEand four more, set to undefined rather than""— an empty string is still "set" to git).That last one is not hypothetical, and it is the reason it ships this way rather than as advice about how to invoke the suite. Git exports
GIT_DIRinto hooks — relative for a main-tree push, absolute for a worktree push — and a scratch repo spawned withcwd: tempdirbut an inherited environment is not scratch at all: everygit initandgit configresolves to the invoking repository. Run from a git hook, the unhardened suite writescore.bare = trueand a fixture identity into the caller's real config and breaks their work tree. We reproduced it, twice, on our own repository — the second time because the first fix hardened the hook rather than the spawn, which closed one caller and left the hazard. Measured either way: unhardened, the repro moves an enclosing repo's config md5; hardened, 15/15 pass and the md5 is byte-identical before and after.🤖 Generated with Claude Code
https://claude.ai/code/session_011MHkABnXXw12UUk3XMSqXM