From 9e38b064e95a4af57d3e8214ee222dc43220e665 Mon Sep 17 00:00:00 2001 From: Jerry-Xin Date: Sat, 14 Mar 2026 18:58:23 +0800 Subject: [PATCH] fix(config): prevent nested .openclaw directory when OPENCLAW_HOME is set When OPENCLAW_HOME is explicitly set to a path ending with a known state directory name (.openclaw, .clawdbot, .moldbot, .moltbot), both resolveStateDir() and resolveConfigDir() now return the path directly without appending another .openclaw segment. This fixes the nested directory bug where OPENCLAW_HOME=~/.openclaw would produce ~/.openclaw/.openclaw instead of ~/.openclaw. Fixes #45765 --- src/config/paths.test.ts | 32 ++++++++++++++++++++++++++++++++ src/config/paths.ts | 15 +++++++++++++++ src/utils.test.ts | 20 ++++++++++++++++++++ src/utils.ts | 11 +++++++++-- 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/config/paths.test.ts b/src/config/paths.test.ts index 6d2ffcfaf08..ab16f26fb1a 100644 --- a/src/config/paths.test.ts +++ b/src/config/paths.test.ts @@ -141,3 +141,35 @@ describe("state + config path candidates", () => { }); }); }); + +describe("resolveStateDir nesting guard (#45765)", () => { + it("does not append .openclaw when OPENCLAW_HOME ends with .openclaw", () => { + const env = { OPENCLAW_HOME: "/home/user/.openclaw" } as NodeJS.ProcessEnv; + expect(resolveStateDir(env)).toBe(path.resolve("/home/user/.openclaw")); + }); + + it("does not append .openclaw when OPENCLAW_HOME ends with .clawdbot", () => { + const env = { OPENCLAW_HOME: "/home/user/.clawdbot" } as NodeJS.ProcessEnv; + expect(resolveStateDir(env)).toBe(path.resolve("/home/user/.clawdbot")); + }); + + it("does not append .openclaw when OPENCLAW_HOME ends with .moldbot", () => { + const env = { OPENCLAW_HOME: "/home/user/.moldbot" } as NodeJS.ProcessEnv; + expect(resolveStateDir(env)).toBe(path.resolve("/home/user/.moldbot")); + }); + + it("handles tilde expansion when OPENCLAW_HOME=~/.openclaw", () => { + const env = { OPENCLAW_HOME: "~/.openclaw", HOME: "/home/user" } as NodeJS.ProcessEnv; + expect(resolveStateDir(env)).toBe(path.resolve("/home/user/.openclaw")); + }); + + it("still appends .openclaw when OPENCLAW_HOME is not a state dir basename", () => { + const env = { OPENCLAW_HOME: "/srv/app" } as NodeJS.ProcessEnv; + expect(resolveStateDir(env)).toBe(path.resolve("/srv/app/.openclaw")); + }); + + it("still appends .openclaw when OPENCLAW_HOME is not set", () => { + const env = { HOME: "/home/user" } as NodeJS.ProcessEnv; + expect(resolveStateDir(env)).toBe(path.resolve("/home/user/.openclaw")); + }); +}); diff --git a/src/config/paths.ts b/src/config/paths.ts index 84c27749bcf..1240e1ba3c7 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -21,6 +21,12 @@ export const isNixMode = resolveIsNixMode(); const LEGACY_STATE_DIRNAMES = [".clawdbot", ".moldbot", ".moltbot"] as const; const NEW_STATE_DIRNAME = ".openclaw"; const CONFIG_FILENAME = "openclaw.json"; + +/** All recognized state directory basenames (for nesting guard). */ +export const ALL_STATE_DIRNAMES: ReadonlySet = new Set([ + NEW_STATE_DIRNAME, + ...LEGACY_STATE_DIRNAMES, +]); const LEGACY_CONFIG_FILENAMES = ["clawdbot.json", "moldbot.json", "moltbot.json"] as const; function resolveDefaultHomeDir(): string { @@ -66,6 +72,15 @@ export function resolveStateDir( if (override) { return resolveUserPath(override, env, effectiveHomedir); } + // Nesting guard: when OPENCLAW_HOME is explicitly set and its basename is + // already a known state directory name, use it directly without appending. + const explicitHome = env.OPENCLAW_HOME?.trim(); + if (explicitHome) { + const resolvedHome = effectiveHomedir(); + if (ALL_STATE_DIRNAMES.has(path.basename(resolvedHome))) { + return resolvedHome; + } + } const newDir = newStateDir(effectiveHomedir); if (env.OPENCLAW_TEST_FAST === "1") { return newDir; diff --git a/src/utils.test.ts b/src/utils.test.ts index 8880f41f6b1..4bb3b16422a 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -139,6 +139,26 @@ describe("resolveConfigDir", () => { expect(resolveConfigDir(env)).toBe(path.resolve("/tmp/openclaw-home", "state")); }); + + it("does not append .openclaw when OPENCLAW_HOME ends with .openclaw (#45765)", () => { + const env = { OPENCLAW_HOME: "/home/user/.openclaw" } as NodeJS.ProcessEnv; + expect(resolveConfigDir(env)).toBe(path.resolve("/home/user/.openclaw")); + }); + + it("does not append .openclaw when OPENCLAW_HOME ends with .clawdbot (#45765)", () => { + const env = { OPENCLAW_HOME: "/home/user/.clawdbot" } as NodeJS.ProcessEnv; + expect(resolveConfigDir(env)).toBe(path.resolve("/home/user/.clawdbot")); + }); + + it("handles tilde expansion when OPENCLAW_HOME=~/.openclaw (#45765)", () => { + const env = { OPENCLAW_HOME: "~/.openclaw", HOME: "/home/user" } as NodeJS.ProcessEnv; + expect(resolveConfigDir(env)).toBe(path.resolve("/home/user/.openclaw")); + }); + + it("still appends .openclaw when OPENCLAW_HOME is not a state dir basename", () => { + const env = { OPENCLAW_HOME: "/srv/app" } as NodeJS.ProcessEnv; + expect(resolveConfigDir(env)).toBe(path.resolve("/srv/app/.openclaw")); + }); }); describe("resolveHomeDir", () => { diff --git a/src/utils.ts b/src/utils.ts index caf5edb1969..158e4ff080c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { resolveOAuthDir } from "./config/paths.js"; +import { ALL_STATE_DIRNAMES, resolveOAuthDir } from "./config/paths.js"; import { logVerbose, shouldLogVerbose } from "./globals.js"; import { resolveEffectiveHomeDir, @@ -290,7 +290,14 @@ export function resolveConfigDir( if (override) { return resolveUserPath(override, env, homedir); } - const newDir = path.join(resolveRequiredHomeDir(env, homedir), ".openclaw"); + // Nesting guard: when OPENCLAW_HOME is explicitly set and its basename is + // already a known state directory name, use it directly without appending. + const explicitHome = env.OPENCLAW_HOME?.trim(); + const resolvedHome = resolveRequiredHomeDir(env, homedir); + if (explicitHome && ALL_STATE_DIRNAMES.has(path.basename(resolvedHome))) { + return resolvedHome; + } + const newDir = path.join(resolvedHome, ".openclaw"); try { const hasNew = fs.existsSync(newDir); if (hasNew) {