diff --git a/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java b/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java
index bd5ab4a..e6103f8 100644
--- a/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java
+++ b/bridged/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java
@@ -356,7 +356,8 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
/**
* Deliver the shared IDE guidance ({@link PeerLauncher#ideOverlayText}) as an on-disk
* {@code CLAUDE.local.md} overlay beside the project's own {@code CLAUDE.md} (CB-634), and
- * register the overlay in the worktree's {@code info/exclude} so it never shows as untracked.
+ * register the overlay in the repository's common {@code info/exclude} so it never shows as
+ * untracked (git reads a worktree's excludes from the common dir, not the per-worktree gitdir).
*
*
Safety gate: the overlay is written ONLY when {@code cwd/.git} is a
* regular file — a provisioned worktree keeps a {@code .git} FILE holding a
@@ -382,7 +383,16 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
if (!gitDir.isAbsolute()) {
gitDir = Path.of(cwd).resolve(gitDir).normalize();
}
- Path exclude = gitDir.resolve("info").resolve("exclude");
+ // git reads info/exclude from the COMMON dir, never the per-worktree gitdir (only
+ // info/sparse-checkout is per-worktree). A provisioned worktree's gitdir is
+ // /worktrees/, so the common dir is two levels up; writing the entry into
+ // the per-worktree gitdir leaves it un-honoured and the overlay shows as untracked.
+ Path commonDir = gitDir;
+ if (gitDir.getParent() != null && gitDir.getParent().getFileName() != null
+ && "worktrees".equals(gitDir.getParent().getFileName().toString())) {
+ commonDir = gitDir.getParent().getParent();
+ }
+ Path exclude = commonDir.resolve("info").resolve("exclude");
Files.createDirectories(exclude.getParent());
String overlayLine = "CLAUDE.local.md";
if (!Files.exists(exclude) || Files.readAllLines(exclude).stream().noneMatch(overlayLine::equals)) {
diff --git a/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java b/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java
index 1d0e257..2e802ac 100644
--- a/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java
+++ b/bridged/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java
@@ -138,14 +138,17 @@ class ClaudeCodeLauncherTest {
}
// CB-634: the IDE guidance is delivered as a CLAUDE.local.md overlay (written only into a
- // provisioned worktree — cwd with a `.git` FILE) and registered in the worktree's info/exclude.
+ // provisioned worktree — cwd with a `.git` FILE) and registered in the repository's COMMON
+ // info/exclude. git reads a worktree's excludes from the common dir, not the per-worktree
+ // gitdir (only info/sparse-checkout is per-worktree), so a real provisioned layout
+ // /worktrees/ must land the entry in /info/exclude.
@Test
- void writeIdeOverlayWritesClaudeLocalAndAddsItToInfoExclude(@TempDir Path root) throws Exception {
+ void writeIdeOverlayWritesClaudeLocalAndAddsItToCommonInfoExclude(@TempDir Path root) throws Exception {
Path worktree = Files.createDirectory(root.resolve("worktree"));
- Path gitDir = Files.createDirectory(root.resolve("gitdir"));
- Files.createDirectories(gitDir.resolve("info"));
- // A provisioned worktree keeps its gitdir as a `.git` FILE holding a gitdir: pointer.
+ // Real worktree layout: the .git FILE points at /worktrees/.
+ Path commonDir = Files.createDirectory(root.resolve("dotgit"));
+ Path gitDir = Files.createDirectories(commonDir.resolve("worktrees").resolve("wt1"));
Files.writeString(worktree.resolve(".git"), "gitdir: " + gitDir);
FakeHerdr herdr = new FakeHerdr();
@@ -156,10 +159,12 @@ class ClaudeCodeLauncherTest {
assertTrue(Files.exists(overlay), "the overlay is written beside the project's CLAUDE.md");
assertTrue(Files.readString(overlay).contains("project_path: \"" + worktree + "\""),
"the overlay pins every ide_* call to the member's own worktree");
- Path exclude = gitDir.resolve("info").resolve("exclude");
- assertTrue(Files.exists(exclude), "info/exclude is created from the gitdir pointer");
- assertTrue(Files.readAllLines(exclude).contains("CLAUDE.local.md"),
- "the overlay is registered so it never shows as untracked");
+ Path commonExclude = commonDir.resolve("info").resolve("exclude");
+ assertTrue(Files.exists(commonExclude), "info/exclude is created in the COMMON git dir");
+ assertTrue(Files.readAllLines(commonExclude).contains("CLAUDE.local.md"),
+ "the overlay is registered in the common exclude git actually honours for a worktree");
+ assertFalse(Files.exists(gitDir.resolve("info").resolve("exclude")),
+ "the entry must NOT go to the per-worktree gitdir, which git ignores for excludes");
}
@Test