From 70cfb69a5f12a47f95537e58e0726c2395dbdb20 Mon Sep 17 00:00:00 2001 From: Soumik Bhatta Date: Mon, 23 Feb 2026 22:22:52 -0500 Subject: [PATCH] fix(doctor): skip false positive permission warnings for Nix store symlinks (#24901) On NixOS/Nix-managed installs, config and state directories are symlinks into /nix/store/. Symlinks on Linux always report 0o777 via lstatSync, causing `openclaw doctor` to incorrectly warn about open permissions. Use lstatSync to detect symlinks, resolve the target, and only suppress the warning when the resolved path lives in /nix/store/ (an immutable filesystem). Symlinks to insecure targets still trigger warnings. Co-authored-by: Claude Opus 4.6 --- src/commands/doctor-state-integrity.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/commands/doctor-state-integrity.ts b/src/commands/doctor-state-integrity.ts index bccb04964eb..2e31da8e76a 100644 --- a/src/commands/doctor-state-integrity.ts +++ b/src/commands/doctor-state-integrity.ts @@ -261,8 +261,15 @@ export async function noteStateIntegrity( } if (stateDirExists && process.platform !== "win32") { try { - const stat = fs.statSync(stateDir); - if ((stat.mode & 0o077) !== 0) { + const dirLstat = fs.lstatSync(stateDir); + const isDirSymlink = dirLstat.isSymbolicLink(); + // For symlinks, check the resolved target permissions instead of the + // symlink itself (which always reports 777). Skip the warning only when + // the target lives in a known immutable store (e.g. /nix/store/). + const stat = isDirSymlink ? fs.statSync(stateDir) : dirLstat; + const resolvedDir = isDirSymlink ? fs.realpathSync(stateDir) : stateDir; + const isImmutableStore = resolvedDir.startsWith("/nix/store/"); + if (!isImmutableStore && (stat.mode & 0o077) !== 0) { warnings.push( `- State directory permissions are too open (${displayStateDir}). Recommend chmod 700.`, ); @@ -282,10 +289,14 @@ export async function noteStateIntegrity( if (configPath && existsFile(configPath) && process.platform !== "win32") { try { - const linkStat = fs.lstatSync(configPath); - const stat = fs.statSync(configPath); - const isSymlink = linkStat.isSymbolicLink(); - if (!isSymlink && (stat.mode & 0o077) !== 0) { + const configLstat = fs.lstatSync(configPath); + const isSymlink = configLstat.isSymbolicLink(); + // For symlinks, check the resolved target permissions. Skip the warning + // only when the target lives in an immutable store (e.g. /nix/store/). + const stat = isSymlink ? fs.statSync(configPath) : configLstat; + const resolvedConfig = isSymlink ? fs.realpathSync(configPath) : configPath; + const isImmutableConfig = resolvedConfig.startsWith("/nix/store/"); + if (!isImmutableConfig && (stat.mode & 0o077) !== 0) { warnings.push( `- Config file is group/world readable (${displayConfigPath ?? configPath}). Recommend chmod 600.`, );