fix(#625): never remove a worktree that is, or holds, a Docker case workspace

The Docker rule was applied to the CASE folder and its common git dir
only. A Docker case can be linked to any host folder, including a managed
worktree (`<repo>/.claude/worktrees/<name>`), which is a natural setup for
running one of the parallel agents in a container. Removing that worktree
from the local case's dialog, or through Close Session's "Kill & remove
worktree", ran `git worktree remove`, which runs a git status INSIDE the
tree (container-written files, checkout filters on this machine) and then
deletes the whole tree, ignored files included, while the container still
mounts it. Reproduced on an isolated instance: DELETE answered 200 and the
Docker case's workspace was gone.

That contradicts the invariant the PR itself documents in CLAUDE.md ("git
never runs ... at or inside a Docker workspace"). removeCaseWorktree now
refuses (400) a present worktree that is, sits inside, or holds a Docker
case workspace, before any git runs; the DELETE route passes the Docker
roots it already reads for the case check. A worktree whose folder is gone
is unaffected (git only drops its record there). Unit and route tests
cover both directions and an unrelated Docker workspace; docs updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-16 01:36:45 +02:00
1 parent 28cbbad591
commit bea9865cff
7 files changed
+94 -12

No files matched your search

+6 -7
View File
@@ -202,15 +202,13 @@ export function registerWorktreeRoutes(
req: FastifyRequest,
reply: FastifyReply,
name: string
): Promise<{ ok: true; repo: CaseRepository; casePath: string } | { ok: false; fail: Fail }> {
): Promise<{ ok: true; repo: CaseRepository; casePath: string; dockerRoots: string[] } | { ok: false; fail: Fail }> {
const folder = await resolveLocalCaseFolder(req, name);
if (!folder.ok) return { ok: false, fail: failWith(reply, folder.code, folder.message) };
const repo = await resolveCaseRepository(folder.casePath, {
git: deps.git,
dockerWorkspaces: await dockerWorkspaces(),
});
const dockerRoots = await dockerWorkspaces();
const repo = await resolveCaseRepository(folder.casePath, { git: deps.git, dockerWorkspaces: dockerRoots });
if (!repo.ok) return { ok: false, fail: failFrom(reply, repo) };
return { ok: true, repo: repo.repo, casePath: folder.casePath };
return { ok: true, repo: repo.repo, casePath: folder.casePath, dockerRoots };
}
app.get('/api/cases/:name/worktrees', async (req, reply): Promise<ApiResponse<CaseWorktreeList>> => {
@@ -282,7 +280,8 @@ export function registerWorktreeRoutes(
isInUse: (path) =>
liveLocalSessions(ctx, resolved.casePath, resolved.repo.repoRoot).some((s) => isPathWithin(s.dir, path)),
},
{ git: deps.git }
// The worktree folder itself must not be (or hold) a Docker case workspace either.
{ git: deps.git, dockerWorkspaces: resolved.dockerRoots }
);
if (!removed.ok) return failFrom(reply, removed);
return { success: true, data: { removed: removed.removed } };