Skip to content

bug(worktrees): a test's git writes hit the live repo under an ambient GIT_DIR, and every fresh worktree starts with 41 stageable mode-only diffs #15246

Closed

Description

@mrveiss

Found while fixing #15176, in two forms — one a latent trap, one an actual corruption that happened.

1. Every fresh worktree starts with 41 uncommitted mode-only diffs

The post-checkout hook chmods 40 hook scripts, so a newly created worktree immediately shows 41 files changed 100644 → 100755 with no content difference.

Any git add -A silently commits them. They do not appear in a diff review — a mode-only change shows as a modified file with no visible content — so they ride along in whatever commit is being made and land in an unrelated PR. With 230+ worktrees in play, and agents that stage broadly, this is a steady low-level source of unexplained diff noise.

2. The same ambient-GIT_DIR defect, with a write instead of a read

autobot_shared/paths_test.py:204-212 — its _git helper passed env={**os.environ, ...}, inheriting an ambient GIT_DIR.

Under the pre-push hook — which exports GIT_DIR when the checkout is a worktree, and every checkout here is — that test's git add -A and git commit -m initial executed against the pushing worktree's own git directory, with a pytest temp directory as the work tree. It wrote a bogus commit onto the live branch that dropped 86 tracked files from the index.

Recovered: no data lost (the affected files are gitignored-but-tracked and were untouched on disk), the branch was reset onto the good commit, and the tree verified back at 9637 tracked files with no other worktree or branch affected. The specific instance is fixed in PR #15243 with the same scrubbed_git_env() helper as the rest of #15176.

It is filed anyway, because the class is worse than the instance. #15176 is about guards reading the wrong root and reporting nonsense. This is a test writing to the wrong repository — and it was in the test file of the very module being changed, reachable by nothing more unusual than pushing from a worktree.

Acceptance criteria

  • A fresh worktree does not start dirty. Either the post-checkout hook stops rewriting modes that are already correct in the index, or the modes are corrected once at source so the hook is a no-op. Establish which before changing anything — a hook that exists to fix a real mode problem should not simply be deleted.
  • git add -A in a fresh worktree stages nothing unexpected. Demonstrate it.
  • Sweep for other tests that run git write commands with an inherited environment. One found while changing an unrelated file is not evidence there is one. Prioritise anything running add, commit, reset, checkout or push.
  • Any such test scrubs the git environment, so a temp-directory repository cannot be confused with the real one.
  • A test that fails if an inherited-environment git write is reintroduced. The contrast mutation for it: restore the inherited env and show it fails.

Refs #15176, PR #15243.

Activity

  1. mrveiss commented on Aug 28, 2026

    @mrveiss
    OwnerAuthor

    The mode noise blocks worktree disposal — sixth independent sighting

    This started as cosmetic. It is not.

    An agent finishing its work tried to remove its own worktree after verifying the content had landed, and git refused:

    fatal: ... contains modified or untracked files, use --force to delete it
    

    The only thing dirtying that tree was the 40 mode-only 100644 => 100755 entries this issue describes. Verified before the decision: 0 added and 0 deleted content lines, 0 untracked files, no non-mode entries in git status. It declined to --force — correctly, since a dirty tree refusing to go is information, and a force would have been indistinguishable from forcing past real uncommitted work.

    So every worktree this hook touches becomes undisposable by the normal command, and the only ways past are --force (which defeats the protection for every future case) or -c core.fileMode=false (which one agent already needed to complete a rebase). That compounds directly with the 230+ worktrees on this machine: work finishes, the tree cannot be cleaned up cleanly, and the count grows.

    Six independent sightings today

    Reported separately by six agents that had no knowledge of each other's runs:

    • three while working unrelated issues, each noting 40–41 mode-only entries in a fresh worktree;
    • one that needed -c core.fileMode=false to rebase past them;
    • one that had to keep them unstaged deliberately to avoid committing them into an unrelated PR;
    • this one, blocked from disposal.

    That spread is the finding. It is not an occasional artefact — it is the default state of every worktree created on this machine, and it interferes with staging, rebasing and cleanup in turn.

    What that adds to the criteria

    The existing criteria ask that a fresh worktree not start dirty. Add:

    • git worktree remove succeeds on a finished worktree without --force. That is the operational symptom, and it is the one that grows the worktree count.
    • Whatever fix lands does not rely on core.fileMode=false as the standing answer. That hides real mode changes as well as spurious ones, and this repository has hook scripts whose executable bit genuinely matters — check_hook_exec_bits.py exists to enforce exactly that.

    Refs #15176, PR #15243.

  2. mrveiss commented on Aug 29, 2026

    @mrveiss
    OwnerAuthor

    Another live instance of the same class, found working #15273 in worktree issue-15273-hardcoded-scope.

    autobot-infrastructure/shared/scripts/hooks/pre-commit-hardcoded-values_test.py has ~20 tests (_run_hook_with_staged, _git) that each do git init / git add in a fresh tmp_path, with no env scrubbing — subprocess.run(["git", *args], cwd=repo, ...), ambient env inherited. tools/git-hooks/pre-push invokes pytest directly (tools/git-hooks/pre-push:190-195) with no unset GIT_DIR/env -u before it, same shape as the paths_test.py instance already fixed in PR #15243.

    Reproduced by git push from a worktree (ambient GIT_DIR pointing at .git/worktrees/<name>): the suite's temp-repo git add calls landed in the real worktree index instead — git status afterward showed ~20 bogus A/AD entries (src/worker.py, deploy.yaml, ssot_config.py, etc., all from the test fixtures) plus two real tracked files (README.md, repo_tests/sdk_request_shared.py) with their index blob overwritten by 1-line test content while the working-tree copy stayed correct (156/461 lines intact — index-only corruption, not a bad commit this time, presumably because no git commit step ran that far before a test failed).

    Recovered with git reset (mixed, no path) — restores the index to HEAD without touching the working tree, which was never touched. No data lost.

    Not fixed here — out of scope for #15273 and #15246's own AC calls for a full sweep rather than a one-off patch. Filing this as evidence for that sweep: pre-commit-hardcoded-values_test.py's _git/_run_hook_with_staged helpers should use the same scrubbed_git_env() helper #15243 introduced.

  3. mrveiss commented on Aug 29, 2026

    @mrveiss
    OwnerAuthor

    Verified in merged Dev_new_gui — closing

    PR #15286 merged as 6147fd069.

    Criterion Verdict Evidence
    A fresh worktree does not start dirty MET Delivered earlier by #15253/#15262 — the post-checkout hook now gates its chmod on a shebang, and 22 stale modes were committed. Established which of the two options applied before changing anything, as this issue required: the hook was fixing a real problem, so the modes were corrected at source rather than the hook deleted
    git add -A in a fresh worktree stages nothing unexpected MET A fresh git worktree add produces 0 porcelain entries, demonstrated at the time and re-verified during this PR
    Sweep for other tests running git writes with an inherited environment MET ~60 candidate files classified by hand; ~27 had genuine unscrubbed writes, all fixed
    Any such test scrubs the git environment MET Via the established scrubbed_git_env() from #15243 — no second mechanism invented
    A test that fails if an inherited-environment write is reintroduced MET tools/lint/check_git_write_env_scrubbed.py, wired at .pre-commit-config.yaml:300, tracked 100755 with a shebang on line 1, reach floor TEST_FILE_FLOOR = 1500 against 2064 tracked test files

    The census framing was wrong twice, and both corrections mattered

    The original sweep searched only synchronous subprocess.* and missed asyncio.create_subprocess_exec / create_subprocess_shell entirely. Any future sweep for this defect class that greps subprocess.run alone will report a clean tree that is not clean.

    The census was also scoped to test files, which is why the worst instance was found only by review. services/task_workspace.py ran git worktree remove --force, git branch -D and git worktree add with no env= at all, and tests/test_task_workspace.py calls those functions in-process, dozens of times, against a tmp_path fixture. In-process is the crux: scrubbing the test's own fixture protects nothing, because the production function builds its own subprocess environment. Under the pre-push hook's ambient GIT_DIR that suite would have registered and removed worktrees and force-deleted branches in the real repository — strictly worse than the incident that prompted this issue, which dropped files from an index and was recoverable. git branch -D destroys commits not merged elsewhere. Now scrubbed at all four sites (task_workspace.py:224,234,359,372).

    A re-sweep under the corrected framing — production code reachable in-process from a test that constructs a temporary repository — found zero further sites.

    The guard could not run, and said so itself

    On first push the new guard was tracked non-executable, so pre-commit could not execute it. verify-precommit-config caught it: "A hook that cannot execute reports nothing about the code, so a green check over it is a false clean bill of health." The guard against tests silently corrupting the repository was itself silently not running, and would have reported clean indefinitely. Fixed with git update-index --chmod=+x — core.fileMode=false is set repo-wide, so a plain chmod never reaches the index and the mismatch is invisible locally.

    Deliberately out of scope, tracked

    The guard scans test files only. Production callers span MCP tools, Celery workers and CLI scripts, and need per-site caller verification rather than a mechanical env= insertion — exactly what task_workspace.py required. #15287 holds ten further production sites, one of which (skills/promoter.py) runs git add and git commit with neither cwd= nor env=, so a scrub alone would not fix it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions