From 63d394d7ae1cfbab538bb32b650987db4a847bf3 Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Sat, 18 Jul 2026 18:37:21 +0300 Subject: [PATCH 1/2] fix(git): stop implicitly linking .claude Keep shared Claude configuration opt-in through .worktreelink so worktrees do not mutate the main checkout's agent state. Generated-By: PostHog Code Task-Id: 20616cb3-db9d-4833-87fe-8d9bc6f276a9 --- packages/git/src/queries.test.ts | 36 +++++++++++++++++++++++++++++- packages/git/src/queries.ts | 14 +++++++----- packages/git/src/sagas/worktree.ts | 34 ++-------------------------- packages/git/src/worktree.test.ts | 32 +++++++++++++++++++++++++- packages/git/src/worktree.ts | 24 ++++++-------------- 5 files changed, 83 insertions(+), 57 deletions(-) diff --git a/packages/git/src/queries.test.ts b/packages/git/src/queries.test.ts index 7cee73847f..d9e6403873 100644 --- a/packages/git/src/queries.test.ts +++ b/packages/git/src/queries.test.ts @@ -1,9 +1,17 @@ -import { mkdir, mkdtemp, rm, unlink, writeFile } from "node:fs/promises"; +import { + mkdir, + mkdtemp, + readFile, + rm, + unlink, + writeFile, +} from "node:fs/promises"; import { devNull, tmpdir } from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createGitClient } from "./client"; import { + addToLocalExclude, anyBranchRefExists, type ChangedFileInfo, computeDiffStatsFromFiles, @@ -648,3 +656,29 @@ describe("listAllFiles", () => { expect(files).toContain("file.txt"); }); }); + +describe("addToLocalExclude", () => { + let repoDir: string; + + afterEach(async () => { + if (repoDir) { + await rm(repoDir, { recursive: true, force: true }); + } + }); + + it("does not confuse a nested pattern with an exact pattern", async () => { + repoDir = await setupRepo(); + const git = createGitClient(repoDir); + const excludePath = path.resolve( + repoDir, + await git.revparse(["--git-path", "info/exclude"]), + ); + await writeFile(excludePath, "**/.claude/worktrees/\n"); + + await addToLocalExclude(repoDir, ".claude"); + + expect(await readFile(excludePath, "utf-8")).toBe( + "**/.claude/worktrees/\n.claude\n", + ); + }); +}); diff --git a/packages/git/src/queries.ts b/packages/git/src/queries.ts index 10148770c9..db28e350fc 100644 --- a/packages/git/src/queries.ts +++ b/packages/git/src/queries.ts @@ -1379,12 +1379,14 @@ export async function addToLocalExclude( content = await fs.readFile(excludePath, "utf-8"); } catch {} - const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`; - const patternWithoutSlash = pattern.replace(/^\//, ""); - if ( - content.includes(normalizedPattern) || - content.includes(patternWithoutSlash) - ) { + const normalizePattern = (value: string): string => + value.startsWith("/") ? value.slice(1) : value; + const normalizedPattern = normalizePattern(pattern); + const existingPatterns = content + .split(/\r?\n/) + .filter((line) => line && !line.startsWith("#")) + .map(normalizePattern); + if (existingPatterns.includes(normalizedPattern)) { return; } diff --git a/packages/git/src/sagas/worktree.ts b/packages/git/src/sagas/worktree.ts index ed268a6182..dbdbd5a48c 100644 --- a/packages/git/src/sagas/worktree.ts +++ b/packages/git/src/sagas/worktree.ts @@ -87,21 +87,8 @@ export class CreateWorktreeSaga extends GitSaga< }); await this.step({ - name: "symlink-claude-config", + name: "symlink-claude-local-instructions", execute: async () => { - const sourceClaudeDir = path.join(baseDir, ".claude"); - const targetClaudeDir = path.join(worktreePath, ".claude"); - const linkedDir = await safeSymlink( - sourceClaudeDir, - targetClaudeDir, - "dir", - ); - if (linkedDir) { - await addToLocalExclude(worktreePath, ".claude", { - abortSignal: signal, - }); - } - const sourceClaudeLocalMd = path.join(baseDir, "CLAUDE.local.md"); const targetClaudeLocalMd = path.join(worktreePath, "CLAUDE.local.md"); const linkedFile = await safeSymlink( @@ -116,9 +103,7 @@ export class CreateWorktreeSaga extends GitSaga< } }, rollback: async () => { - const targetClaudeDir = path.join(worktreePath, ".claude"); const targetClaudeLocalMd = path.join(worktreePath, "CLAUDE.local.md"); - await fs.rm(targetClaudeDir, { force: true }).catch(() => {}); await fs.rm(targetClaudeLocalMd, { force: true }).catch(() => {}); }, }); @@ -191,21 +176,8 @@ export class CreateWorktreeForBranchSaga extends GitSaga< }); await this.step({ - name: "symlink-claude-config", + name: "symlink-claude-local-instructions", execute: async () => { - const sourceClaudeDir = path.join(baseDir, ".claude"); - const targetClaudeDir = path.join(worktreePath, ".claude"); - const linkedDir = await safeSymlink( - sourceClaudeDir, - targetClaudeDir, - "dir", - ); - if (linkedDir) { - await addToLocalExclude(worktreePath, ".claude", { - abortSignal: signal, - }); - } - const sourceClaudeLocalMd = path.join(baseDir, "CLAUDE.local.md"); const targetClaudeLocalMd = path.join(worktreePath, "CLAUDE.local.md"); const linkedFile = await safeSymlink( @@ -220,9 +192,7 @@ export class CreateWorktreeForBranchSaga extends GitSaga< } }, rollback: async () => { - const targetClaudeDir = path.join(worktreePath, ".claude"); const targetClaudeLocalMd = path.join(worktreePath, "CLAUDE.local.md"); - await fs.rm(targetClaudeDir, { force: true }).catch(() => {}); await fs.rm(targetClaudeLocalMd, { force: true }).catch(() => {}); }, }); diff --git a/packages/git/src/worktree.test.ts b/packages/git/src/worktree.test.ts index f6ccd085d9..ea4f70697d 100644 --- a/packages/git/src/worktree.test.ts +++ b/packages/git/src/worktree.test.ts @@ -247,7 +247,7 @@ describe("WorktreeManager worktree link/include processing", () => { await seedGit.addConfig("commit.gpgsign", "false"); await writeFile( path.join(seedDir, ".gitignore"), - ".env\n.envrc\nnode_modules/\n", + ".claude/\n.env\n.envrc\nCLAUDE.local.md\nnode_modules/\n", ); await writeFile(path.join(seedDir, ".worktreelink"), "# secrets\n.envrc\n"); await writeFile(path.join(seedDir, ".worktreeinclude"), ".env\n"); @@ -264,6 +264,12 @@ describe("WorktreeManager worktree link/include processing", () => { await writeFile(path.join(localDir, ".env"), "secret\n"); await writeFile(path.join(localDir, ".envrc"), "export FOO=1\n"); + await mkdir(path.join(localDir, ".claude")); + await writeFile( + path.join(localDir, ".claude", "settings.local.json"), + "{}\n", + ); + await writeFile(path.join(localDir, "CLAUDE.local.md"), "local rules\n"); await mkdir(path.join(localDir, "node_modules", "dep"), { recursive: true, }); @@ -306,6 +312,30 @@ describe("WorktreeManager worktree link/include processing", () => { expect(await dirExists(path.join(info.worktreePath, "node_modules"))).toBe( false, ); + expect(await dirExists(path.join(info.worktreePath, ".claude"))).toBe( + false, + ); + + const localInstructions = await lstat( + path.join(info.worktreePath, "CLAUDE.local.md"), + ); + expect(localInstructions.isSymbolicLink()).toBe(true); + }); + + it("links .claude when explicitly configured", async () => { + await writeFile( + path.join(localDir, ".worktreelink"), + "# secrets\n.envrc\n.claude\n", + ); + const manager = new WorktreeManager({ + mainRepoPath: localDir, + worktreeBasePath: worktreeBaseDir, + }); + + const info = await manager.createWorktree({ baseBranch: "main" }); + + const linked = await lstat(path.join(info.worktreePath, ".claude")); + expect(linked.isSymbolicLink()).toBe(true); }); }); diff --git a/packages/git/src/worktree.ts b/packages/git/src/worktree.ts index 4f2e164194..31ccc259e1 100644 --- a/packages/git/src/worktree.ts +++ b/packages/git/src/worktree.ts @@ -403,16 +403,16 @@ export class WorktreeManager { } /** - * Runs the post-create steps shared by every worktree: symlink the Claude - * config, then process the worktree link/include files and the post-checkout - * hook. + * Runs the post-create steps shared by every worktree: link local Claude + * instructions, then process the worktree link/include files and the + * post-checkout hook. */ private async finalizeWorktree( worktreePath: string, onOutput?: (data: string) => void, ): Promise { this.log.info("Finalizing worktree", { worktreePath }); - await this.symlinkClaudeConfig(worktreePath); + await this.symlinkClaudeLocalInstructions(worktreePath); const linkWarnings = await processWorktreeLink( this.mainRepoPath, worktreePath, @@ -815,19 +815,9 @@ export class WorktreeManager { } } - private async symlinkClaudeConfig(worktreePath: string): Promise { - const sourceClaudeDir = path.join(this.mainRepoPath, ".claude"); - const targetClaudeDir = path.join(worktreePath, ".claude"); - - const linkedDir = await safeSymlink( - sourceClaudeDir, - targetClaudeDir, - "dir", - ); - if (linkedDir) { - await addToLocalExclude(worktreePath, ".claude"); - } - + private async symlinkClaudeLocalInstructions( + worktreePath: string, + ): Promise { const sourceClaudeLocalMd = path.join(this.mainRepoPath, "CLAUDE.local.md"); const targetClaudeLocalMd = path.join(worktreePath, "CLAUDE.local.md"); From 1144a6e60d35fc71536f9cf338b565ca053158c4 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:16:17 +0000 Subject: [PATCH 2/2] chore(visual): update storybook baselines 1 updated Run: e43a3b55-29a3-4c2b-a915-738c28b25f20 Co-authored-by: richardsolomou <2622273+richardsolomou@users.noreply.github.com> --- apps/code/snapshots.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/code/snapshots.yml b/apps/code/snapshots.yml index cfbdd290b1..26cb8c4587 100644 --- a/apps/code/snapshots.yml +++ b/apps/code/snapshots.yml @@ -27,7 +27,7 @@ snapshots: archive-archivedtasksview--many-tasks--dark: hash: v1.k4693efd2.40d24f07a24c6400c32ae68b39fd908a056beb5dc6df8bcb237ec2ab2b494f4a.l2vjfSQDalCPRtDNV7pbfTKGlMsQoI81jI-UdJfLHWE archive-archivedtasksview--many-tasks--light: - hash: v1.k4693efd2.52ec9963bfe6f248ffcdaa4c26945d2b7e303115825b65d978aa5aefd4af1007.ZLtOfTZUrznhYeB-N2SIHzQWRzF6TDSaipNkfd8uzjM + hash: v1.k4693efd2.74c25b303262f2d4c0f22890d351e76f482827548548f7c023bc309327c927b8.DP9VVrvBz1naIJMwHORkfqm69BlO785ulOt4o6zFs94 archive-archivedtasksview--mixed-modes--dark: hash: v1.k4693efd2.d94039b8cc17a4ad1b720364f41fee58a4843aa9a901443997ba62602f698794.441LWZhT-2WQZJHni4LoOgQsOSr2O103NZOk1pF8ME4 archive-archivedtasksview--mixed-modes--light: