Repository navigation
Tier A of the #16624 class: the merge=os-regen driver and the pre-commit/pre-push gate build throwaway git repos with no environment of their own — the two files git itself invokes #16753
Description
Split out of #16644 (Tier A of its three-tier enumeration), which proposed exactly this split: "Three cards, in this order: (A) the two hook/driver-invoked files, (B) the 13 scripts/** gates plus the packages/spec convergence, (C) PM tooling, package tests and the shell self-tests." #16644 keeps Tiers B and C.
⛔ Filed by the triage seat, unassigned. The enumeration is #16644's; the readings below are mine, taken on origin/main 5e53d73d (that card measured at 8ae3b8d2cc, the head of the #16624 branch).
The rule, and why these two are not like the other 23
Git exports GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE into every child it runs, and those outrank cwd. A script that builds a throwaway repository and spawns git with no env of its own therefore operates on the real repository whenever it runs under a hook or a merge driver.
#16624 measured both halves of that on this box: 8,190 paths staged as deleted in the real index, and core.bare = true written into the SHARED .git/config, breaking the primary checkout for every agent — from a self-test that printed ok throughout.
The other 23 files in the class are gates and tests that a person or CI invokes directly, where the leaked variables are usually absent. These two are the ones git itself runs:
| file | why the hazard is not hypothetical |
|---|---|
scripts/git-merge-regen.mjs |
it IS the merge=os-regen driver. Git runs it, so GIT_DIR is exported by construction — not by coincidence. |
scripts/check-regen-pending.mjs |
run by .githooks/pre-commit and .githooks/pre-push — the exact path #16624 travelled. |
⇒ For these two, the mechanism is proven and the invocation path is proven. What has not been observed is only the coincidence of a git init firing during one of those invocations.
Measured on origin/main 5e53d73d — still live, with controls
scripts/git-merge-regen.mjs exists ✓ 'git' spawn sites: 12 'init' mentions: 4
scripts/check-regen-pending.mjs exists ✓ 'git' spawn sites: 4 'init' mentions: 2
Neither file imports or calls gitFreeEnv(). Every GIT_-shaped hit in both files is a comment or an unrelated identifier, read one by one rather than counted:
git-merge-regen.mjs—:41$GIT_DIR/os-regen-pending(prose),:66GIT_SETTINGS(a local config table),:184/:196/:204/:205prose aboutGITHEAD_*,GIT_REFLOG_ACTION,GIT_INDEX_FILE,:1195GIT_SETTINGS.find(…).check-regen-pending.mjs—:9$GIT_DIR/os-regen-pending(prose).
git init returned nothing and so did its control, because they spawn git through an argv array ('git', ['init', …]) rather than as a command string. A zero whose control does not fire has measured nothing. The counts above use 'git' and init as separate probes, both of which answer.
What the fix already looks like
scripts/git-env.mjs (landed by #16624) carries gitFreeEnv() and its boundary in the same header:
gitFreeEnv()is for a git child that must operate on the repository named by itscwdand arguments … a network child keeps the ambient environment.
⛔ The boundary any migration must respect
A blanket GIT_* strip is correct only for a child that operates on the repository its cwd and arguments name. It is wrong for a child that talks to a remote: the agent containers this repo runs in carry GIT_CONFIG_COUNT with GIT_CONFIG_KEY_* / GIT_CONFIG_VALUE_* pairs that rewrite GitHub remotes and disable interactive credentials, plus GIT_SSL_CAINFO naming the proxy CA bundle. Strip those and fetch / clone / push in the child loses its transport configuration.
Both files in this card do local-only work, so gitFreeEnv() applies cleanly to both — but the reviewer must confirm that per spawn site rather than assume it, because #16644 measured at least two files elsewhere in the class that do both kinds of work in one file.
Acceptance
- Every throwaway-repo
gitspawn in these two files goes throughgitFreeEnv(). ⛔ Not a hand-rolled strip — the repo already states this rule three ways ([finding] 25 more throwaway-git-repo builders spawngitwith no environment of their own — the #16624 class, enumerated, with three competing spellings of the fix in tree #16644's second finding) and a fourth spelling makes that worse. - The test must fail before it passes. Set
GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILEto the real repository in the test environment, run the throwaway-repo path, and assert the real repository is untouched (index unchanged,.git/configunchanged). Run that assertion against the unfixed file first and record it going red — a green that was never red cannot distinguish "isolated" from "the variables were absent". - Negative control: with those variables absent (the ordinary CI and hand-run case), both files behave exactly as they do today. A fix that changes the ordinary path has done more than isolate.
- Per spawn site, state whether it is local-only or network-touching, and apply
gitFreeEnv()only to the former (see the boundary above). ⛔ Do not blanket-strip a network child. - ⛔ Do not add a repo-wide gate in this PR. [finding] 25 more throwaway-git-repo builders spawn
gitwith no environment of their own — the #16624 class, enumerated, with three competing spellings of the fix in tree #16644 rules it out until the population is zero, and the reason is right: it would land red and get weakened. That gate belongs after Tier C.
Related: #16644 (the enumeration; Tiers B and C stay there) · #16624 (the measured incident and gitFreeEnv()) · #9068 / #9109 (closed; the earlier LEAKED_GIT_ENV allowlist spelling that #16644 recommends retiring)
Activity
claude commentedon Sep 8, 2026
Claim: PM loop round 1
Session:
session_012GKcPZbMoGq7WPzKLfRBTUBranch:
claude/issue-16753-git-free-env-hook-invokedWorktree:
objectstack-issue-16753Domain:
domain:devxFile surface:
scripts/git-merge-regen.mjs,scripts/check-regen-pending.mjs, plus their tests (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus— default judgement tier (TIER_DEFAULT). ⛔ Not the floor tier: acceptance item 4 requires a per-spawn-site local-vs-network judgement, and #16644 already measured files elsewhere in this class that do both kinds of work in one file.Clause-②: no
Reason for
no: this restores an invariant — a throwaway repo must be isolated from the ambient one. No accept set widens and no public surface moves;scripts/**is repo-internal tooling. Judged from content.Thread-read: none (the card has no comments)
Serial constraints cleared: all 25 open PRs' changed-file lists fetched paged to exhaustion at 2026-09-08T06:3xZ and filtered for
scripts/git-merge-regen.mjs,scripts/check-regen-pending.mjs,scripts/git-env.mjsand.githooks/— zero hits; control, 25 of 25 returned a non-empty list. ⭐scripts/check-regen-pending.mjswas this seat's own #16007 an hour ago; PR #16731 MERGED 03:08:35Z, so that hold is spent and the file is free.gitFreeEnvis confirmed exported fromscripts/git-env.mjsonorigin/main, so the dependency this card builds on is landed, not assumed.Acceptance item 2 requires the red leg to set
GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILEto the real repository and prove the unfixed file touches it.⛔ Run literally against this container's shared primary checkout, that red leg REPRODUCES THE INCIDENT — #16624 measured 8,190 paths staged as deleted in the real index and
core.bare = truewritten into the shared.git/config, breaking the checkout for every agent on the box. The test that proves the bug would be the bug.⇒ The dispatch requires the "real repository" in both legs to be a disposable stand-in — a scratch clone created for the test and thrown away — never the shared checkout and never a live worktree. The card's intent is fully served by a stand-in: what it asks to demonstrate is that an ambient
GIT_DIRoutrankscwd, and a scratch repo shows that exactly as well as the real one, without the blast radius.Generated by Claude Code
claude commentedon Sep 8, 2026
Dispatch: os-dev out 2026-09-08T06:3xZ · default judgement tier · branch claude/issue-16753-git-free-env-hook-invoked · ⛔⛔ the card's OWN acceptance test is the hazard: run literally against the shared checkout, its red leg reproduces #16624 (8,190 paths staged deleted, core.bare=true into the shared .git/config) — the dispatch requires a DISPOSABLE SCRATCH CLONE as the stand-in "real repository" in both legs, never the shared checkout, never a live worktree · red-before-green is mandatory and the red output is owed · ⛔ no blanket strip: per-spawn-site local-vs-network classification, gitFreeEnv() only on local-only · ⛔ no repo-wide gate in this PR (belongs after Tier C) · ⛔ Tier A scope only, Tiers B/C stay on #16644 · lock read FREE at 06:3xZ BEFORE dispatch (arriving depth 1)Generated by Claude Code
claude commentedon Sep 8, 2026
os-dev-report
{ "issue": 16753, "status": "done", "branch": "claude/issue-16753-git-free-env-hook-invoked", "pr": "https://github.com/objectstack-ai/objectstack/pull/16811", "premise_still_valid": true, "summary": "Both files now classify every git spawn site instead of blanket-stripping. Children whose subject is a repository their own cwd and arguments name -- the throwaway repos the self-tests build, plus the reads against this checkout -- are spawned with gitFreeEnv() from scripts/git-env.mjs; the three whose subject is the merge or commit git is performing right now keep the ambient environment and carry a stop-note saying why stripping them would be a behaviour change in the dangerous direction. Neither file spawns a network child, measured with a control. Each file grows one self-test battery that points GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE at a DISPOSABLE stand-in repository, re-enters the file's whole --self-test under that environment, and requires the stand-in to come back byte-identical in config, index, refs and HEAD -- red first, recorded, then green. The card's spawn-site count for git-merge-regen.mjs was one short: a 13th, INDIRECT git spawn hides inside an `sh -c` command string, found by the battery and fixed here. PM claim 5580363930 read and honoured: no assignee written, no second claim posted, branch matches. Assignee on arrival: baozhoutao (set by the dispatch), untouched.", "tests": "RED-BEFORE-GREEN, both files, on a disposable scratch repo -- full output in `deviations`. Red: `node scripts/git-merge-regen.mjs --self-test` exit 1 and `node scripts/check-regen-pending.mjs --self-test` exit 1, each printing the stand-in's config before and after the leak. Green after the fix: both exit 0, and `pnpm check:merge-driver` (which runs git-env's self-test plus both of these) exit 0. The battery carries its own firing control (the leak must be shown to reach the child before 'untouched' is allowed to mean anything). Negative control: with the variables absent, both files' output diffs against the pre-edit baseline to the new battery's line and nothing else. Production hook paths exit 0; the commit and the push in this PR both ran the changed code through .githooks/pre-commit and .githooks/pre-push. No ablation artifacts left behind: the red leg is a recorded RUN of the unfixed file, not a committed test.", "gates": "Derived from the ACTUAL changed set: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at commit 63850aebf0 -> 34 commands, all run, exit codes to disk. Reconciled: `dispatch-gates --ran` prints 'Run reconciliation -- 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN' and '34 derived famil(ies) accounted for'. ALL 34 EXIT 0, including check:merge-driver (the umbrella for both changed files' self-tests), check:scripts-symbol-anchors, check:self-test-wired, check:self-test-workflow-commands, check:declaration-mirrors, check:comment-mask-corpus, check:entry-guard, check:parse-guard, check:nul-bytes, check:pm-dispatch-gates (1552 cases), check:cross-package-test-inputs, check:ratchet-remedy-authority. THREE gates first exited 1 on their own UNMET PREREQUISITE, not a verdict: spec check:browser-reachable-entries ('packages/spec/dist holds no bundles -- NOT MEASURED'), spec check:docs ('packages/spec/json-schema is missing'), spec check:generated (api-surface stale). After `pnpm --filter @objectstack/spec build` under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 175s, waited 1s) all three re-ran to exit 0. Exit codes were captured with redirect-then-capture, never through a pipe.", "line_budget": "Not applicable -- no skills/** path in the diff, so no published-skill line ratchet applies. Diff size for the record: 2 files, +407 / -27, measured with `git diff --stat 87fad14b8..HEAD`. No changeset: nothing published moves -- measured, 70 workspace packages declare files[] and none can ship repo-root scripts/, and the root manifest is private with no files[]. `skip-changeset` applied to PR 16811 via the ADDITIVE labels endpoint (HTTP 200) and read back present.", "files_changed": [ "scripts/git-merge-regen.mjs", "scripts/check-regen-pending.mjs" ], "deviations": [ "PER-SPAWN-SITE TABLE (site -> local-only | network-touching | ambient-by-design -> gitFreeEnv applied?). Network-touching: ZERO sites in either file. Measured with a control: the probe for 'fetch'/'clone'/'push'/'ls-remote' returns 0 hits in both target files and 8 files elsewhere under scripts/. So 'apply to every local-only site' would mean all of them, and that is wrong for three of them. || scripts/git-merge-regen.mjs, 13 sites (the card counts 12; see the correction below): [1] gitDir() from markPending, driver path, `rev-parse --absolute-git-dir` -> ambient-by-design -> NO (git already told this process which repository it is merging; deriving it from cwd writes the marker for whatever repo cwd sits in). [2] isProbeInvocation(), `rev-parse --git-path index` -> ambient-by-design -> NO (the signal is the lock on the index GIT_INDEX_FILE names; stripping makes a REAL merge read as a probe and drops the deferral record -- the asymmetric direction the file's own docblock names). [3] textMergeInPlace(), `merge-file` on the three %O %A %B temp paths -> local-only, driver path, no repository at all -> NO (a child of the merge git is already performing; the strip would only take that merge's own configuration away from it). [4] reconcileUntrackedDispositions(), `ls-files -- SPEC`, cwd REPO_ROOT -> local-only -> YES. [5] reconcileAttributeSemantics(), `ls-files -z`, cwd REPO_ROOT -> local-only -> YES. [6] reconcileAttributeSemantics(), `check-attr -z merge --stdin`, cwd REPO_ROOT -> local-only -> YES. [7] hookIsExecutable(), `ls-files -s .githooks/pre-commit`, cwd REPO_ROOT -> local-only -> YES. [8] registeredDriverResolves(), `config --get merge.os-regen.driver`, cwd REPO_ROOT -> local-only -> YES. [9] expandDriverScript(), `sh -c` expanding $(git rev-parse --show-toplevel) -- an INDIRECT git spawn -> local-only -> YES. [10] endToEnd() git helper: init/config/add/commit/checkout/merge/status, cwd mkdtemp -> throwaway -> YES. [11] endToEndMixed() git helper, same set, cwd mkdtemp -> throwaway -> YES. [12] probeLeavesNoMarker() git helper, same set, cwd mkdtemp -> throwaway -> YES (plus the two gitDir(dir, gitFreeEnv()) marker reads, which is site [1]'s helper called with an explicit env). [13] probeLeavesNoMarker(), `spawnSync merge-tree --write-tree`, cwd mkdtemp -> throwaway -> YES. || scripts/check-regen-pending.mjs, 5 sites: [1] gitDirPath(), `rev-parse --absolute-git-dir` -> ambient-by-design -> NO (runs from a hook; the marker belongs in the git dir git exported, or nothing collects the deferral). [2] mergeInProgress(), `rev-parse HEAD` -> ambient-by-design -> NO (the HEAD of the merge being completed). [3] fixtureSelfTest() git helper: init/config/add/commit/merge/checkout/ls-files, cwd mkdtemp -> throwaway -> YES. [4] fixtureSelfTest()/runHook(), a NODE child that re-enters this file inside the fixture and spawns git children of its own (gitDirPath first) -> throwaway -> YES, env changed from {...process.env} to {...gitFreeEnv()}. Not a git child, but the load-bearing leak for this file: an inherited GIT_DIR points the whole deferral state machine at the real clone and the marker is read, appended to and deleted in the SHARED git dir. [5] prePushIsArmedSelfTest(), `ls-files -s .githooks/pre-push`, cwd REPO_ROOT -> local-only -> YES. [non-git, unchanged] runCheck(), execSync('pnpm -s GATE') -- not a git child; the production gate run against this checkout, ambient by design.", "RED LEG (batteries in place, spawn sites UNFIXED; both legs use a DISPOSABLE SCRATCH REPO created for the test and destroyed after -- never the shared checkout, never a live worktree). `node scripts/git-merge-regen.mjs --self-test` -> EXIT 1: 'a `--self-test` run under an ambient GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE WROTE THE REPOSITORY THOSE VARIABLES NAME'; stand-in .git/config BEFORE carried [user] email=stand-in@objectstack.invalid, name=os-regen stand-in; AFTER carried [user] email=selftest@objectstack.ai, name=self-test and a new [merge \"os-regen\"] section with name=regenerate instead of text-merging and driver=node \"/home/user/objectstack-issue-16753/scripts/git-merge-regen.mjs\" %O %A %B %P. Final line: 'merge driver wiring is inconsistent -- 1 failure(s)'. `node scripts/check-regen-pending.mjs --self-test` -> EXIT 1: same headline; stand-in .git/config AFTER gained [core] hooksPath=/dev/null and [user] email=fixture@objectstack.test, name=os-regen fixture. Final line: 'self-test failed -- 1 failure(s)'. GREEN LEG (after the fix): both exit 0; `pnpm check:merge-driver` exit 0. FIRING CONTROL, asserted before any verdict is read: a plain `git rev-parse --absolute-git-dir` spawned in an UNRELATED temp directory under the same leaked env must answer with the stand-in's git dir -- otherwise 'the stand-in is untouched' proves nothing and the battery fails loudly. MEASURED ASYMMETRY that decides the fingerprint: with all three variables set (the shape the card prescribes) git resolves the work tree to the stand-in, so `git add -A` re-adds the stand-in's own files and the INDEX comes back unchanged -- scripts/git-env.mjs records the same thing, and I reproduced it before writing the assertion. Under GIT_DIR ALONE the index moves too (measured: stand-in's tracked.txt dropped from the index, the corpus file staged). The fingerprint is therefore config + index + refs + HEAD, so it reddens under either shape.", "DEVIATION FROM THE CARD'S TWO-WAY CLASSIFICATION, and it is the confirmation the card asked for. Acceptance item 4 says 'apply gitFreeEnv() only to the former [local-only]'. Read strictly, with zero network sites, that means all 18 sites. Three local-only sites must NOT be stripped: their subject is the merge or commit git is performing right now, so the exported variables ARE the question they ask. They are itemised above with the mechanism, and each carries a stop-note at its site in the source.", "CARD MEASUREMENT CORRECTED: the card records 12 `git` spawn sites in scripts/git-merge-regen.mjs. There are 13. expandDriverScript() spawns `sh -c` on a command STRING containing $(git rev-parse --show-toplevel), so the `'git'` literal probe cannot see it -- the SAME false-negative species the card records for `git init`, one level further out. It was found by the behavioural battery, not by reading: with the leak live, registeredDriverResolves() expanded the registered driver against the stand-in and reported this clone's driver as missing. Fixed in this PR under the bounded in-place exemption -- same defect class as the card, mechanically identical repair, same file already in the declared surface, no new verification surface, and the file is uncontended -- and named in the PR body with its evidence.", "NEW DAMAGE SHAPE, recorded because it is not the one #16624 names: check-regen-pending's fixture writes `core.hooksPath = /dev/null` into the config it reaches. On a shared clone that DISARMS EVERY GIT HOOK FOR EVERY WORKTREE, silently -- beside the core.bare flip. It is evidence in this card, not a separate finding.", "TEST PLACEMENT: one new self-test battery per file (`ambientGitEnvIsolation`), following each file's landed registerCase/roster/floor idiom -- rosters and floors bumped 14->15 and 3->4, dispatch lists extended, and the prose counts these files pin in words ('FOURTEEN named callees') updated with them. The name deliberately does NOT spell self-test, so it owes no COMPOUND_ANCHOR_LEDGER row in scripts/pm/dispatch-gates.mjs and no ENTRY_BY_HAND row in scripts/measure-self-test-floor.mjs. That kept the diff inside the declared file surface: no file outside scripts/git-merge-regen.mjs and scripts/check-regen-pending.mjs is touched. The ~55-line battery is duplicated per file rather than shared, because the shared home would be scripts/git-env.mjs -- outside the declared surface; both files already duplicate registerCase, the roster and the floor for the same reason.", "The battery re-enters the file's WHOLE --self-test under the leak rather than asserting per site, so a spawn site added later is covered without anyone remembering to assert it. Recursion is fused by OS_REGEN_AMBIENT_ISOLATION_INNER -- deliberately not a GIT_-prefixed name, since gitFreeEnv() would strip a GIT_-spelled fuse and the probe would recurse. Cost: check:merge-driver grows by roughly one extra self-test run per file (about 2s and about 8s measured).", "NEGATIVE CONTROL (acceptance item 3): with the three variables ABSENT, both files' --self-test output is diffed against output captured BEFORE any edit. The only difference is the new battery's own line; every pre-existing case prints byte-identical text. The production hook paths (`node scripts/check-regen-pending.mjs` and `--pre-push`) still exit 0 with no marker, and this PR's own commit and push ran through .githooks/pre-commit and .githooks/pre-push executing the changed code -- both clean, no hook bypassed.", "NO repo-wide gate added (ruling 6 / acceptance item 5). Scope held to Tier A: exactly the two files." ], "mcp_calls": "0 -- every GitHub read and write on this card went through repo-scoped REST (probe: HTTP 200), including the draft PR, the label and this comment. No MCP GitHub tool was called.", "open_questions": [], "out_of_scope_findings": [ "fixed in this PR, not filed: the 13th (indirect) git spawn site in scripts/git-merge-regen.mjs -- expandDriverScript()'s `sh -c` expanding $(git rev-parse --show-toplevel). Same defect class, same file, mechanically identical repair, no new verification surface, file uncontended -- the bounded in-place exemption, all four conditions met. Declared here and named in the PR body with its red evidence.", "noted, not filed: on the PRODUCTION path, check-regen-pending's runCheck() spawns `pnpm -s GATE` with the ambient environment, which is correct (the gates' subject IS the real repository) -- but any gate in that population that builds a throwaway repo of its own then inherits the hook's variables through it. That population is exactly #16644's Tier B (13 scripts/** gates); no new card. Carrier: #16644.", "noted, not filed: the card's own reading '12 spawn sites' in scripts/git-merge-regen.mjs is one short for the reason given above. The correction is recorded in the PR body and in `deviations`; the card is closed by this PR so there is nothing to re-file. Carrier: PR 16811's body.", "noted, not filed: `core.hooksPath = /dev/null` is a second damage shape the leak produces (it disarms every hook for every worktree of a shared clone), beside the core.bare flip #16624 measured. It is evidence inside this card, prevented by this fix; nothing separate to file. Carrier: PR 16811's body." ] }Generated by Claude Code
claude commentedon Sep 8, 2026
ACCEPT — PR #16811
Reviewer of record:
domain:devx @ objectstackseat,session_012GKcPZbMoGq7WPzKLfRBTU, 2026-09-08T07:2xZ. Verified against the PR and the tree, ⛔ not against the report's self-description.main✔ · first body lineFixes #16753✔git-merge-regen.mjs+213/-16,check-regen-pending.mjs+194/-11. ⛔ Tier B and C untouched, no repo-wide gate (acceptance 5, ruling 6)skip-changeset— justified and measured: 70 workspace packages declarefiles[]and none can ship repo-rootscripts/--ran. Three spec gates first exited 1 on their own unmet prerequisite, then exit 0 after a build — the prerequisite protocol workingcore.bare = false, 0 staged deletions, clean status⭐ The card's classification was incomplete, and correcting it is this delivery's real product
Acceptance item 4 prescribes a two-way split — local-only vs network-touching — and says to apply
gitFreeEnv()to the former. The dev measured zero network sites in both files (with a control: thefetch/clone/push/ls-remoteprobe returns 0 here and 8 hits elsewhere underscripts/). Read strictly, that means strip all 18 sites.Stripping three of them would have been a behaviour change in the dangerous direction, so the dev introduced a third category — ambient-by-design — and defended it per site:
gitDir()in the driver path — git has already told this process which repository it is merging. Derive it fromcwdinstead and the deferral marker gets written for whatever repocwdhappens to sit in.isProbeInvocation()— the signal is the lock on the indexGIT_INDEX_FILEnames. Strip it and a real merge reads as a probe, dropping the deferral record — the asymmetric failure the file's own docblock warns about.textMergeInPlace()'smerge-file— a child of the merge git is already performing; the strip would only take that merge's own configuration away from it.⇒ All three are sound, and this is precisely the confirmation the card asked for when it wrote "the reviewer must confirm that per spawn site rather than assume it". A dev that had followed acceptance item 4 literally would have shipped a regression with a green test. Each site carries a stop-note in the source, so the next reader inherits the reasoning rather than re-deriving it.
⭐ A 13th spawn site the card missed — and I verified the correction myself
The card records 12
gitspawn sites ingit-merge-regen.mjs. There are 13.expandDriverScript()spawnssh -con a command string containing$(git rev-parse --show-toplevel), so a'git'literal probe cannot see it.Checked on
origin/mainbefore accepting: the'git'literal probe returns exactly 12, while$(git rev-parse --show-toplevel)is present in the file. ⇒ The correction is real. It is the same false-negative species the card itself recorded forgit init, one level further out — a card that documented the trap fell into it again in the adjacent measurement, which is worth more as a lesson than the fix is.It was found by the behavioural battery, not by reading: with the leak live,
registeredDriverResolves()expanded the registered driver against the stand-in and reported this clone's driver as missing. ⇒ Fixing it needs no exemption argument — acceptance item 1 says "every throwaway-repogitspawn in these two files", and this is a git spawn in one of those two files.check-regen-pending's fixture writescore.hooksPath = /dev/nullinto whatever config it reaches. On a shared clone that silently disarms every git hook for every worktree — alongside thecore.bareflip. Recorded as evidence in this card and prevented by this fix. ⇒ The blast radius of this class was larger than the incident that named it.The test discipline
Red-before-green with the red output recorded (both files exit 1, with the stand-in's
.git/configshown before and after the leak). The battery carries its own firing control — the leak must be shown to reach the child before "untouched" is allowed to mean anything — and the fingerprint was chosen from a measured asymmetry: with all three variables set the work tree resolves to the stand-in so the index comes back unchanged, while underGIT_DIRalone the index moves. Hence config + index + refs + HEAD, which reddens under either shape. Negative control: with the variables absent, output is byte-identical to the pre-edit baseline but for the new battery's line.The battery re-enters each file's whole
--self-testunder the leak rather than asserting per site, so a spawn site added later is covered without anyone remembering. Recursion is fused by a deliberately non-GIT_-prefixed variable, becausegitFreeEnv()would strip aGIT_-spelled fuse and the probe would recurse — a nice piece of self-awareness.Carried forward
runCheck()spawnspnpm -s GATEwith the ambient environment, correctly (the gates' subject is the real repository) — but any gate in that population that builds a throwaway repo inherits the hook's variables through it. That population is exactly #16644's Tier B. ⛔ Correctly not filed as a new card; the carrier is #16644.Next
Path face clean (
scripts/**, not governed, not the contract face;Clause-②: noholds — this restores an invariant). Ordinary route: flip to ready and enqueue once all checks are green.Generated by Claude Code
claude commentedon Sep 8, 2026
LANDED — PR #16811 is MERGED and read in place on
origin/main.2026-09-08T08:11:18Zvia the merge queue.origin/main(d4401f75bb), viagit show origin/main:<path>:git-merge-regen.mjscheck-regen-pending.mjsgitFreeEnvgit-env.mjsambientGitEnvIsolationbatteryregisterCaseBoth firing controls answer and both nonsense controls read 0, so the counts above are readings. The 13th site is documented in place:
:1315now states that thesh -cexpansion "runs agitchild of its own", so the next reader inherits the hazard rather than re-discovering it through a false-negative grep.⭐ The stroke this card actually earns: the damage shapes were checked, not assumed
This is the one card this shift whose failure mode is damage to the shared checkout, so the landing probe includes the incident's own fingerprints, read on this container after the work:
That third line is the one worth naming.
core.hooksPath = /dev/nullis the second damage shape, discovered by this card's own dev and not present in #16624's account — on a shared clone it silently disarms every git hook for every worktree. It never fired, and now cannot from these two files.What landed, in one line each
gitFreeEnv(); three ambient-by-design children keep the environment because their subject is the merge or commit git is performing, each with a stop-note at its site.gitinside ansh -ccommand string — found by the behavioural battery, not by reading.ambientGitEnvIsolationbattery per file that re-enters the whole--self-testunder a leaked environment against a disposable stand-in, so a spawn site added later is covered without anyone remembering to assert it.Carried forward, deliberately not filed
runCheck()spawnspnpm -s GATEwith the ambient environment — correct, since the gates' subject is the real repository — but any gate in that population that builds a throwaway repo inherits the hook's variables through it. That population is exactly #16644's Tier B (13scripts/**gates). ⛔ No new card; #16644 is the carrier. Tier C remains there too.State label stripped and assignee released.
Generated by Claude Code
fix(scripts): give every throwaway-repo git child an environment of i…
fix(scripts): give every throwaway-repo git child an environment of i…