Skip to content

fix(server): reject a bare repository root when adding a project - #8646

Open
walid-baharwal wants to merge 1 commit into
pingdotgg:mainfrom
walid-baharwal:fix/bare-root-worktree-project
Open

fix(server): reject a bare repository root when adding a project#8646
walid-baharwal wants to merge 1 commit into
pingdotgg:mainfrom
walid-baharwal:fix/bare-root-worktree-project

Conversation

@walid-baharwal

@walid-baharwal walid-baharwal commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

What Changed

Adding a project whose root holds a bare repository and its worktrees — no working tree of
its own — is now rejected with an explanation, instead of being accepted as an ordinary
repository.

Verified through the real CLI against layouts built with real git:

$ t3 project add <root>                     # bare repo at <root>/.bare, worktrees alongside
WorkspaceRootBareRepositoryLayoutError: '<root>' holds a bare repository and its worktrees
rather than a working tree of its own. Add one of the worktree directories inside it instead.

$ t3 project add <root>/develop             # unchanged
Added project c8499b91-… (develop) at <root>/develop.

Before this change the first command printed Added project … at <root>, and git status
there reported every worktree as untracked (?? .bare/, ?? develop/).

Why

Fixes #8164. git rev-parse --is-inside-work-tree answers true at such a root once
core.bare is false, so the root was accepted and became the parent of every worktree: the
diff view scoped across all of them and a thread's cwd spanned every checkout at once.

Detection reads the .git file only — no new git invocation. A directory .git is an
ordinary repository; a .git file pointing outside the root is a linked worktree or a
submodule. Among what is left, the missing index is the discriminator: a bare repository
stages nothing, while git init --separate-git-dir pointed inside its own working tree
writes one. Six real layouts were checked end to end through t3 project add:

layout result
bare root with worktrees rejected
bare root before any worktree exists rejected
git init --separate-git-dir inside the root, with a worktree accepted
ordinary repository accepted
a bare root's own worktree accepted
an ordinary repository's linked worktree accepted

The gitdir parse moved into packages/shared/src/git.ts and is now shared with
devHome.ts, which had the same parse inline; that also gives this path the Windows
backslash normalization it was missing.

Two gaps left uncovered on purpose:

  • Auto-bootstrap (serverRuntimeStartup.ts) dispatches project.create directly, so
    launching with --cwd <bare root> still creates the project. Rejecting there would fail
    server startup rather than inform anyone, which is worse than the bug.
  • orchestration/http.ts maps dispatch failures to invalid_command, dropping the
    message for HTTP callers. Pre-existing and not this change's to fix.

UI Changes

None rendered. The rejection reuses the existing "Failed to add project" toast in web and
the error banner in mobile — both already display error.message, so no client change was
needed. CLI output matches the shape of the existing workspace-root errors.

Verification

vp test run apps/server/src/workspace/WorkspacePaths.test.ts \
  apps/server/src/cli/project.test.ts \
  apps/server/src/orchestration/Normalizer.test.ts \
  packages/shared/src/devHome.test.ts packages/shared/src/git.test.ts   # 39 passed
cd apps/server && tsgo --noEmit      # clean
cd packages/shared && tsgo --noEmit  # clean
vp lint <changed files>
vp format --check <changed files>
git diff --check

Plus the real-git matrix above, driven through t3 project add with a scratch
T3CODE_HOME. The rejection test fails without the source change.

Checklist

  • One concern
  • Focused tests, failing before the fix
  • Typecheck, lint and format run on the changed packages
  • No new dependencies, no committed artifacts
  • Existing helper reused rather than duplicated
  • Entry points walked: web, mobile, CLI covered; auto-bootstrap and HTTP dispatch
    documented above as uncovered

Model: Claude Opus 5 (1M context). Harness: Claude Code.


Note

Medium Risk
Changes which paths can become projects using filesystem heuristics; misclassification could block valid roots or still allow mis-scoped ones, but scope is limited to project-add/create paths.

Overview
Adding a project now rejects workspace roots that are only a bare-repo + worktree container (no checkout of their own), with WorkspaceRootBareRepositoryLayoutError telling users to pick a worktree directory instead.

Detection lives on WorkspacePaths.ensureNotBareRepositoryLayout: after normalizing the path, it reads the root’s .git file (no extra git calls), resolves an in-root gitdir, and treats the layout as invalid when that gitdir has linked worktrees and no index—while still allowing ordinary repos, linked worktrees, separate-git-dir setups, and bare roots before any worktree exists (per tests).

The check runs on t3 project add and on orchestration project.create normalization; other callers of normalizeWorkspaceRoot are unchanged.

parseGitDirPointer is extracted to packages/shared/src/git.ts (with Windows backslash normalization) and reused from devHome for linked-worktree detection.

Reviewed by Cursor Bugbot for commit 48fc1cd. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reject bare repository root layout in projectAddCommand and normalizeProjectWorkspaceRootForCreate

  • Adds WorkspacePaths.ensureNotBareRepositoryLayout, which detects a bare-repository layout: .git is a file pointing to a gitdir inside the root, gitdir/worktrees has entries, and gitdir has no index file. Fails with WorkspaceRootBareRepositoryLayoutError when detected.
  • Chains the new check after normalizeWorkspaceRoot in both the project-add CLI handler and the project.create dispatch normalization path, so only roots with an actual working tree are accepted.
  • Adds parseGitDirPointer to git.ts and refactors pointsAtLinkedWorktree to use it for robust .git file parsing.
  • Behavioral Change: project-add now rejects roots that are bare git directories containing only linked worktrees; other WorkspacePaths callers intentionally do not enforce this check.

Macroscope summarized 48fc1cd.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 77613745-32fe-48fb-98b3-ac557baac157

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 29, 2026
Comment thread apps/server/src/workspace/WorkspacePaths.ts Outdated
const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
const workspaceRoot = yield* normalizeWorkspaceRootForProjectCommand(
flags.workspaceRoot,
).pipe(Effect.flatMap(workspacePaths.ensureNotBareRepositoryLayout));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High cli/project.ts:467

project add rejects a valid empty git init --separate-git-dir working tree because ensureNotBareRepositoryLayout classifies any in-root gitdir without an index as bare. Conversely, adding an index to the embedded gitdir makes the same bare/worktree-only layout pass, so this heuristic both rejects valid repositories and permits the cross-worktree scope it is meant to prevent. Please detect the repository layout using Git metadata rather than index presence.

Also found in 2 other location(s)

apps/server/src/workspace/WorkspacePaths.ts:209

isBareRepositoryLayout treats any in-root gitdir without an index as bare. A freshly initialized working tree made with git init --separate-git-dir &lt;root&gt;/store has exactly the .git pointer created by git init but no index until git add stages a file, so adding that valid empty project is rejected as a bare-worktree layout.

apps/server/src/workspace/WorkspacePaths.ts:209

The missing-index heuristic also has a false-negative path: isBareRepositoryLayout returns false whenever the embedded bare git directory contains an index. An index is not a property of a working tree (Git's index tooling can write $GIT_DIR/index without a worktree), so a bare-root/worktree layout with an existing index is accepted and recreates the cross-worktree project scope this check is intended to prevent.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/cli/project.ts around line 467:

`project add` rejects a valid empty `git init --separate-git-dir` working tree because `ensureNotBareRepositoryLayout` classifies any in-root gitdir without an `index` as bare. Conversely, adding an `index` to the embedded gitdir makes the same bare/worktree-only layout pass, so this heuristic both rejects valid repositories and permits the cross-worktree scope it is meant to prevent. Please detect the repository layout using Git metadata rather than index presence.

Also found in 2 other location(s):
- apps/server/src/workspace/WorkspacePaths.ts:209 -- `isBareRepositoryLayout` treats any in-root gitdir without an `index` as bare. A freshly initialized working tree made with `git init --separate-git-dir <root>/store` has exactly the `.git` pointer created by `git init` but no index until `git add` stages a file, so adding that valid empty project is rejected as a bare-worktree layout.
- apps/server/src/workspace/WorkspacePaths.ts:209 -- The missing-index heuristic also has a false-negative path: `isBareRepositoryLayout` returns `false` whenever the embedded bare git directory contains an `index`. An index is not a property of a working tree (Git's index tooling can write `$GIT_DIR/index` without a worktree), so a bare-root/worktree layout with an existing index is accepted and recreates the cross-worktree project scope this check is intended to prevent.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9ecd6ca. Configure here.

Comment thread apps/server/src/workspace/WorkspacePaths.ts
A worktree-only layout keeps a bare repository inside the root (commonly
`<root>/.bare`) and checks every branch out as a sibling directory, so the
root holds no working tree of its own. `git rev-parse --is-inside-work-tree`
still answers true there, so the root was accepted as an ordinary repository
and became the parent of every worktree: status reported each worktree as
untracked, the diff view was scoped across all of them, and a thread's cwd
spanned every checkout at once.

Recognize the layout where a project is added and explain that a worktree
directory inside it is what to add. Detection reads the `.git` file: a
directory `.git` is an ordinary repository, and a `.git` file pointing outside
the root is a linked worktree or a submodule. What remains is shared with
`git init --separate-git-dir`, and git records nothing that separates the two —
it writes no `core.worktree` for either — so two structural signals are
required together: the git directory hosts linked worktrees, and it never
staged anything of its own. A working tree that has committed has an index; a
bare repository does not.

Anything ambiguous is accepted, since wrongly refusing a valid root is worse
than the misscoping this prevents. Containment is tested with a relative path
so a root at the filesystem boundary is compared correctly.

Fixes pingdotgg#8164
@macroscopeapp

macroscopeapp Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR changes project creation behavior across CLI and server orchestration using a nontrivial filesystem heuristic to classify bare-repository layouts. The gate affects existing project-creation paths, and reported edge cases around valid layouts warrant human review.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@walid-baharwal
walid-baharwal force-pushed the fix/bare-root-worktree-project branch from 9ecd6ca to 48fc1cd Compare August 29, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bare-root worktree layouts are silently accepted as a normal repo, making the project root the parent of every worktree

1 participant