Skip to content

Commit 54be0a8

Browse files
authored
fix: add safeguards against deletes (#221)
1 parent 74d12a2 commit 54be0a8

File tree

4 files changed

+79
-2
lines changed

4 files changed

+79
-2
lines changed

apps/array/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@posthog/array",
3-
"version": "0.10.0",
3+
"version": "0.11.0",
44
"description": "Array - PostHog desktop task manager",
55
"main": ".vite/build/index.js",
66
"versionHash": "dynamic",

apps/array/src/main/services/workspace/workspaceService.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,11 +449,43 @@ export class WorkspaceService {
449449
const repoName = path.basename(folderPath);
450450
const repoWorktreeFolderPath = path.join(worktreeBasePath, repoName);
451451

452+
// Safety check 1: Never delete the project folder itself
453+
if (path.resolve(repoWorktreeFolderPath) === path.resolve(folderPath)) {
454+
log.warn(
455+
`Skipping cleanup of worktree folder: path matches project folder (${folderPath})`,
456+
);
457+
return;
458+
}
459+
452460
if (!fs.existsSync(repoWorktreeFolderPath)) {
453461
return;
454462
}
455463

464+
// Safety check 2: Check if any other registered folder shares the same basename
465+
const allFolders = foldersStore.get("folders", []);
466+
const otherFoldersWithSameName = allFolders.filter(
467+
(f) => f.path !== folderPath && path.basename(f.path) === repoName,
468+
);
469+
470+
if (otherFoldersWithSameName.length > 0) {
471+
log.info(
472+
`Skipping cleanup of worktree folder ${repoWorktreeFolderPath}: used by other folders: ${otherFoldersWithSameName.map((f) => f.path).join(", ")}`,
473+
);
474+
return;
475+
}
476+
456477
try {
478+
// Safety check 3: Only delete if empty (ignoring .DS_Store)
479+
const files = fs.readdirSync(repoWorktreeFolderPath);
480+
const validFiles = files.filter((f) => f !== ".DS_Store");
481+
482+
if (validFiles.length > 0) {
483+
log.info(
484+
`Skipping cleanup of worktree folder ${repoWorktreeFolderPath}: folder not empty (contains: ${validFiles.slice(0, 3).join(", ")}${validFiles.length > 3 ? "..." : ""})`,
485+
);
486+
return;
487+
}
488+
457489
fs.rmSync(repoWorktreeFolderPath, { recursive: true, force: true });
458490
log.info(`Cleaned up worktree folder at ${repoWorktreeFolderPath}`);
459491
} catch (error) {

packages/agent/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@posthog/agent",
3-
"version": "1.27.0",
3+
"version": "1.28.0",
44
"repository": "https://github.com/PostHog/array",
55
"description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
66
"main": "./dist/index.js",

packages/agent/src/worktree-manager.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,51 @@ export class WorktreeManager {
686686
}
687687

688688
async deleteWorktree(worktreePath: string): Promise<void> {
689+
const resolvedWorktreePath = path.resolve(worktreePath);
690+
const resolvedMainRepoPath = path.resolve(this.mainRepoPath);
691+
692+
// Safety check 1: Never delete the main repo path
693+
if (resolvedWorktreePath === resolvedMainRepoPath) {
694+
const error = new Error(
695+
"Cannot delete worktree: path matches main repo path",
696+
);
697+
this.logger.error("Safety check failed", { worktreePath, error });
698+
throw error;
699+
}
700+
701+
// Safety check 2: Never delete a parent of the main repo path
702+
if (
703+
resolvedMainRepoPath.startsWith(resolvedWorktreePath) &&
704+
resolvedMainRepoPath !== resolvedWorktreePath
705+
) {
706+
const error = new Error(
707+
"Cannot delete worktree: path is a parent of main repo path",
708+
);
709+
this.logger.error("Safety check failed", { worktreePath, error });
710+
throw error;
711+
}
712+
713+
// Safety check 3: Check for .git directory (indicates main repo)
714+
try {
715+
const gitPath = path.join(resolvedWorktreePath, ".git");
716+
const stat = await fs.stat(gitPath);
717+
if (stat.isDirectory()) {
718+
const error = new Error(
719+
"Cannot delete worktree: path appears to be a main repository (contains .git directory)",
720+
);
721+
this.logger.error("Safety check failed", { worktreePath, error });
722+
throw error;
723+
}
724+
} catch (error) {
725+
// If .git doesn't exist or we can't read it, proceed (unless it was the directory check above)
726+
if (
727+
error instanceof Error &&
728+
error.message.includes("Cannot delete worktree")
729+
) {
730+
throw error;
731+
}
732+
}
733+
689734
this.logger.info("Deleting worktree", { worktreePath });
690735

691736
try {

0 commit comments

Comments
 (0)