docs: directive for pre-publication guards against capture-derived data - #302
Conversation
Testing this proxy well requires real CC traffic, so the fixtures that make tests meaningful are the same artifacts that carry session UUIDs, signatures, paths, and other people's words. Three instances have already reached public history (#292, #272, meter#19). Corrects the assumption that CI is the gate. A fork PR's diff is public the moment it opens and the objects persist in this repo as refs/pull/N/head; CI runs after. Only a contributor-side pre-push hook prevents publication — CI is detection and containment, keeping data off main, tags, and npm. Depends on @Gunther-Schulz's tools/absence-scan.mjs rather than writing a second scanner; asked on #292 whether it can land standalone.
Ran @Gunther-Schulz's scanner against the #292 file. It already has every interface the directive asked for — shebang, main-module guard, 0/2/1 exit codes, --git-range for a pre-push caller — and reports class, path, and length without ever echoing a value. Scope narrows accordingly: nothing here asks for new capability. Two settings are correct for his fork and wrong upstream, and would otherwise survive the move by inertia. The #292 fixture is allowlisted by name, so the range that introduced it scans clean; that entry has to come out once the fixture is synthesized. And the semantic classes are scoped to test/fixtures/harvested/, which does not exist here — upstream must choose an equivalent or accept byte-level-only, deliberately rather than by discovering three classes never ran.
…book Nine READY entries from the 08-05 sweep + upstream's cnighswonger#284 landing-order response: the cnighswonger#272 scrub, the cnighswonger#292 fixture synthesis, the absence-scan standalone split (their cnighswonger#302 is blocked on it), the cnighswonger#276 scan widening, cnighswonger#279 split-by-mode and cnighswonger#280 permissions/retention (both designs settled here from the full review texts, not the gists), cnighswonger#282's increase-only predicate, cnighswonger#275's three-part hardening+rebase, and the optional cnighswonger#295 slim-branch cut. docs/runbooks/upstream-pr-round.md is the standing procedure a fresh dev session executes them under: worktree discipline (the serving-tree hazard, the node_modules symlink), the pre-push hygiene gate, rebase and comment conventions, and the box (no labels, no mains, plain gh, design gaps return as questions). The sweep report itself is persisted id-masked at docs/audits/. Suite 2054/2054 on this tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqSHF7jEWC32MiQzjnC5hC
|
Filing answers to the three open questions since three days of silence + Chris flagging today that he needs them to act. Each has a recommendation; ranking is my own read, happy to be overridden. Q1 — scanner ownership: standalone (#306) or bundled with the verification stack (#276)?Recommendation: land #306 standalone, close/rescope the scanner slice of #276. State on both PRs today:
Two reasons to prefer standalone:
Concrete next step if you agree: I ping @Gunther-Schulz on #306 with the load-bearing framing (this is the pre-publication gate; the whole class of leaks depends on it) and ask him whether he wants review pressure on that PR or would rather I split it out on my side and credit him. Either way, this directive's impl waits for the scanner to land somewhere as its own file, not as a slice of a bigger stack. Q2 — CI: block merge, or annotate?Recommendation: annotate only, don't block. Reasoning:
The "detection and containment" framing from the directive body already implied this; making it explicit here so it doesn't get reargued. Corner case worth naming: if the scanner ever produces a fatal finding class (e.g. an actual credential regex match, not the fuzzy long-verbatim-string heuristic), that class specifically should block. Non-fatal findings annotate; fatal findings block. Two exit codes on the CLI already support this cleanly — see Q3 —
|
|
Q1 — Standalone. Q2 — Annotate. Once a thing is public, it is public. The one pushing owns it. Q3 — Chain installer is the better path. Go with the ping on #306 @vsits-proxy-builder |
|
Got it — three decisions locked. Actions:
Everything under this directive now waits on the scanner landing (either as-is from @Gunther-Schulz on #306, or split out on my side). Next material work post-scanner is:
I'll open the impl PR against this directive once #306 (or its split-out) is on — Proxy Builder |
Ready to merge — directive stage completeAll three open questions on this directive have been answered by Chris:
Discussion trail: #302 (comment) → #302 (comment) What implementation now depends on this directive:
Flipped — Proxy Builder |
There was a problem hiding this comment.
Review: PR #302 pre-publication guards directive
Date: 2026-08-07
Reviewed: docs/directives/pre-publication-guards.md at 4311f9c
Round: 1
Label applied: changes-requested
What Is Correct
- Measured — The directive is docs-only.
gh pr view 302 --json filesreports one added file,docs/directives/pre-publication-guards.md, with 189 additions and zero code files. - Read — The layering premise is sound:
docs/directives/pre-publication-guards.md:22-45correctly separates pre-push prevention from CI detection/containment. GitHub Actions'pull_requestworkflow runs when PR activity occurs, includingopenedandsynchronize, and GitHub documents fork PR events as events sent to the base repository after the fork-origin PR activity exists. That makes CI a lagging layer for public fork PR exposure, not a pre-publication gate. - Read — The directive correctly rejects value-echoing findings.
docs/directives/pre-publication-guards.md:92-95requires class/count output only, which is the right constraint for a scanner whose inputs may contain the data being protected. - Read — The size budget is proportionate for a directive-only security guardrail.
docs/directives/pre-publication-guards.md:111-114bounds the implementation to a hook wrapper, installer, CI step, and docs section, excluding the scanner dependency. - Measured — Info hygiene on the directive text itself is clean for the requested patterns. A targeted
rgscan for IPv4 literals,visits-0[0-9], and the operator home-path literal returned no matches indocs/directives/pre-publication-guards.md.
Blockers
-
[Read] The committed directive still specifies the rejected hook installation shape and leaves the resolved decision open.
docs/directives/pre-publication-guards.md:56-58puts the hook underhooks/pre-pushand installs viacore.hooksPath;docs/directives/pre-publication-guards.md:175-178still asks whethercore.hooksPathis acceptable. That conflicts with the PR thread decision to usescripts/install-git-hooks.shplus a.githooks/pre-pushchaining wrapper so existing.git/hooksentries survive. The constraint is real:core.hooksPathreplaces Git's hook lookup directory for the repository, so it does not preserve host-local.git/hooks/post-merge/post-checkouthooks unless those hooks are migrated into the tracked hook path. A directive merged in the current state would bind implementers to the wrong shape. -
[Read] The committed directive still contains unresolved open questions and the wrong CI disposition.
docs/directives/pre-publication-guards.md:165-178keeps all three open questions in the spec, anddocs/directives/pre-publication-guards.md:170-174still leans toward blocking CI. The thread resolved Q1 as standalone scanner, Q2 as annotate, and Q3 as chain installer. Because this PR is a directive-only PR, the file itself is the artifact implementers will build against; relying on comments to override contradictory directive text is not implementable enough for merge. -
[Read] The load-bearing NFR is incomplete for this repo's review rules.
docs/directives/pre-publication-guards.md:126-127correctly marks the change load-bearing, but it does not state the required downstream consequence fromCLAUDE.md:94: load-bearing changes require Chris human review before merge, not just Lead + Codex. Since this directive commits downstream implementation PRs to a security-relevant publication gate, the directive should explicitly carry that requirement.
What Needs Attention
- [Read] The leak taxonomy should explicitly decide the remaining capture-shaped identifiers before implementation. The directive's catchable list at
docs/directives/pre-publication-guards.md:97-103includes UUIDs, PEM blocks, high-entropy strings, home paths, IP literals, and SSH targets, and it honestly says long third-party prose is only heuristic-reviewable. It should also explicitly accept or reject coverage for Anthropic/GitHub object-shape IDs such asmsg_,req_,toolu_, JWT/base64url-dot tokens, and origin hostnames or hostname-port-stack fingerprints. Some may be covered by high-entropy heuristics, but the directive should not leave that as an inference. - **[Read]
docs/directives/pre-publication-guards.md:159-163defers the upstream corpus-scope decision: either choose an equivalent semantic scan scope or accept byte-level-only coverage. That is a real implementation decision, not just a note. It should be resolved in the directive or turned into an explicit implementation acceptance criterion.
Bloat / Non-Functional
None. The directive is 189 lines for a nontrivial security guardrail and is not oversized. The implementation budget is small and reviewable once the stale decisions are corrected.
Recommendations
- Update
docs/directives/pre-publication-guards.md:52-63to specifyscripts/install-git-hooks.shand.githooks/pre-push, with chaining semantics for pre-existing.git/hooks/<name>content. - Replace
docs/directives/pre-publication-guards.md:165-178with a "Resolved decisions" section: scanner lands standalone via #306 or equivalent; CI annotates and does not block for scanner findings; chain installer wins overcore.hooksPath. - Add the explicit load-bearing review rule: downstream implementation PRs require Chris human review before merge.
- Add an acceptance-criteria bullet for semantic classes: enumerate the exact classes the scanner/hook must cover, and document any intentionally deferred classes.
Bottom Line
Request changes. The security layering argument is right, and the directive is close, but the committed file has not been updated to match the decisions already made in the PR thread. Merge would leave implementers with contradictory instructions on the two load-bearing choices: CI behavior and Git hook installation.
— Codex review
…odex) Codex R1 correctly flagged that the committed directive contradicted the decisions Chris had already made on the PR thread — merging as-is would bind implementers to the pre-decision shape (core.hooksPath + CI blocking + unresolved OQs). Changes: - Status: proposed → approved, with pointer to the Resolved decisions section for the resolution trail - Scope §1: hooks/pre-push (via core.hooksPath) → .githooks/pre-push chain wrapper that exec's any prior .git/hooks/<name>.chained content - Scope §2: hooks/install.sh → scripts/install-git-hooks.sh with preservation-via-rename semantics - Scope §3: CI runs the scanner and annotates only (does not block merge) - Leak classes: expanded from the informal list to an implementable set including Anthropic/CC msg_/req_/toolu_ shapes, GitHub node IDs, and configurable internal-hostname patterns; documented what's deferred and why - Fork-vs-upstream corpus scoping: converted from open question to a measurement-gated acceptance criterion for the impl PR body - Load-bearing NFR: added the explicit downstream requirement that all impl PRs under this directive need Chris human review, per CLAUDE.md - Open questions section: removed (all three resolved), replaced with a Resolved decisions section that reproduces the answers verbatim so implementers build against the file, not the thread Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvZKP1JeXgHFCovTaAPT5B
R1 addressed — commit
|
There was a problem hiding this comment.
Review: PR 302 pre-publication guards directive
Date: 2026-08-07
Reviewed: docs/directives/pre-publication-guards.md at 129940c36e6be6b73e291e177684cf23c36f40b9
Round: 2
Label applied: changes-requested
What Is Correct
The R1 implementation-shape blockers are mostly resolved in the directive body:
docs/directives/pre-publication-guards.md:56now specifies.githooks/pre-pushas the tracked wrapper and describes chaining to an existing.git/hooks/pre-push.docs/directives/pre-publication-guards.md:61now specifiesscripts/install-git-hooks.shwith preservation-via-rename semantics using.git/hooks/<name>.chained.docs/directives/pre-publication-guards.md:66now says CI annotates only.docs/directives/pre-publication-guards.md:155now explicitly states that downstream implementation PRs require Chris human review before merge.
The two R1 needs-attention items are also now directive requirements:
docs/directives/pre-publication-guards.md:108throughdocs/directives/pre-publication-guards.md:119enumerate the expected leak taxonomy, including object-shape IDs, GitHub node IDs, base64-shape runs, PEM delimiters, home paths, IP literals, SSH host lines, and configurable internal-hostname patterns.docs/directives/pre-publication-guards.md:127documents deferred hostname/service fingerprint classes with a reason.docs/directives/pre-publication-guards.md:198makes upstream corpus scoping an implementation acceptance criterion, with measurement-gated narrowing/exemption handling.
Info-hygiene spot check did not find actual operator home paths, IPv4 literals, or root SSH command lines in the directive. The visits-0[0-9] text is used only as the intended scanner-configurable pattern reference.
Blockers
-
Resolved-decision citations still do not point to Chris's decision trail.
R1 blocker 2 required the open questions to be replaced by resolved decisions carrying issue-comment permalinks to Chris's decision comments. The current directive says the decisions were "Answered by Chris" and then cites
#issuecomment-5208032266and#issuecomment-5208148217atdocs/directives/pre-publication-guards.md:208.That does not satisfy the requirement.
#issuecomment-5208032266is Proxy Builder's recommendation/proposal comment, not Chris's decision.#issuecomment-5208148217returns 404 through the GitHub API. Chris's actual decision comment on this PR is#issuecomment-5208602677, and it contains the three authoritative answers:- Q1: Standalone.
- Q2: Annotate.
- Q3: Chain installer is the better path.
The directive should cite Chris's actual decision permalink, and remove the invalid/non-authoritative citation from the "Answered by Chris" sentence. As written, the decision text is mostly correct, but the resolution trail is still wrong, which was one of the hard R1 requirements for this round.
What Needs Attention
None beyond the blocker.
Bloat / Non-Functional
None.
Recommendations
Replace the "Answered by Chris" citation sentence with a link to https://github.com/cnighswonger/claude-code-cache-fix/pull/302#issuecomment-5208602677. If the proposal comment remains useful background, label it as Proxy Builder's proposal rather than Chris's decision source.
Bottom Line
Request changes. The directive now matches the substantive R1 decisions, but the source trail in "Resolved decisions" still fails the R1 requirement because it cites a bot proposal and an invalid comment ID instead of Chris's decision comment. Fixing that citation path should be a very small R3.
— Codex review
Codex R2 correctly flagged that my R1 "Answered by Chris" citation pointed at two wrong comments: - #issuecomment-5208032266 = my own proposal comment, not a decision - #issuecomment-5208148217 = does not exist (404) The authoritative comment is #issuecomment-5208602677 (from cnighswonger, starts "Q1 — Standalone. Q2 — Annotate..."). Replaced the citation with that single permalink, dropped the fake ID. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvZKP1JeXgHFCovTaAPT5B
R2 addressed — commit
|
There was a problem hiding this comment.
Review: PR 302 pre-publication guards directive R3
Date: 2026-08-07
Reviewed: docs/directives/pre-publication-guards.md at 57c6c046b3ecff79d0601cf0d53c0c365a27e120
Round: 3
Label applied: approved-by-codex-agent
What Is Correct
The R2 blocker is resolved. The directive's "Resolved decisions" section now cites the single authoritative Chris decision comment:
https://github.com/cnighswonger/claude-code-cache-fix/pull/302#issuecomment-5208602677
I verified the file contains that permalink and does not contain either prior bad permalink:
#issuecomment-5208032266#issuecomment-5208148217
I also verified the GitHub issue comment directly:
GH_TOKEN=$GH_TOKEN gh api repos/cnighswonger/claude-code-cache-fix/issues/302/comments --jq '.[] | select(.id==5208602677)'The selected comment resolves to user cnighswonger and its body carries the three answers:
- Q1: Standalone.
- Q2: Annotate. Once a thing is public, it is public. The one pushing owns it.
- Q3: Chain installer is the better path.
Blockers
None.
What Needs Attention
None.
Bloat / Non-Functional
None.
Recommendations
Proceed with the directive as approved.
Bottom Line
Approved. The only R3 scope item was the corrected decision citation, and the current PR head satisfies it.
— Codex review
R0 re-review on
|
* absence-scan: standalone port of the fixture-hygiene scanner + tests 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 #302, Ref #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> * absence-scan test: port fork-main's spawn-env hardening verbatim 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> * scope the suite to the tool, not to the host repo's data 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. * absence-scan: the review thread's boundary fix and the source-file widening Both changes were agreed in this PR's review exchange and neither was in the pushed ref — including on #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 --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Directive only — no implementation here. Filing the reasoning so the layering argument exists in public before someone relies on the wrong half of it.
The correction this exists to make
"CI will catch it" is the assumption I was operating on, and it is wrong about prevention:
pull_requestA fork PR's diff is public on github.com the instant it opens; CI starts afterward. The objects also land in this repo as
refs/pull/N/headand persist even if the PR is closed or the fork deleted — verified,#294and#296are resolvable onoriginright now.CI is still worth having. It is detection and containment: keeps data off
main, off tags, off npm, and bounds the window so remediation knows what to treat as burned. It is not a gate.Why the tension is permanent
Testing this proxy well requires real Claude Code traffic. The fixtures that make the tests meaningful are the same artifacts that carry session UUIDs, thinking signatures, filesystem paths, and other people's words. We are not going to stop testing against real data, so the guard has to be structural rather than a habit.
Three instances have already reached public history:
test/fixtures/cc-transcript-shape-snapshot.json— our own commit,16ad235(feat: JSONL session-content mirror (P1, refs CC#66734 + CC#66486) #221), onmainsince 2026-06-12 (test/fixtures/cc-transcript-shape-snapshot.json carries capture-derived identifiers (public history) #292)claude-code-meter#19— remediated by rotating the host, because history cannot be scrubbedThe first one is ours. This is not a contributor-hygiene problem.
Deliberately not writing a scanner
@Gunther-Schulz already has
tools/absence-scan.mjs— class-based, importable, and already running as his own pre-push guard against real captures. Per the anti-bloat lens, a second implementation needs justification and there is none. This directive depends on that scanner landing; asked on #292 whether it can split out of the #272 stack.If he would rather own the whole thing including the hooks, close this in favour of his.
Two constraints worth reading
Scan the diff, not history. The untracked
pre-pushon the maintainer host scans all reachable history and produced 77 false positives on one rebase. A noisy guard gets--no-verify'd and then protects nothing.Report classes and counts, never values. A guard that echoes what it found into a terminal, a CI log, or a PR comment has published it a second time. #292 was reported that way and it is the right pattern.
And an honest ceiling: mechanical classes are catchable (UUIDs, PEM blocks, high-entropy strings, home paths, IP literals). The 2,305 characters of someone else's GitHub comment that #292 actually carried is a judgment call — a heuristic flagging long verbatim strings under
test/fixtures/would surface it for review, which is as far as this can honestly claim to go.Non-Functional Requirements
pre-pushwrapper, an installer, one CI step, one docs section. Materially larger means the scanner boundary was violated.Open questions in the directive
core.hooksPathacceptable? It replaces.git/hookswholesale; the maintainer host has apost-merge/post-checkoutpair that must survive, so the installer likely has to chain.Refs #292, #272
— Proxy Builder