fix(self): stop SessionStart hook from resetting the user's business repo - #299
Merged
Merged
Conversation
The real field data-loss trigger is HTTP mode (clawpro/openclaw local-agent) in project scope: SessionStart runs a background `pull`, whose auto-report block called reportUsageToTeam on `<projectRoot>/.teamai/team-repo`. In http mode that dir is created with `ensureDir` and has no dedicated .git, so resetToCleanMaster's `git reset --hard` + checkout bubbled up to the user's business repo — wiping uncommitted work and switching them to the default branch. Present in v0.19.0 (reset path from 547f9e2); the project-scope report target lacked the `kind !== 'http'` filter the user-scope target had. The same root cause also affects self mode and project-scope git mode. Fixes, two independent layers: - pull.ts: project-scope report target now also filters `kind !== 'http'` (primary defense — http never reaches reportUsageToTeam). - utils/git.ts: extract `isDedicatedRepoRoot` — confirms repoPath is its own git top level via revparse + realpath, fail-closed when unconfirmed. - team-push.ts: report path uses the shared guard (backstop). - push.ts: guard before resetToCleanMaster; on a non-dedicated root abort the whole push with a clear message instead of reset --hard-ing the business repo. Tests (real git, no mocks + scope isolation), each verified to fail when its guard is removed (negative control): - project-scope e2e: business tree/branch survive + no spurious commit - http project-scope: pull never calls reportUsageToTeam - mock utils/git test factories updated to export isDedicatedRepoRoot Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
m0Nst3r873
force-pushed
the
fix/self-mode-hook-reset
branch
from
August 20, 2026 09:04
37c7695 to
ef8fc5b
Compare
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.
Summary
Fixes a data-loss bug: after teamai is set up, a background
pullon everysession start could run
git reset --hard+git checkout <defaultBranch>against the user's own business repo — wiping uncommitted work and switching
them off their branch.
The field reports came from HTTP mode (clawpro / openclaw local-agent) in
project scope, but the same root cause also affects self mode and project-scope
git mode. This PR closes the whole family with two independent defense layers and
locks each path with a regression test.
Root cause
The SessionStart
pullhook runs in the background (its handler is notgit-only, so it also fires under the HTTP provider) and auto-reports usage:
repoPathis expected to be a disposable cache clone, but in severalconfigurations it lives inside the user's repo and has no
.gitof its own,so git commands bubble up to the enclosing business repo:
repo.localPath.git?<projectRoot>/.teamai/team-repoensureDir)<businessRoot>/.teamai<projectRoot>/.teamai/team-repoThe trigger that reached real users: in
pull(), the user-scope reporttarget filtered out
kind === 'http', but the project-scope target didnot (asymmetric). So an HTTP project-scope config fell through into
reportUsageToTeam's git-mode branch and reset the working tree. This has beenpresent since v0.19.0 (the reset path dates to 547f9e2).
Fix — two independent layers
src/pull.ts— the project-scope report target now also filterskind !== 'http', symmetric with the user-scope target. Primary defense:in HTTP mode both targets are skipped, so
reportUsageToTeamis never calledand no git command runs against the business repo.
src/utils/git.ts— new sharedisDedicatedRepoRoot(repoPath): confirmsrepoPathis its own git top level (git rev-parse --show-toplevel,compared via
fs.realpathon both sides so a symlinked prefix like macOS/tmp → /private/tmpdoesn't misjudge). Returnsfalse(fail-closed) wheneverthat can't be positively confirmed, so callers bail.
src/team-push.ts— the git-mode report branch uses the shared guard as abackstop: if
repoPathisn't a dedicated root, it bails out entirely — noreset, pull, stats write, or commit/push.
src/push.ts—teamai pushapplies the same guard beforeresetToCleanMaster. On a non-dedicated root it aborts the whole push with aclear message (
Cannot push: team repo path is not a dedicated git root…)rather than reset --hard-ing the business repo or failing silently.
Tests
src/__tests__/pull-scope-isolation.test.ts— HTTP project scope: assertspullnever callsreportUsageToTeam(the primary defense). Also keeps theexisting assertion that
selfConfigis forwarded for the self-mode target.src/__tests__/self-mode-no-business-reset.test.ts— real-git E2E (no mocks):.git: business tree/branchsurvive and no spurious stats commit lands on the user's branch;
Both new guards were verified with a negative control — each test fails when
its guard is removed (http filter /
isDedicatedRepoRoot), confirming the testsactually catch the regression.
Test plan
npm run buildnpx tsc --noEmitnpx vitest run— all related suites passisDedicatedRepoRootfails the project-scope E2E🤖 Generated with Claude Code