Skip to content

Commit 8ae3b8d

Browse files
committed
fix(tooling): isolate git children from ambient GIT_* and make a shared core.bare flip loud
A gate self-test that shells out to `git` inherited the repository location a hook exports (`GIT_DIR` / `GIT_WORK_TREE` / `GIT_INDEX_FILE`), which outranks `cwd`: its `git add -A` staged 8,190 paths as deleted in the real index and its `git init` wrote `core.bare = true` into the SHARED `.git/config`, breaking the primary checkout for every agent on the box. Every case still printed `ok`. - scripts/git-env.mjs: the class rule in one place. `gitFreeEnv()` for a git child that must stay inside its own `cwd`, `withoutGitEnv()` for a call one frame down that passes no environment of its own, and the boundary stated where it can be read: never strip for a child that fetches or pushes, because GIT_CONFIG_* and GIT_SSL_* carry the transport configuration. Its self-test reproduces the leak against real git (red) before proving the strip (green). - scripts/symbol-anchors.mjs: `trackedFiles` passes an explicit stripped environment, so a sweep of a synthetic root can no longer answer with the real repository's file list. Pinned by a self-test case that injects a bogus GIT_DIR and requires the sweep to resolve its own tree; before the change that case threw. - The two corpus gates built on that resolver build their fixture repositories the same way, so both spawn stripped too. - scripts/check-system-context-census.mjs drops its local copy of the strip and imports the shared one; its regression pin is unchanged. - scripts/setup-git-hooks.mjs carries the tripwire. In the flipped state no commit is possible, so no hook can be the alarm; `pnpm install` is the first thing that runs after the damage. It warns there and refuses under `--self-test`. Measured: `rev-parse --is-inside-work-tree` prints `false` and exits 0 under the flip, so the predicate reads `core.bare` on the shared common dir, which is also the only reading a linked worktree can take. Refs #16624 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
1 parent 8341ed2 commit 8ae3b8d

7 files changed

Lines changed: 607 additions & 17 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"clean": "turbo run clean && rm -rf dist",
1717
"setup": "pnpm install && pnpm --filter @objectstack/spec build",
1818
"prepare": "node scripts/setup-git-hooks.mjs",
19-
"check:merge-driver": "node scripts/git-merge-regen.mjs --self-test && node scripts/check-regen-pending.mjs --self-test",
19+
"check:merge-driver": "node scripts/git-env.mjs --self-test && node scripts/git-merge-regen.mjs --self-test && node scripts/check-regen-pending.mjs --self-test",
2020
"version": "changeset version && node scripts/sync-protocol-version.mjs && node scripts/sync-template-versions.mjs && node scripts/sync-docs-image-tags.mjs && node scripts/sync-release-index-currency.mjs",
2121
"release": "pnpm run build && bash scripts/build-console.sh && bash scripts/release-publish.sh",
2222
"docs:dev": "pnpm --filter @objectstack/docs dev",

scripts/check-adr-symbol-anchors.mjs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
8383
import { tmpdir } from 'node:os';
8484
import { dirname, join } from 'node:path';
8585

86+
import { gitFreeEnv } from './git-env.mjs';
8687
import { isEntrypoint } from './invoked-as.mjs';
8788
import { ANCHOR_GRAMMAR, defineCorpus, sweepCorpus } from './symbol-anchors.mjs';
8889

@@ -239,9 +240,15 @@ export function selfTest() {
239240
'A bogus exemption `src/thing.ts:99` <!-- anchor-exempt: NOPE --> must be found.',
240241
].join('\n'));
241242
write('docs/adr/0003-exempt.md', 'An excused anchor `src/gone.ts:7` <!-- anchor-exempt: HISTORICAL --> is silent.');
242-
// the sweep reads `git ls-files`, so the fixture needs to be a repo
243-
execFileSync('git', ['init', '-q'], { cwd: tmp });
244-
execFileSync('git', ['add', '-A'], { cwd: tmp });
243+
/* The sweep reads `git ls-files`, so the fixture needs to be a repo — and
244+
* both children get an EXPLICIT, GIT_*-stripped environment (#16624). A
245+
* hook exports `GIT_DIR` into everything it runs and that outranks `cwd`,
246+
* so an inheriting `git init` here creates nothing and the inheriting
247+
* `git add -A` writes THE REPOSITORY's index instead. Measured on this
248+
* repo, from `pre-commit`: 8,190 paths staged as deleted, and `core.bare`
249+
* written into the SHARED `.git/config`, by a self-test that printed `ok`. */
250+
execFileSync('git', ['init', '-q'], { cwd: tmp, env: gitFreeEnv() });
251+
execFileSync('git', ['add', '-A'], { cwd: tmp, env: gitFreeEnv() });
245252

246253
const { findings, counts } = sweepCorpus(CORPUS, tmp);
247254
const kinds = findings.map((f) => f.kind);

scripts/check-scripts-symbol-anchors.mjs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
125125
import { tmpdir } from 'node:os';
126126
import { dirname, join } from 'node:path';
127127

128+
import { gitFreeEnv } from './git-env.mjs';
128129
import { isEntrypoint } from './invoked-as.mjs';
129130
import { ANCHOR_GRAMMAR, commentProse, defineCorpus, sweepCorpus } from './symbol-anchors.mjs';
130131

@@ -362,9 +363,15 @@ export function selfTest() {
362363
'// A bare path code span `some/abbreviated/spelling.ts` is NOT judged here.',
363364
].join('\n'));
364365
write('scripts/exempt.mjs', '// An excused anchor `src/gone.ts:7` <!-- anchor-exempt: HISTORICAL --> is silent.');
365-
// the sweep reads `git ls-files`, so the fixture needs to be a repo
366-
execFileSync('git', ['init', '-q'], { cwd: tmp });
367-
execFileSync('git', ['add', '-A'], { cwd: tmp });
366+
/* The sweep reads `git ls-files`, so the fixture needs to be a repo — and
367+
* both children get an EXPLICIT, GIT_*-stripped environment (#16624). A
368+
* hook exports `GIT_DIR` into everything it runs and that outranks `cwd`,
369+
* so an inheriting `git init` here creates nothing and the inheriting
370+
* `git add -A` writes THE REPOSITORY's index instead. Measured on this
371+
* repo, from `pre-commit`: 8,190 paths staged as deleted, and `core.bare`
372+
* written into the SHARED `.git/config`, by a self-test that printed `ok`. */
373+
execFileSync('git', ['init', '-q'], { cwd: tmp, env: gitFreeEnv() });
374+
execFileSync('git', ['add', '-A'], { cwd: tmp, env: gitFreeEnv() });
368375

369376
const { findings, counts, declined } = sweepCorpus(CORPUS, tmp);
370377
const kinds = findings.map((f) => f.kind);

scripts/check-system-context-census.mjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ import { tmpdir } from 'node:os';
213213
import { join } from 'node:path';
214214
import { fileURLToPath } from 'node:url';
215215

216+
import { gitFreeEnv } from './git-env.mjs';
216217
import { isEntrypoint } from './invoked-as.mjs';
217218
import { CORPUS_ROOTS, runCensus, symbolPopulation } from './isystem-census.mjs';
218219
import {
@@ -1688,15 +1689,14 @@ function fixturePage({ anchor = 'pkg/a.ts#handler', helper = 'pkg/a.ts#isSystemO
16881689
* stripped for the duration of the self-test AND passed stripped to each child
16891690
* here, rather than relying on either one alone.
16901691
*
1692+
* ⭐ The strip itself now lives in `scripts/git-env.mjs`, so the rule this gate
1693+
* discovered has ONE spelling for the whole repo rather than a copy per gate
1694+
* that learns it (#16624). The local copy that used to sit here is gone; what
1695+
* stays here is the pin below, because the pin is about THIS gate's children.
1696+
*
16911697
* @param {{ symbol?: string, pad?: number }} shape
16921698
* @returns {{ dir: string, sourceLine: number }}
16931699
*/
1694-
function gitFreeEnv() {
1695-
const env = { ...process.env };
1696-
for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
1697-
return env;
1698-
}
1699-
17001700
function buildRedFirstCorpus({ symbol = 'handler', pad = 0 } = {}) {
17011701
const dir = mkdtempSync(join(tmpdir(), 'system-context-corpus-'));
17021702
mkdirSync(join(dir, 'content', 'docs', 'permissions'), { recursive: true });

0 commit comments

Comments
 (0)