Skip to content

Make the verdict-review-gate and claude-lane tests hermetic against global git hooks - #204

Merged
seathatflowsinourveins merged 1 commit into
mainfrom
claude/hermetic-git-review-gate-20260924
Sep 24, 2026
Merged

seathatflowsinourveins merged 1 commit into
mainfrom
claude/hermetic-git-review-gate-20260924

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This finishes the job #181 started (Fixes #179 there). On a host with a global core.hooksPath and ignore file, as the agent-ecosystem-managed ~/.config/git has, clean main fails locally:

  • 46 errors in tests.test_verdict_review_gate
  • 1 failure in tests.test_claude_lane

CI runners have no global git config, so CI stays green. The global hook stays strict. This PR adds no hook exemption; it fixes the tests.

Causes

  • test_verdict_review_gate
    • GateFixture.git and the read-tracer subprocess drop every inherited GIT_* variable. That includes the GIT_CONFIG_GLOBAL / GIT_CONFIG_NOSYSTEM that Make tests hermetic against global and system git config #181 sets.
    • As a result, fixture commits read the developer's global config and ran its gitleaks pre-commit hook. The hook flagged synthetic digests as generic-api-key, for example "packet_keys_sha256" in the scratch run-manifest.json, and blocked 46 fixture commits.
    • The gate's own git_environment() drops those variables too. So gate.main --head also ran the global post-checkout hook in its head worktree; a failing hook there fails git worktree add.
  • test_claude_lane: not a hook.
    • Its git calls inherit Make tests hermetic against global and system git config #181's environment. But git reads $XDG_CONFIG_HOME/git/ignore (~/.config/git/ignore) as the default core.excludesFile, and GIT_CONFIG_GLOBAL does not change that.
    • The managed ignore file lists .claude/settings.local.json. So claude_lane.py's git status --porcelain -- .claude … did not see the test's local settings file and returned 0 instead of 2.

Changes

  • tests/__init__.py
    • The hermetic global file now sets core.excludesFile and core.attributesFile to /dev/null. These are the two XDG defaults that GIT_CONFIG_GLOBAL does not replace.
    • New hermetic_git_environment(): os.environ without inherited GIT_* routing variables, with the hermetic configuration applied again.
  • tests/test_verdict_review_gate.py
    • GateFixture.git and trace() use hermetic_git_environment().
    • GateFixture.setUp patches gate.git_environment with it, so the gate's own git calls on the scratch repository are hermetic too, including worktree add. Routing variables are still dropped.
    • Production code (scripts/verdict_review_gate.py, claude_lane.py) is unchanged.
  • tests/test_hermetic_git.py
    • Adds a reusable hostile_home() fixture: every hook exits 1 via ~/.gitconfig and $XDG_CONFIG_HOME/git/config, the ignore file is *, and the attributes file is * hostile.
    • Asserts that scratch repositories are unaffected by it. A non-vacuous control shows that a git which reads it ignores the file, sets the attribute and cannot commit.
    • Asserts that hermetic_git_environment() drops GIT_DIR / GIT_INDEX_FILE and keeps the hermetic config.
    • Runs test_head_revision_is_checked_in_a_temporary_worktree and the claude-lane BIND-R4-8 test under the hostile HOME.
  • manifests/evidence.json: new sha256 and byte count for tests/__init__.py only.

Local reproduction

Environment: macOS arm64, Apple Git 2.50.1, Python 3.13.15. Command:

uv run --no-project --python 3.13 python -m unittest tests.test_verdict_review_gate tests.test_claude_lane tests.test_hermetic_git

Run main 3699012 This branch
Real global hooks present 182 tests: 46 errors (gate), 1 failure (claude_lane); 99.9 s 185 tests, OK; 42.7 s
Hostile temp HOME: every ~/.config/git/hooks/* exits 1, ignore * not run for the whole modules 185 tests, OK; none of the hostile hooks ran

The two new module-level hostile tests fail against main's tests/__init__.py and gate test module, and pass on this branch:

  • Gate: git init is aborted by the hostile reference-transaction hook.
  • claude-lane: nothing to commit under the hostile ignore file.

Other checks, all local:

  • scripts/validate.py passed.
  • gitleaks 8.30.1 with .gitleaks.toml over origin/main..HEAD: no leaks. The commit also passed the strict global pre-commit guard.
  • Full python -m unittest on this branch (rebased on 149940a): 4324 tests, 522 skipped, 1 failure. The failure is test_adoption_bootstrap_macos…test_install_path_with_shell_syntax_is_data_in_the_wrapper. It fails the same way with main's tests/__init__.py: SIP strips DYLD_LIBRARY_PATH through /bin/sh on this Mac, so it predates and is unrelated to this change.

Follow-ups (not in this PR)

  • claude_lane.py's BIND-R4-8 check (git status --porcelain -- .claude CLAUDE.md AGENTS.md) honours the operator's global excludes. On a host whose ~/.config/git/ignore lists .claude/settings.local.json, a local settings file in the agent-lab checkout is therefore not reported. The check may want --ignored, or an explicit core.excludesFile.
  • tests/test_workflow_hardening.py and tests/test_osv_lockfile_coverage.py use the same GIT_*-stripping idiom and pass here. They can adopt hermetic_git_environment() separately.

Refs #179, #181

🤖 Generated with Claude Code

…lobal git hooks

On a host with a global core.hooksPath and ignore file (a managed
~/.config/git), clean main failed locally: 46 errors in
tests.test_verdict_review_gate and 1 failure in tests.test_claude_lane.
CI runners have no global git config and stayed green.

GateFixture.git and the read tracer dropped every inherited GIT_* variable,
including the GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM that #181 sets. So
fixture commits ran the global gitleaks pre-commit hook, which flagged
synthetic fixture digests as generic-api-key. The gate's own
git_environment() drops them as well, so its head worktree ran the global
post-checkout hook. The claude-lane failure is not a hook. Git reads
$XDG_CONFIG_HOME/git/ignore as the default core.excludesFile whatever
GIT_CONFIG_GLOBAL says. The managed ignore file lists
.claude/settings.local.json, which hid the test's local settings file from
claude_lane.py's git status check.

tests/__init__.py now sets core.excludesFile and core.attributesFile to
/dev/null in the hermetic global file. It also provides
hermetic_git_environment(), which drops inherited GIT_* routing variables
but keeps the hermetic configuration. The gate tests use it for their
helpers and patch gate.git_environment with it. Production code and the
global hooks are unchanged. test_hermetic_git gains a reusable hostile HOME
(hooks that all exit 1, an ignore file matching everything, a hostile
attributes file) and a non-vacuous control. It runs the gate's
head-worktree test and the claude-lane BIND-R4-8 test under that HOME. The
evidence manifest records the new tests/__init__.py bytes.

Refs #179, #181

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Sep 24, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@seathatflowsinourveins
seathatflowsinourveins merged commit cbbd589 into main Sep 24, 2026
24 of 25 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/hermetic-git-review-gate-20260924 branch September 24, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scratch-repo tests inherit global git hooks (gitleaks pre-commit) and fail on hosts that adopt them

1 participant