Skip to content

Commit 743051d

Browse files
xydigit-sjsallyom
andauthored
fix(uninstall): refuse to remove current working directory during cleanup (#90813)
* fix(uninstall): refuse to remove current working directory during cleanup * fix(uninstall): guard cleanup ancestors of cwd --------- Co-authored-by: sallyom <somalley@redhat.com>
1 parent 153a2ba commit 743051d

3 files changed

Lines changed: 45 additions & 1 deletion

File tree

docs/install/uninstall.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ openclaw uninstall
2121

2222
When using the CLI, state removal preserves configured workspace directories unless you also select `--workspace`.
2323

24-
Non-interactive (automation / npx):
24+
Preview what will be removed (safe):
25+
26+
```bash
27+
openclaw uninstall --dry-run --all
28+
```
29+
30+
Non-interactive (automation / npx). Use with caution and only after confirming scopes:
2531

2632
```bash
2733
openclaw uninstall --all --yes --non-interactive

src/commands/cleanup-utils.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { RuntimeEnv } from "../runtime.js";
99
import { withEnvAsync } from "../test-utils/env.js";
1010
import {
1111
buildCleanupPlan,
12+
removePath,
1213
removeStateAndLinkedPaths,
1314
removeWorkspaceAttestationPaths,
1415
removeWorkspaceDirs,
@@ -201,4 +202,38 @@ describe("cleanup path removals", () => {
201202
await fs.rm(tmpRoot, { recursive: true, force: true });
202203
}
203204
});
205+
206+
it("refuses to remove the current working directory", async () => {
207+
const runtime = createRuntimeMock();
208+
const result = await removePath(process.cwd(), runtime, { dryRun: true });
209+
210+
expect(result.ok).toBe(false);
211+
expect(result.skipped).toBeUndefined();
212+
expect(runtime.error.mock.calls.length).toBe(1);
213+
expect(runtime.error.mock.calls[0][0]).toMatch(/Refusing to remove unsafe path/);
214+
expect(runtime.log.mock.calls.length).toBe(0);
215+
});
216+
217+
it("refuses to remove a directory containing the current working directory", async () => {
218+
const runtime = createRuntimeMock();
219+
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cleanup-cwd-"));
220+
const nestedCwd = path.join(tmpRoot, "nested");
221+
const cwdSpy = vi.spyOn(process, "cwd");
222+
223+
try {
224+
await fs.mkdir(nestedCwd);
225+
cwdSpy.mockReturnValue(nestedCwd);
226+
227+
const result = await removePath(tmpRoot, runtime, { dryRun: true });
228+
229+
expect(result.ok).toBe(false);
230+
expect(result.skipped).toBeUndefined();
231+
expect(runtime.error.mock.calls.length).toBe(1);
232+
expect(runtime.error.mock.calls[0][0]).toMatch(/Refusing to remove unsafe path/);
233+
expect(runtime.log.mock.calls.length).toBe(0);
234+
} finally {
235+
cwdSpy.mockRestore();
236+
await fs.rm(tmpRoot, { recursive: true, force: true });
237+
}
238+
});
204239
});

src/commands/cleanup-utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ function isUnsafeRemovalTarget(target: string): boolean {
8383
if (home && resolved === path.resolve(home)) {
8484
return true;
8585
}
86+
if (isPathWithin(path.resolve(process.cwd()), resolved)) {
87+
return true;
88+
}
8689
return false;
8790
}
8891

0 commit comments

Comments
 (0)