From 328d47f1df45bd53b80863fc22421ed3a5419ca7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 01:53:42 +0000 Subject: [PATCH] fix: normalize ~ in path config --- src/config/io.ts | 12 +++++--- src/config/normalize-paths.ts | 57 +++++++++++++++++++++++++++++++++++ src/config/paths.ts | 2 +- 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 src/config/normalize-paths.ts diff --git a/src/config/io.ts b/src/config/io.ts index 7b1ac6fb0..af86cdc7e 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -24,6 +24,7 @@ import { } from "./defaults.js"; import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js"; import { findLegacyConfigIssues } from "./legacy.js"; +import { normalizeConfigPaths } from "./normalize-paths.js"; import { resolveConfigPath, resolveStateDir } from "./paths.js"; import { applyConfigOverrides } from "./runtime-overrides.js"; import type { @@ -182,6 +183,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { ), ), ); + normalizeConfigPaths(cfg); const duplicates = findDuplicateAgentDirs(cfg, { env: deps.env, @@ -306,10 +308,12 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { raw, parsed: parsedRes.parsed, valid: true, - config: applyTalkApiKey( - applyModelDefaults( - applySessionDefaults( - applyLoggingDefaults(applyMessageDefaults(validated.config)), + config: normalizeConfigPaths( + applyTalkApiKey( + applyModelDefaults( + applySessionDefaults( + applyLoggingDefaults(applyMessageDefaults(validated.config)), + ), ), ), ), diff --git a/src/config/normalize-paths.ts b/src/config/normalize-paths.ts new file mode 100644 index 000000000..ab47bb61c --- /dev/null +++ b/src/config/normalize-paths.ts @@ -0,0 +1,57 @@ +import { resolveUserPath } from "../utils.js"; +import type { ClawdbotConfig } from "./types.js"; + +const PATH_VALUE_RE = /^~(?=$|[\\/])/; + +const PATH_KEY_RE = /(dir|path|paths|file|root|workspace)$/i; +const PATH_LIST_KEYS = new Set(["paths"]); + +function isPlainObject(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function normalizeStringValue(key: string | undefined, value: string): string { + if (!PATH_VALUE_RE.test(value.trim())) return value; + if (!key) return value; + if (PATH_KEY_RE.test(key) || PATH_LIST_KEYS.has(key)) { + return resolveUserPath(value); + } + return value; +} + +function normalizeAny(key: string | undefined, value: unknown): unknown { + if (typeof value === "string") return normalizeStringValue(key, value); + + if (Array.isArray(value)) { + const normalizeChildren = Boolean(key && PATH_LIST_KEYS.has(key)); + return value.map((entry) => { + if (typeof entry === "string") { + return normalizeChildren ? normalizeStringValue(key, entry) : entry; + } + if (Array.isArray(entry)) return normalizeAny(undefined, entry); + if (isPlainObject(entry)) return normalizeAny(undefined, entry); + return entry; + }); + } + + if (!isPlainObject(value)) return value; + + for (const [childKey, childValue] of Object.entries(value)) { + const next = normalizeAny(childKey, childValue); + if (next !== childValue) value[childKey] = next; + } + + return value; +} + +/** + * Normalize "~" paths in path-ish config fields. + * + * Goal: accept `~/...` consistently across config file + env overrides, while + * keeping the surface area small and predictable. + */ +export function normalizeConfigPaths(cfg: ClawdbotConfig): ClawdbotConfig { + if (!cfg || typeof cfg !== "object") return cfg; + normalizeAny(undefined, cfg); + return cfg; +} diff --git a/src/config/paths.ts b/src/config/paths.ts index 054fabc3f..dc2cb4bdd 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -57,7 +57,7 @@ export function resolveConfigPath( stateDir: string = resolveStateDir(env, os.homedir), ): string { const override = env.CLAWDBOT_CONFIG_PATH?.trim(); - if (override) return override; + if (override) return resolveUserPath(override); return path.join(stateDir, "clawdbot.json"); }