Skip to content

test: guard the live harness gate against Claude Code's auto-updater - #6002

Merged
kunchenguid merged 3 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-up-5959-live-autoupdate
Sep 28, 2026
Merged

kunchenguid merged 3 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-up-5959-live-autoupdate

Conversation

@karotkriss

@karotkriss karotkriss commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Fixes #5959

Live harness tests can let Claude Code's auto-updater rewrite the real ~/.local/bin/claude.
Nothing in the repository sets DISABLE_AUTOUPDATER, and fm_live_gate in tests/lib.sh lets a live run proceed without it, so a live Claude test can repoint the shared symlink into a temporary directory and stop every Claude process on the machine from starting.
When the live gate lets a live run proceed it should export DISABLE_AUTOUPDATER=1 so the updater cannot run during tests.

What Changed

  • fm_live_gate in tests/lib.sh now exports DISABLE_AUTOUPDATER=1 on the proceed path, so any live-harness run it green-lights cannot let Claude Code's auto-updater rewrite the installed binary; a comment on the gate documents the guarantee.
  • Added test_gate_exports_disable_autoupdater_for_a_proceeding_run to tests/fm-live-gate.test.sh, asserting a proceeding guard sees DISABLE_AUTOUPDATER=1 in its environment.
  • Added test_disable_autoupdater_reaches_the_claude_pane_on_the_fm_spawn_launch_path, which stages a real fm-spawn claude launch and replays it against a synthetic pane whose stub claude records the inherited value, proving the ambient variable rides the launch through to the harness pane; the file now sources fixtures.sh for the spawn helpers.

Risk Assessment

✅ Low: A one-line export on the gate's single proceed path exactly implements the required behavior, and the two new tests are behavioral regressions (not source-content assertions) that cover both the gate export and the fm-spawn inheritance path the user chose to fix.

Testing

Drove the change through its real executable interface, tests/fm-live-gate.test.sh, which runs live guard scripts as separate processes calling fm_live_gate. A proceeding guard observes DISABLE_AUTOUPDATER=1, and I confirmed this is a true regression by removing the fix line (guard then saw autoupdater=unset) and restoring it. The fm-spawn launch-path case stages a real claude launch and runs it as a synthetic recording pane, proving the ambient variable is passed through. The family sweep confirms all 40 guards wire through the shared gate. No UI surface exists for this test-harness change, so evidence is the CLI transcript rather than a screenshot.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
A live guard that passes the gate has DISABLE_AUTOUPDATER=1 in its environment ✅ pass live bash tests/fm-live-gate.test.sh -> ok 'a proceeding live run exports DISABLE_AUTOUPDATER=1'; guard process prints autoupdater=1
Regression: without the fix, a proceeding guard sees the variable unset (updater could run) ✅ pass live Removed the export line -> not ok ... (missing: 'autoupdater=1'), autoupdater=unset, EXIT=1; restored -> passes
fm-spawn's claude launch preserves the ambient DISABLE_AUTOUPDATER through to the harness pane ✅ pass live bash tests/fm-live-gate.test.sh -> ok 'DISABLE_AUTOUPDATER rides fm-spawn's claude launch through to the harness pane'; synthetic pane stub records autoupdater=1 from the staged launch
Every live guard refuses through the shared gate on FM_LIVE=0 (export lives on the single proceed path) ✅ pass live bash tests/fm-live-gate.test.sh -> ok 'all 40 live guards refuse together on FM_LIVE=0'
Evidence: fm-live-gate suite (with fix, all ok)

Source: fm-live-gate suite (with fix, all ok)

=== fm-live-gate.test.sh (with fix, HEAD) ===
ok - a default-on guard runs wherever its tools are installed
ok - an absent tool is a named capability skip, not a silent pass
ok - a prompt-submitting guard stays off until it is asked for
ok - a guard's own variable turns it on
ok - a demanded run refuses to pass as a skip
ok - FM_LIVE switches the whole family
ok - a guard's own setting wins over FM_LIVE
ok - any entry point of a multi-mode guard turns it on
ok - the shared gate carries the gate-refusal bypass into every live guard
ok - a proceeding live run exports DISABLE_AUTOUPDATER=1
ok - DISABLE_AUTOUPDATER rides fm-spawn's claude launch through to the harness pane
ok - all 40 live guards refuse together on FM_LIVE=0
EXIT=0
Evidence: Regression: fix removed -> proceeding-run test fails
not ok - a live run the gate lets proceed must export DISABLE_AUTOUPDATER=1 so Claude Code's auto-updater cannot run (missing: 'autoupdater=1')
autoupdater=unset
EXIT=1

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
A live guard that passes the gate has DISABLE_AUTOUPDATER=1 in its environment ✅ pass live bash tests/fm-live-gate.test.sh -> ok 'a proceeding live run exports DISABLE_AUTOUPDATER=1'; guard process prints autoupdater=1
Regression: without the fix, a proceeding guard sees the variable unset (updater could run) ✅ pass live Removed the export line -> not ok ... (missing: 'autoupdater=1'), autoupdater=unset, EXIT=1; restored -> passes
fm-spawn's claude launch preserves the ambient DISABLE_AUTOUPDATER through to the harness pane ✅ pass live bash tests/fm-live-gate.test.sh -> ok 'DISABLE_AUTOUPDATER rides fm-spawn's claude launch through to the harness pane'; synthetic pane stub records autoupdater=1 from the staged launch
Every live guard refuses through the shared gate on FM_LIVE=0 (export lives on the single proceed path) ✅ pass live bash tests/fm-live-gate.test.sh -> ok 'all 40 live guards refuse together on FM_LIVE=0'
  • bash tests/fm-live-gate.test.sh (all cases ok, EXIT=0)
  • Regression check: removed export DISABLE_AUTOUPDATER=1 from tests/lib.sh, re-ran suite -> test_gate_exports_disable_autoupdater_for_a_proceeding_run fails with autoupdater=unset, then restored lib.sh (git status clean)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

fm_live_gate let a live run proceed without ever setting
DISABLE_AUTOUPDATER, so a live Claude test could let the real updater
repoint ~/.local/bin/claude into a temporary directory and stop every
Claude process on the machine from starting. Export
DISABLE_AUTOUPDATER=1 on every path where the gate lets a live run
proceed, and assert the export in tests/fm-live-gate.test.sh, including
that it reaches a child process the same way a real harness pane would
inherit it.
@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Test harness adds environment guard for live runs.

The PR appears safe to merge; no outstanding blocking issue was identified.

Reviews (4) · Last reviewed commit: "no-mistakes(ci): Fixed Greptile finding ..."

Comment thread tests/lib.sh
…nheritance test only checked a `bash -c` direct child, not the fm-spawn.sh launch path. The user chose to fix it with a regression on that path. In tests/fm-live-gate.test.sh I replaced the generic child test with test_disable_autoupdater_reaches_the_claude_pane_on_the_fm_spawn_launch_path: it drives the real fm-spawn claude launch through the spawn fixtures, captures the exact staged launch command, and runs it as a synthetic pane whose only `claude` is a stub recording the inherited DISABLE_AUTOUPDATER, asserting it saw 1. Switched the file to source fixtures.sh (pulls in lib.sh, guarded) for the spawn helpers. Verified it is a real guard: the stub records `1` when the ambient var is set and `unset` when absent, so it fails if fm-spawn ever scrubbed the variable (e.g. env -i or -u). This confirms fm-spawn's launch construction never references the name and passes it through via ordinary ambient inheritance with no allowlist. Full suite passes (12 tests ok), shellcheck clean. Note for the outer executor: I embedded the daemon caveat as a code comment in the test, but the finding also asks the PR body to state that a backend daemon already running before the gate exported the variable does not inherit it and fully covering that would need launcher support - that forge-side PR-body sentence is outside this CI phase's scope
@karotkriss karotkriss closed this Sep 28, 2026
@karotkriss karotkriss reopened this Sep 28, 2026
@karotkriss karotkriss changed the title test(tests): export DISABLE_AUTOUPDATER when the live gate proceeds test: guard the live harness gate against Claude Code's auto-updater Sep 28, 2026
… handed its launch command to an already-running backend daemon that never inherited the test process's exported DISABLE_AUTOUPDATER, so ambient inheritance dropped it and Claude's auto-updater could still run. Fix (bin/fm-spawn.sh): when DISABLE_AUTOUPDATER is set in the spawn's own environment, embed `export DISABLE_AUTOUPDATER=<value>;` into the LAUNCH command text (same idiom as the adjacent COMPACT_ADVISER_DISABLE export), so it survives a daemon-built pane, the env -i allowlist path, and relaunch alike; gated on presence so ordinary spawns are unchanged. Added regression test test_disable_autoupdater_survives_a_daemon_pane_that_never_inherited_it in tests/fm-live-gate.test.sh: stages a real claude launch with DISABLE_AUTOUPDATER set in the spawn env, then runs that exact command in a synthetic pane with `env -u DISABLE_AUTOUPDATER` and asserts the claude stub still recorded autoupdater=1. Verified the test fails (autoupdater=unset) without the fix and passes with it; the round-1 ambient test stays green either way. Full suite passes (13 ok); test file and isolated snippet shellcheck-clean (full fm-spawn.sh shellcheck kept getting terminated by the memory-constrained host, not by findings). Forge-side note for the outer executor: the PR-body caveat that fully covering the daemon case would need launcher support no longer applies to the Claude launch path and should be corrected

@kunchenguid kunchenguid left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking as Kun's firstmate: approving after tip-vs-main inspection.

Contract-class restore: fm_live_gate proceed path now exports DISABLE_AUTOUPDATER=1, and fm-spawn embeds that value into the launch only when already set — ordinary unconfigured spawns unchanged. Restores the concrete test-isolation promise in #5959 (live harness must not rewrite host ~/.local/bin/claude).

VISION: aligns (scripts own mechanics; no new default consent; fleet survives vendor auto-updater failure mode during live tests). Safe, MATCH attestation, green CI, no .github/workflows.

@kunchenguid
kunchenguid merged commit 29213a0 into kunchenguid:main Sep 28, 2026
20 checks passed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this is merged. Thank you @karotkriss — really appreciate you taking the time on this.

Contract-class: restore (own tip-vs-main). fm_live_gate export of DISABLE_AUTOUPDATER=1 on proceed + fm-spawn embed only when already set restores the #5959 test-isolation promise; ordinary unconfigured spawns unchanged. VISION aligns. MATCH, CI SUCCESS, no workflow-scope files.

knowttl pushed a commit to knowttl/firstmate that referenced this pull request Sep 29, 2026
…unchenguid#6002)

* fix(tests): disable Claude Code's auto-updater during live harness runs

fm_live_gate let a live run proceed without ever setting
DISABLE_AUTOUPDATER, so a live Claude test could let the real updater
repoint ~/.local/bin/claude into a temporary directory and stop every
Claude process on the machine from starting. Export
DISABLE_AUTOUPDATER=1 on every path where the gate lets a live run
proceed, and assert the export in tests/fm-live-gate.test.sh, including
that it reaches a child process the same way a real harness pane would
inherit it.

* no-mistakes(ci): Greptile flagged that the PR's DISABLE_AUTOUPDATER inheritance test only checked a `bash -c` direct child, not the fm-spawn.sh launch path. The user chose to fix it with a regression on that path. In tests/fm-live-gate.test.sh I replaced the generic child test with test_disable_autoupdater_reaches_the_claude_pane_on_the_fm_spawn_launch_path: it drives the real fm-spawn claude launch through the spawn fixtures, captures the exact staged launch command, and runs it as a synthetic pane whose only `claude` is a stub recording the inherited DISABLE_AUTOUPDATER, asserting it saw 1. Switched the file to source fixtures.sh (pulls in lib.sh, guarded) for the spawn helpers. Verified it is a real guard: the stub records `1` when the ambient var is set and `unset` when absent, so it fails if fm-spawn ever scrubbed the variable (e.g. env -i or -u). This confirms fm-spawn's launch construction never references the name and passes it through via ordinary ambient inheritance with no allowlist. Full suite passes (12 tests ok), shellcheck clean. Note for the outer executor: I embedded the daemon caveat as a code comment in the test, but the finding also asks the PR body to state that a backend daemon already running before the gate exported the variable does not inherit it and fully covering that would need launcher support - that forge-side PR-body sentence is outside this CI phase's scope

* no-mistakes(ci): Fixed Greptile finding ci-1. Root cause: fm-spawn.sh handed its launch command to an already-running backend daemon that never inherited the test process's exported DISABLE_AUTOUPDATER, so ambient inheritance dropped it and Claude's auto-updater could still run. Fix (bin/fm-spawn.sh): when DISABLE_AUTOUPDATER is set in the spawn's own environment, embed `export DISABLE_AUTOUPDATER=<value>;` into the LAUNCH command text (same idiom as the adjacent COMPACT_ADVISER_DISABLE export), so it survives a daemon-built pane, the env -i allowlist path, and relaunch alike; gated on presence so ordinary spawns are unchanged. Added regression test test_disable_autoupdater_survives_a_daemon_pane_that_never_inherited_it in tests/fm-live-gate.test.sh: stages a real claude launch with DISABLE_AUTOUPDATER set in the spawn env, then runs that exact command in a synthetic pane with `env -u DISABLE_AUTOUPDATER` and asserts the claude stub still recorded autoupdater=1. Verified the test fails (autoupdater=unset) without the fix and passes with it; the round-1 ambient test stays green either way. Full suite passes (13 ok); test file and isolated snippet shellcheck-clean (full fm-spawn.sh shellcheck kept getting terminated by the memory-constrained host, not by findings). Forge-side note for the outer executor: the PR-body caveat that fully covering the daemon case would need launcher support no longer applies to the Claude launch path and should be corrected
RooseveltAdvisors pushed a commit to RooseveltAdvisors/firstmate that referenced this pull request Sep 29, 2026
…unchenguid#6002)

* fix(tests): disable Claude Code's auto-updater during live harness runs

fm_live_gate let a live run proceed without ever setting
DISABLE_AUTOUPDATER, so a live Claude test could let the real updater
repoint ~/.local/bin/claude into a temporary directory and stop every
Claude process on the machine from starting. Export
DISABLE_AUTOUPDATER=1 on every path where the gate lets a live run
proceed, and assert the export in tests/fm-live-gate.test.sh, including
that it reaches a child process the same way a real harness pane would
inherit it.

* no-mistakes(ci): Greptile flagged that the PR's DISABLE_AUTOUPDATER inheritance test only checked a `bash -c` direct child, not the fm-spawn.sh launch path. The user chose to fix it with a regression on that path. In tests/fm-live-gate.test.sh I replaced the generic child test with test_disable_autoupdater_reaches_the_claude_pane_on_the_fm_spawn_launch_path: it drives the real fm-spawn claude launch through the spawn fixtures, captures the exact staged launch command, and runs it as a synthetic pane whose only `claude` is a stub recording the inherited DISABLE_AUTOUPDATER, asserting it saw 1. Switched the file to source fixtures.sh (pulls in lib.sh, guarded) for the spawn helpers. Verified it is a real guard: the stub records `1` when the ambient var is set and `unset` when absent, so it fails if fm-spawn ever scrubbed the variable (e.g. env -i or -u). This confirms fm-spawn's launch construction never references the name and passes it through via ordinary ambient inheritance with no allowlist. Full suite passes (12 tests ok), shellcheck clean. Note for the outer executor: I embedded the daemon caveat as a code comment in the test, but the finding also asks the PR body to state that a backend daemon already running before the gate exported the variable does not inherit it and fully covering that would need launcher support - that forge-side PR-body sentence is outside this CI phase's scope

* no-mistakes(ci): Fixed Greptile finding ci-1. Root cause: fm-spawn.sh handed its launch command to an already-running backend daemon that never inherited the test process's exported DISABLE_AUTOUPDATER, so ambient inheritance dropped it and Claude's auto-updater could still run. Fix (bin/fm-spawn.sh): when DISABLE_AUTOUPDATER is set in the spawn's own environment, embed `export DISABLE_AUTOUPDATER=<value>;` into the LAUNCH command text (same idiom as the adjacent COMPACT_ADVISER_DISABLE export), so it survives a daemon-built pane, the env -i allowlist path, and relaunch alike; gated on presence so ordinary spawns are unchanged. Added regression test test_disable_autoupdater_survives_a_daemon_pane_that_never_inherited_it in tests/fm-live-gate.test.sh: stages a real claude launch with DISABLE_AUTOUPDATER set in the spawn env, then runs that exact command in a synthetic pane with `env -u DISABLE_AUTOUPDATER` and asserts the claude stub still recorded autoupdater=1. Verified the test fails (autoupdater=unset) without the fix and passes with it; the round-1 ambient test stays green either way. Full suite passes (13 ok); test file and isolated snippet shellcheck-clean (full fm-spawn.sh shellcheck kept getting terminated by the memory-constrained host, not by findings). Forge-side note for the outer executor: the PR-body caveat that fully covering the daemon case would need launcher support no longer applies to the Claude launch path and should be corrected
andrewesweet pushed a commit to andrewesweet/firstmate that referenced this pull request Sep 30, 2026
…unchenguid#6002)

* fix(tests): disable Claude Code's auto-updater during live harness runs

fm_live_gate let a live run proceed without ever setting
DISABLE_AUTOUPDATER, so a live Claude test could let the real updater
repoint ~/.local/bin/claude into a temporary directory and stop every
Claude process on the machine from starting. Export
DISABLE_AUTOUPDATER=1 on every path where the gate lets a live run
proceed, and assert the export in tests/fm-live-gate.test.sh, including
that it reaches a child process the same way a real harness pane would
inherit it.

* no-mistakes(ci): Greptile flagged that the PR's DISABLE_AUTOUPDATER inheritance test only checked a `bash -c` direct child, not the fm-spawn.sh launch path. The user chose to fix it with a regression on that path. In tests/fm-live-gate.test.sh I replaced the generic child test with test_disable_autoupdater_reaches_the_claude_pane_on_the_fm_spawn_launch_path: it drives the real fm-spawn claude launch through the spawn fixtures, captures the exact staged launch command, and runs it as a synthetic pane whose only `claude` is a stub recording the inherited DISABLE_AUTOUPDATER, asserting it saw 1. Switched the file to source fixtures.sh (pulls in lib.sh, guarded) for the spawn helpers. Verified it is a real guard: the stub records `1` when the ambient var is set and `unset` when absent, so it fails if fm-spawn ever scrubbed the variable (e.g. env -i or -u). This confirms fm-spawn's launch construction never references the name and passes it through via ordinary ambient inheritance with no allowlist. Full suite passes (12 tests ok), shellcheck clean. Note for the outer executor: I embedded the daemon caveat as a code comment in the test, but the finding also asks the PR body to state that a backend daemon already running before the gate exported the variable does not inherit it and fully covering that would need launcher support - that forge-side PR-body sentence is outside this CI phase's scope

* no-mistakes(ci): Fixed Greptile finding ci-1. Root cause: fm-spawn.sh handed its launch command to an already-running backend daemon that never inherited the test process's exported DISABLE_AUTOUPDATER, so ambient inheritance dropped it and Claude's auto-updater could still run. Fix (bin/fm-spawn.sh): when DISABLE_AUTOUPDATER is set in the spawn's own environment, embed `export DISABLE_AUTOUPDATER=<value>;` into the LAUNCH command text (same idiom as the adjacent COMPACT_ADVISER_DISABLE export), so it survives a daemon-built pane, the env -i allowlist path, and relaunch alike; gated on presence so ordinary spawns are unchanged. Added regression test test_disable_autoupdater_survives_a_daemon_pane_that_never_inherited_it in tests/fm-live-gate.test.sh: stages a real claude launch with DISABLE_AUTOUPDATER set in the spawn env, then runs that exact command in a synthetic pane with `env -u DISABLE_AUTOUPDATER` and asserts the claude stub still recorded autoupdater=1. Verified the test fails (autoupdater=unset) without the fix and passes with it; the round-1 ambient test stays green either way. Full suite passes (13 ok); test file and isolated snippet shellcheck-clean (full fm-spawn.sh shellcheck kept getting terminated by the memory-constrained host, not by findings). Forge-side note for the outer executor: the PR-body caveat that fully covering the daemon case would need launcher support no longer applies to the Claude launch path and should be corrected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Live harness tests can let Claude Code's auto-updater rewrite the real ~/.local/bin/claude

2 participants