Repository navigation
fix(tooling): isolate the throwaway git repos in the merge driver and the pre-commit/pre-push gate - #16811
Merged
Conversation
…invoked scripts Git exports GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE into every child it runs and those outrank `cwd`, so a `git init` / `git config` / `git add` aimed at a temp directory lands on the REAL repository whenever these two files run under a hook or as the merge driver. Both are exactly that: git-merge-regen.mjs IS the `merge=os-regen` driver, and check-regen-pending.mjs is what .githooks/pre-commit and .githooks/pre-push invoke. Every spawn site is classified rather than blanket-stripped. Children whose subject is a repository their own `cwd` and arguments name take `gitFreeEnv()` from scripts/git-env.mjs; the three whose subject is the merge git is currently performing keep the ambient environment and carry a note saying why stripping them would be a behaviour change in the dangerous direction. Neither file spawns a network child, measured with a control. A new self-test battery in each file proves it behaviourally: a disposable stand-in repository is created, the three variables are pointed at it, and the file's whole --self-test is re-entered under that environment. The stand-in must come back byte-identical in config, index, refs and HEAD, and a firing control asserts the leaked environment actually reached the child first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
baozhoutao
marked this pull request as ready for review
September 8, 2026 07:49
baozhoutao
enabled auto-merge
September 8, 2026 07:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #16753
Tier A of the #16644 class: the two files git itself invokes built throwaway git repositories with no environment of their own.
scripts/git-merge-regen.mjsis themerge=os-regendriver, so git exportsGIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILEinto it by construction;scripts/check-regen-pending.mjsis what.githooks/pre-commitand.githooks/pre-pushinvoke — the exact path #16624 travelled. Those variables outrankcwd, so agit init/git config/git addaimed at a temp directory lands on the real repository.Tiers B and C stay on #16644. No repo-wide gate here — that belongs after Tier C.
Per spawn site, classified rather than blanket-stripped
Acceptance item 4 asks for a local-only / network-touching call per site. Neither file spawns a network child — measured with a control: zero
fetch/clone/push/ls-remotesites in these two files, non-zero in eight other files underscripts/. So "apply to every local-only site" would mean all of them, and that is wrong: three local-only sites read the ambient repository on purpose, and stripping them is a behaviour change in the dangerous direction. The table below is the confirmation the card asked the reviewer to produce.scripts/git-merge-regen.mjs
gitFreeEnv()gitDir()frommarkPending(driver path)rev-parse --absolute-git-dirisProbeInvocation()rev-parse --git-path indextextMergeInPlace()merge-fileon the three%O %A %Btemp pathsreconcileUntrackedDispositions()ls-files -- SPECreconcileAttributeSemantics()ls-files -zreconcileAttributeSemantics()check-attr -z merge --stdinhookIsExecutable()ls-files -s .githooks/pre-commitregisteredDriverResolves()config --get merge.os-regen.driverexpandDriverScript()sh -cexpandinggit rev-parse --show-toplevel— indirectendToEnd()initconfigaddcommitcheckoutmergestatusendToEndMixed()probeLeavesNoMarker()gitDir(dir, gitFreeEnv())probeLeavesNoMarker()spawnSync merge-tree --write-treescripts/check-regen-pending.mjs
gitFreeEnv()gitDirPath()rev-parse --absolute-git-dirmergeInProgress()rev-parse HEADfixtureSelfTest()initconfigaddcommitmergecheckoutls-filesfixtureSelfTest()/runHook()prePushIsArmedSelfTest()ls-files -s .githooks/pre-pushrunCheck()execSync('pnpm -s GATE')— not a git child; the production gate run against this checkoutWhy the three ambient sites must stay ambient, in one line each:
isProbeInvocation()reads the lock on the indexGIT_INDEX_FILEnames — strip it and a real merge reads as a probe and the deferral's own record is dropped, which the file's own docblock already calls the asymmetric direction;gitDir()on the driver path would otherwise write the marker for whatever repositorycwdhappens to sit in;merge-fileis a child of the merge git is already performing and reading no index, sogitFreeEnv()would only take that merge's configuration away from it.A thirteenth spawn site the card's probe could not see
#16753 counts 12
gitspawn sites ingit-merge-regen.mjs. There are 13.expandDriverScript()spawnssh -con a command string containing$(git rev-parse --show-toplevel), so the'git'literal probe does not find it — the same false-negative species the card records forgit init, one level further out. It was found by the new behavioural battery, not by reading: with the leak live,registeredDriverResolves()expanded the registered driver against the stand-in repository and reported this clone's driver as missing.Red before green, on a disposable stand-in
⛔ Run literally against a shared checkout, this card's own acceptance test reproduces #16624. Both legs therefore use a scratch repository created for the test and thrown away; what is under test is that an ambient
GIT_DIRoutrankscwd, and any real repository shows that.Each file grows one self-test battery,
ambientGitEnvIsolation, which builds the stand-in, points all three variables at it, and re-enters the file's whole--self-testunder that environment. The stand-in must come back byte-identical in config, index, refs and HEAD. It carries a firing control first — a plaingitchild in an unrelated directory must answer with the stand-in's git dir — because "untouched" is also what an environment that never reached the child produces.RED, with the batteries in place and the spawn sites unfixed:
The second one is worth naming: the fixture writes
core.hooksPath = /dev/nullinto the config, and on a shared clone that disarms every git hook for every worktree — a damage shape beside thecore.bareflip #16624 measured.GREEN, after the fix: both exit 0,
pnpm check:merge-driverexit 0.Measured asymmetry, recorded because it decides the fingerprint: with all three variables set, git resolves the work tree to the stand-in, so
git add -Are-adds the stand-in's own files and the index comes back unchanged —scripts/git-env.mjsrecords the same thing. UnderGIT_DIRalone the index moves too (measured here: the stand-in's tracked file dropped, the corpus file staged). The fingerprint is therefore config + index + refs + HEAD, so it reddens under either shape.Negative control
With the variables absent — ordinary CI and hand-run — both files behave exactly as they do today. 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.mjsand--pre-push) still exit 0 with no marker, and this PR's own commit and push ran through.githooks/pre-commitand.githooks/pre-pushexecuting the changed code.Gates
Derived from the actual changed set with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack; every derived command run, exit codes recorded. All green, includingcheck:merge-driver,check:scripts-symbol-anchors,check:self-test-wired,check:nul-bytesandcheck:entry-guard. Threepackages/specgates first exited 1 naming their own unmet prerequisite (packages/spec/distnot built,json-schema/absent) — NOT MEASURED, not a verdict; afterpnpm --filter @objectstack/spec buildall three exit 0.No changeset: nothing published moves. Measured — 70 workspace packages declare
files[]and none can ship repo-rootscripts/; the root manifest is private with nofiles[].Generated by Claude Code