Support .worktreeinclude files for new worktrees - #1029
Conversation
A new worktree checks out tracked files only, so a local .env or a credentials directory stays behind in the source checkout. Add a repo-root .worktreeinclude file that lists what a worktree needs. It uses gitignore pattern syntax. bb copies every matching untracked file from the source checkout after `git worktree add` and before .bb-env-setup.sh, so the setup script can read the copied files. Matching runs through `git ls-files --others --ignored --exclude-from`, which makes git's own matcher decide every pattern. Directory patterns, `**`, and `!` negation behave exactly as they do in .gitignore, and the feature needs no new dependency. bb copies files only. It skips symlinks rather than copying their targets, and it refuses any destination that leaves the worktree through a committed symlink. No failure here fails provisioning: each copy and each skip goes to the thread's provisioning transcript. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
🚨 SLOP COP 🚨 · I am the SlopCop. I am reviewing this pull request under the Stack: PR #1029 only. The second stack check found no later pull request. I skipped no pull requests. I will run security, code quality, architecture, performance, and end-to-end checks. |
|
🚨 SLOP COP 🚨 · Security review completed. The review found a high-impact destination symlink defect at The same copy step can replace a tracked destination file when the source checkout and target base branch differ. I will include the exact fix and evidence in the final review. |
|
🚨 SLOP COP 🚨 · Performance review completed. The matcher runs Large result sets also create unbounded transcript text and can exceed the existing 16 MB Git output buffer. I will include the actionable performance finding in the final review. |
|
🚨 SLOP COP 🚨 · Code-quality and architecture review completed. The review confirmed the destination symlink and tracked-file overwrite defect. It also found three contract gaps: Git listing failures become a false zero-copy success, transcript output has no size limit, and cancellation does not stop the file-copy loop. The repository scan found no substantial duplicate implementation. The new helper is the correct ownership boundary, but it needs the safety and failure controls above. |
|
🚨 SLOP COP 🚨 · End-to-end review completed. This change has no browser route, so a dev-browser test cannot exercise it. I tested the real host boundary instead. Turbo ran the A separate real |
|
|
||
| const copied: string[] = []; | ||
| const skipped: string[] = []; | ||
| for (const relativePath of relativePaths) { |
There was a problem hiding this comment.
🚨 slopcop/review — P2: Stop the copy loop after cancellation
The abort signal controls the Git command only. A large copy continues after cancellation, and cleanup later deletes all copied data.
Check the signal during each loop step. Throw the existing provision_cancelled error so the caller keeps the correct behavior.
| { cwd: sourcePath, allowFailure: true, signal }, | ||
| ); | ||
| if (result.exitCode !== 0) { | ||
| return []; |
There was a problem hiding this comment.
🚨 slopcop/review — P2: Report Git listing failures
This return converts every Git error into a false zero-match result. It also hides the existing 16 MB output-buffer error.
Provisioning then reports a successful zero-file copy. Throw the error or return a clear skip reason with the Git error text.
The read helper also hides non-ENOENT file errors at line 47. Report those errors as the documentation promises.
| sourcePath: string, | ||
| signal: AbortSignal | undefined, | ||
| ): Promise<string[]> { | ||
| const result = await runGit( |
There was a problem hiding this comment.
🚨 slopcop/review — P2: Avoid the full untracked-tree scan for narrow patterns
This command has no pathspec. It walks the complete untracked tree before it returns one narrow match.
A 3.3 GB checkout had 109,768 paths. The warm scan took 0.48–0.87 seconds, compared with 0.023 seconds for a pathspec scan.
Use safe pathspec pruning for patterns that permit it. Add a time limit for the remaining full scans.
There was a problem hiding this comment.
Not taking this one. The measured cost is 0.48–0.87s warm on a 3.3 GB checkout, and it is paid once, only when the repo actually commits a .worktreeinclude, inside an operation that already runs git worktree add over the same tree and then usually a dependency install. Against that baseline the scan is not the bottleneck.
Safe pathspec pruning is also narrower than it looks. A pattern only yields a usable path prefix when it is anchored to a directory segment. The dominant real-world config is .env, which is unanchored and matches at any depth, so it prunes to nothing — the optimization would miss exactly the case it is meant to help while adding a second matching path that can disagree with git's matcher. Handing every pattern to git ls-files --exclude-from is what makes the semantics correct by construction, and I would rather not trade that for a speedup that does not apply to the common case.
A time limit would change a slow copy into a silent partial copy, which is worse than slow for something that provisions secrets.
The other five findings are fixed in 5ccf703.
| emitOutput( | ||
| args.onProgress, | ||
| "worktree-include", | ||
| `Copied ${result.copied.length} file(s): ${result.copied.join(", ")}`, |
There was a problem hiding this comment.
🚨 slopcop/review — P2: Limit transcript path lists
A broad pattern can put thousands of paths in this one entry. The skipped-path loop also emits one entry for each file.
The daemon keeps and sends the full transcript. Limit each list, show the first paths, and report the remaining count.
| keySuffix: "target", | ||
| cwd: args.targetPath, | ||
| }); | ||
| await copyIncludedFiles({ |
There was a problem hiding this comment.
🚨 slopcop/review — P1: Increase the host daemon protocol version
This behavior runs in the host daemon. An old enrolled daemon stays connected at protocol version 71 and silently omits this copy step.
The new transcript entries also cross the daemon connection. Increase HOST_DAEMON_PROTOCOL_VERSION and update its contract test.
| skipped.push(`${relativePath}: destination escapes the worktree`); | ||
| continue; | ||
| } | ||
| await fs.copyFile(sourceFile, targetFile); |
There was a problem hiding this comment.
🚨 slopcop/review — P1: Do not follow an existing destination symlink
The parent check does not validate targetFile. fs.copyFile follows a destination symlink and writes outside the worktree.
I reproduced this through createWorktree with a tracked symlink on the base branch. It replaced a host file with the copied secret.
The same call replaces an existing tracked regular file. The documentation promises that bb never does this.
Use an exclusive or no-follow destination open. Skip every destination that exists, and add a target-side symlink test.
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
Review result: I found two P1 defects and four P2 defects.
Stack: PR #1029 only. I skipped no pull requests.
P1 findings:
- The copy follows a tracked destination symlink. A real
createWorktreetest wrote a copied secret outside the worktree. - The change does not increase
HOST_DAEMON_PROTOCOL_VERSION. Old enrolled daemons stay connected and silently omit the feature.
P2 findings:
- Git listing and pattern-file read errors become false zero-match results.
- Cancellation does not stop the file-copy loop.
- Transcript path output has no size limit.
- Narrow patterns still scan the complete untracked tree.
The repository scan found no substantial duplicate implementation. The helper has the correct package owner.
Turbo test and typecheck tasks passed for @bb/host-workspace. All 178 tests passed.
The feature has no browser route. I tested the real Git worktree and setup-script path instead.
I used a comment-only review. I did not approve or request changes.
Two defects and three contract gaps from the review of #1029: - Never write through a destination that already exists. fs.copyFile followed a symlink tracked by the base branch and wrote the copied secret outside the worktree; it also replaced an existing tracked file, which the docs promised bb would not do. bb now skips any present destination, using lstat plus COPYFILE_EXCL to close the race. - Bump HOST_DAEMON_PROTOCOL_VERSION to 72. The copy step runs in the host daemon. An old daemon stays connected, skips the copy, and emits no such transcript entry, so a repo that lists its .env silently gets a worktree without it. - Stop the copy loop on cancellation instead of copying every remaining file and then deleting all of it during cleanup. - Report git ls-files failures and non-ENOENT include-file read errors rather than converting them into a false zero-match success. - Cap the paths named in one transcript entry. A broad pattern could put thousands of paths into the transcript the daemon keeps and forwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
A new worktree checks out tracked files only, so a local
.envor a credentials directory stays behind in the source checkout. Today the only escape hatch is to hand-write the copy inside.bb-env-setup.sh.This adds a repo-root
.worktreeincludefile that lists what a worktree needs, matching the semantics Claude Code already ships:bb copies every matching untracked file from the source checkout after
git worktree addand before.bb-env-setup.sh, so the setup script can read the copied files.Implementation
packages/host-workspace/src/worktree-include.ts—copyWorktreeIncludeFiles. Matching runs throughgit ls-files --others --ignored --exclude-from=.worktreeinclude -z. Passing the include file as the only exclude source makes git's own matcher decide every pattern, so directory patterns,**, and!negation behave exactly as in.gitignore— and the feature needs no new dependency.packages/host-workspace/src/provisioning.ts—copyIncludedFilesruns insidecreateWorktreeand writes each copy and each skip into the provisioning transcript.Two deliberate differences from Claude Code: bb accepts any untracked file rather than only gitignored ones, because listing a path is already explicit intent; and bb reports into the provisioning transcript instead of a debug log, so the result is visible in the app.
Tests
6 new tests in
packages/host-workspace/test/worktree-include.test.tscover pattern matching,!negation, symlink refusal, comment-only files, ordering against the setup script, and the missing-file case.pnpm exec turbo run test --filter=@bb/host-workspace --force— 178 passedpnpm exec turbo run typecheck --filter=@bb/domain --filter=@bb/host-workspace --filter=@bb/templates --filter=@bb/server— passedDocs
docs/worktrees.md,docs/platform-support.md, thebb guide environmentstemplate (regenerated), and thebb-cliskill.Notes
No
HOST_DAEMON_PROTOCOL_VERSIONbump: this adds no field to any session payload, WebSocket message, or host RPC command.Large directories such as
node_modulesare copied file by file, which is slow. The docs point users at.bb-env-setup.shfor dependency installation instead.🤖 Generated with Claude Code