Filed unassigned while implementing #16624 (branch claude/issue-16624-git-env-isolation-for-gate-self-tests). Observation-class: no failure has been measured in these 25 files. Triage's ruling on #16624 asked for the population to be enumerated as part of remedy 2 — "Several gates build throwaway repos; this is the one that was caught" — and it turned out to be too large for one PR, so the enumeration lands here with a proposed split. Duplicate-searched: #9068 and #9109 cover an earlier, smaller slice of this class (two packages/spec test harnesses) and are both closed; no open card carries this signature.
The rule, restated
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 population, measured
Measured on objectstack-ai/objectstack at 8ae3b8d2cc (the head of the #16624 branch), over every tracked .mjs / .mts / .js / .ts / .sh file that both creates a temp directory and spawns git init against one. 32 files, in three groups:
Blanket strip — 5 (all landed by #16624): scripts/git-env.mjs · scripts/symbol-anchors.mjs · scripts/check-system-context-census.mjs · scripts/check-adr-symbol-anchors.mjs · scripts/check-scripts-symbol-anchors.mjs
Named-key list — 2 (landed by #9068 / #9109, a DIFFERENT spelling): packages/spec/scripts/build-schemas-check-mode.test.ts · packages/spec/scripts/sharded-artifacts.test.ts. Both carry a hand-maintained LEAKED_GIT_ENV array of 7 keys.
No environment at all — 25. Grouped by who invokes them, because that is what decides the risk:
Tier A — invoked BY git, or from a git hook (do these first)
| file |
init sites |
why it is Tier A |
scripts/git-merge-regen.mjs |
3 |
it IS the merge=os-regen driver — git runs it, so GIT_DIR is exported by construction |
scripts/check-regen-pending.mjs |
1 |
run by .githooks/pre-commit and .githooks/pre-push — the exact path #16624 travelled |
Tier B — repo gates under scripts/** (CI and hand runs)
scripts/ablation-dist-preflight.mjs (1) · scripts/check-adr-0087-registration.mjs (6) · scripts/check-bash32-floor.mjs (1) · scripts/check-changeset-no-major.mjs (1) · scripts/check-empty-changeset.mjs (4) · scripts/check-engine-split-ratio.mjs (1) · scripts/check-nul-bytes.mjs (1) · scripts/check-skill-frame-freshness.mjs (1) · scripts/check-type-check-coverage.mjs (1) · scripts/docs-audit/check-drift-comment.mjs (1) · scripts/objectui-changeset-digest.mjs (9) · scripts/objectui-range.mjs (1) · scripts/collect-release-notes.sh (1)
Tier C — PM tooling, package tests, agent-hook self-tests
scripts/pm/dispatch-gates.mjs (8) · scripts/pm/check-governed-merges.mjs (1) · scripts/pm/git-history.mjs (1) · scripts/pm/release-rehearsal-clone.mjs (1) · scripts/pm/os-regen-merge.sh (2) · scripts/bump-objectui.selftest.sh (2) · .claude/hooks/guard-main-checkout.selftest.sh (1) · .claude/hooks/guard-main-checkout-bash.selftest.sh (2) · packages/spec/scripts/check-skill-examples.ts (1) · packages/create-objectstack/src/scaffold-skills-single-copy.test.ts (1)
The second finding: three spellings
The repo now states one rule three ways — a 7-key LEAKED_GIT_ENV allowlist (#9068), a local blanket strip (the #16624 incident's own fix), and gitFreeEnv() in scripts/git-env.mjs (#16624). The allowlist shape is the one worth retiring: it has to be maintained against git's own list of location variables, and the key it misses is the key that bites. Converging the two packages/spec files onto gitFreeEnv() is mechanical.
One boundary any migration must respect (measured, and it bites)
⛔ 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. That matters for at least scripts/pm/release-rehearsal-clone.mjs and scripts/objectui-*, which do both kinds of work in one file. scripts/git-env.mjs's header carries the rule and the boundary together.
Suggested 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. A repo-wide gate that refuses a new inheriting spawn is worth considering once the population is at zero, but ⛔ not before — it would land red and get weakened.
Reproducing the enumeration
The classifier is 20 lines: every tracked source file that matches mkdtempSync|mktemp|tmpdir() and has a non-comment line spawning git init, then split on whether the file mentions a blanket strip (startsWith('GIT_') / gitFreeEnv / env -u GIT_ / unset GIT_) or the LEAKED_GIT_ENV allowlist. The ObjectStack CLI's own init command is excluded by name so packages/cli/test/** does not inflate the count.
Generated by Claude Code
Filed unassigned while implementing #16624 (branch
claude/issue-16624-git-env-isolation-for-gate-self-tests). Observation-class: no failure has been measured in these 25 files. Triage's ruling on #16624 asked for the population to be enumerated as part of remedy 2 — "Several gates build throwaway repos; this is the one that was caught" — and it turned out to be too large for one PR, so the enumeration lands here with a proposed split. Duplicate-searched: #9068 and #9109 cover an earlier, smaller slice of this class (twopackages/spectest harnesses) and are both closed; no open card carries this signature.The rule, restated
Git exports
GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILEinto every child it runs, and those outrankcwd. A script that builds a throwaway repository and spawnsgitwith noenvof 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, andcore.bare = truewritten into the SHARED.git/config, breaking the primary checkout for every agent — from a self-test that printedokthroughout.The population, measured
Measured on
objectstack-ai/objectstackat8ae3b8d2cc(the head of the #16624 branch), over every tracked.mjs/.mts/.js/.ts/.shfile that both creates a temp directory and spawnsgit initagainst one. 32 files, in three groups:Blanket strip — 5 (all landed by #16624):
scripts/git-env.mjs·scripts/symbol-anchors.mjs·scripts/check-system-context-census.mjs·scripts/check-adr-symbol-anchors.mjs·scripts/check-scripts-symbol-anchors.mjsNamed-key list — 2 (landed by #9068 / #9109, a DIFFERENT spelling):
packages/spec/scripts/build-schemas-check-mode.test.ts·packages/spec/scripts/sharded-artifacts.test.ts. Both carry a hand-maintainedLEAKED_GIT_ENVarray of 7 keys.No environment at all — 25. Grouped by who invokes them, because that is what decides the risk:
Tier A — invoked BY git, or from a git hook (do these first)
scripts/git-merge-regen.mjsmerge=os-regendriver — git runs it, soGIT_DIRis exported by constructionscripts/check-regen-pending.mjs.githooks/pre-commitand.githooks/pre-push— the exact path #16624 travelledTier B — repo gates under
scripts/**(CI and hand runs)scripts/ablation-dist-preflight.mjs(1) ·scripts/check-adr-0087-registration.mjs(6) ·scripts/check-bash32-floor.mjs(1) ·scripts/check-changeset-no-major.mjs(1) ·scripts/check-empty-changeset.mjs(4) ·scripts/check-engine-split-ratio.mjs(1) ·scripts/check-nul-bytes.mjs(1) ·scripts/check-skill-frame-freshness.mjs(1) ·scripts/check-type-check-coverage.mjs(1) ·scripts/docs-audit/check-drift-comment.mjs(1) ·scripts/objectui-changeset-digest.mjs(9) ·scripts/objectui-range.mjs(1) ·scripts/collect-release-notes.sh(1)Tier C — PM tooling, package tests, agent-hook self-tests
scripts/pm/dispatch-gates.mjs(8) ·scripts/pm/check-governed-merges.mjs(1) ·scripts/pm/git-history.mjs(1) ·scripts/pm/release-rehearsal-clone.mjs(1) ·scripts/pm/os-regen-merge.sh(2) ·scripts/bump-objectui.selftest.sh(2) ·.claude/hooks/guard-main-checkout.selftest.sh(1) ·.claude/hooks/guard-main-checkout-bash.selftest.sh(2) ·packages/spec/scripts/check-skill-examples.ts(1) ·packages/create-objectstack/src/scaffold-skills-single-copy.test.ts(1)The second finding: three spellings
The repo now states one rule three ways — a 7-key
LEAKED_GIT_ENVallowlist (#9068), a local blanket strip (the #16624 incident's own fix), andgitFreeEnv()inscripts/git-env.mjs(#16624). The allowlist shape is the one worth retiring: it has to be maintained against git's own list of location variables, and the key it misses is the key that bites. Converging the twopackages/specfiles ontogitFreeEnv()is mechanical.One boundary any migration must respect (measured, and it bites)
⛔ A blanket
GIT_*strip is correct only for a child that operates on the repository itscwdand arguments name. It is WRONG for a child that talks to a remote: the agent containers this repo runs in carryGIT_CONFIG_COUNTwithGIT_CONFIG_KEY_*/GIT_CONFIG_VALUE_*pairs that rewrite GitHub remotes and disable interactive credentials, plusGIT_SSL_CAINFOnaming the proxy CA bundle. Strip those andfetch/clone/pushin the child loses its transport configuration. That matters for at leastscripts/pm/release-rehearsal-clone.mjsandscripts/objectui-*, which do both kinds of work in one file.scripts/git-env.mjs's header carries the rule and the boundary together.Suggested split
Three cards, in this order: (A) the two hook/driver-invoked files, (B) the 13
scripts/**gates plus thepackages/specconvergence, (C) PM tooling, package tests and the shell self-tests. A repo-wide gate that refuses a new inheriting spawn is worth considering once the population is at zero, but ⛔ not before — it would land red and get weakened.Reproducing the enumeration
The classifier is 20 lines: every tracked source file that matches
mkdtempSync|mktemp|tmpdir()and has a non-comment line spawninggit init, then split on whether the file mentions a blanket strip (startsWith('GIT_')/gitFreeEnv/env -u GIT_/unset GIT_) or theLEAKED_GIT_ENVallowlist. The ObjectStack CLI's owninitcommand is excluded by name sopackages/cli/test/**does not inflate the count.Generated by Claude Code