Skip to content

fix(self): stop SessionStart hook from resetting the user's business repo - #299

Merged
jeff-r2026 merged 1 commit into
Tencent:mainfrom
m0Nst3r873:fix/self-mode-hook-reset
Aug 20, 2026
Merged

fix(self): stop SessionStart hook from resetting the user's business repo#299
jeff-r2026 merged 1 commit into
Tencent:mainfrom
m0Nst3r873:fix/self-mode-hook-reset

Conversation

@m0Nst3r873

@m0Nst3r873 m0Nst3r873 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a data-loss bug: after teamai is set up, a background pull on every
session 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.

#292 (single-repo / self mode) has merged, so this PR's diff is clean — the six
files below are the entire change.

Root cause

The SessionStart pull hook runs in the background (its handler is not
git-only, so it also fires under the HTTP provider) and auto-reports usage:

SessionStart hook → pull({ silent: true })
  → reportUsageToTeam(repoPath, …)                       # repoPath = <root>/.teamai/team-repo
    → resetToCleanMaster: git reset --hard HEAD
                          git checkout <defaultBranch>    # walks up to the business repo's .git!

repoPath is expected to be a disposable cache clone, but in several
configurations it lives inside the user's repo and has no .git of its own,
so git commands bubble up to the enclosing business repo:

Mode / scope repo.localPath Own .git? Result before fix
HTTP, project (clawpro) <projectRoot>/.teamai/team-repo no (ensureDir) reset --hard on business repo
self mode <businessRoot>/.teamai no (subdir) reset --hard on business repo
git, project (team-repo clone missing/incomplete) <projectRoot>/.teamai/team-repo no reset --hard on business repo

The trigger that reached real users: in pull(), the user-scope report
target filtered out kind === 'http', but the project-scope target did
not (asymmetric). So an HTTP project-scope config fell through into
reportUsageToTeam's git-mode branch and reset the working tree. This has been
present since v0.19.0 (the reset path dates to 547f9e2).

Fix — two independent layers

  • src/pull.ts — the project-scope report target now also filters
    kind !== 'http', symmetric with the user-scope target. Primary defense:
    in HTTP mode both targets are skipped, so reportUsageToTeam is never called
    and no git command runs against the business repo.
  • src/utils/git.ts — new shared isDedicatedRepoRoot(repoPath): confirms
    repoPath is its own git top level (git rev-parse --show-toplevel,
    compared via fs.realpath on both sides so a symlinked prefix like macOS
    /tmp → /private/tmp doesn't misjudge). Returns false (fail-closed) whenever
    that can't be positively confirmed, so callers bail.
  • src/team-push.ts — the git-mode report branch uses the shared guard as a
    backstop: if repoPath isn't a dedicated root, it bails out entirely — no
    reset, pull, stats write, or commit/push.
  • src/push.tsteamai push applies the same guard before
    resetToCleanMaster. On a non-dedicated root it aborts the whole push with a
    clear 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: asserts
    pull never calls reportUsageToTeam (the primary defense). Also keeps the
    existing assertion that selfConfig is forwarded for the self-mode target.
  • src/__tests__/self-mode-no-business-reset.test.ts — real-git E2E (no mocks):
    • self mode: business repo's uncommitted changes + current branch survive;
    • project scope, team-repo dir without its own .git: business tree/branch
      survive and no spurious stats commit lands on the user's branch;
    • a genuine dedicated cache clone is still reset (no regression).

Both new guards were verified with a negative control — each test fails when
its guard is removed (http filter / isDedicatedRepoRoot), confirming the tests
actually catch the regression.

Test plan

  • npm run build
  • npx tsc --noEmit
  • npx vitest run — all related suites pass
  • Real-git E2E: business working tree + branch preserved across self mode and project scope; dedicated cache clone still reset
  • Negative control: removing the http filter fails the HTTP test; removing isDedicatedRepoRoot fails the project-scope E2E

🤖 Generated with Claude Code

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
m0Nst3r873 force-pushed the fix/self-mode-hook-reset branch from 37c7695 to ef8fc5b Compare August 20, 2026 09:04
@jeff-r2026
jeff-r2026 merged commit 13a15b5 into Tencent:main Aug 20, 2026
7 checks passed
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.

2 participants