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
This commit is contained in:
Jerry-Xin 2026-03-14 18:58:23 +08:00
parent c08f2aa21a
commit 9e38b064e9
4 changed files with 76 additions and 2 deletions

View File

@ -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"));
});
});

View File

@ -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<string> = 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;

View File

@ -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", () => {

View File

@ -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) {