openclaw/openclaw · critical · Error

non-canonical file path

Error message

non-canonical file path

What it means

validateOwnedFile requires path.resolve(filePath) to equal fs.realpath(filePath), meaning no component anywhere along the path is a symbolicic link. If realpath differs (the path resolves through one or more symlinks), the path is non-canonical and throws.

Source

Thrown at extensions/browser/src/browser/extension-native-host.ts:83

async function validateOwnedFile(filePath: string, executable: boolean): Promise<string> {
  const resolved = path.resolve(filePath);
  const info = await fs.lstat(resolved);
  if (!info.isFile() || info.isSymbolicLink()) {
    throw new Error("unsafe file type");
  }
  if (process.platform !== "win32") {
    const uid = process.getuid?.();
    if (uid !== undefined && info.uid !== uid) {
      throw new Error("foreign file owner");
    }
    const mode = info.mode & 0o777;
    if ((mode & 0o077) !== 0 || (executable && (mode & 0o100) === 0)) {
      throw new Error("unsafe file mode");
    }
  }
  const canonical = await fs.realpath(resolved);
  if (canonical !== resolved) {
    throw new Error("non-canonical file path");
  }
  return canonical;
}

async function validateNativeManifest(params: {
  manifestPath: string;
  launcherPath: string;
  callerOrigin: string;
  expectedOrigins: string[];
  stateDir?: string;
}): Promise<void> {
  const manifestPath = await validateOwnedFile(params.manifestPath, false);
  const launcherPath = await validateOwnedFile(params.launcherPath, true);
  const managedRoot = path.resolve(
    params.stateDir ?? resolveStateDir(),
    "browser",
    "native-messaging",
  );

View on GitHub (pinned to 01804a7531)

Solutions

  1. Set OPENCLAW_STATE_DIR to a canonical (realpath-resolved) path with no symlink components.
  2. Resolve symlinks in the state directory before passing it: fs.realpathSync(dir).
  3. On macOS, prefer canonical paths (/private/var, /Users) over their symlinked aliases (/var).

Example fix

// before: OPENCLAW_STATE_DIR=/var/lib/openclaw (symlink) -> "non-canonical file path"
// after: use the realpath
process.env.OPENCLAW_STATE_DIR = fs.realpathSync("/var/lib/openclaw");
Defensive patterns

Strategy: try-catch

Validate before calling

import fs from "node:fs/promises";
import path from "node:path";
async function isCanonicalPath(p: string): Promise<boolean> {
  const resolved = path.resolve(p);
  try {
    return (await fs.realpath(resolved)) === resolved;
  } catch {
    return false;
  }
}

Try / catch

try {
  await validateNativeManifest(params);
} catch (error) {
  if (error instanceof Error && error.message === "non-canonical file path") {
    // resolve OPENCLAW_STATE_DIR with realpath to remove symlink components
  }
}

Prevention

When it happens

Trigger: Any directory component of the manifest or launcher path is a symlink, so fs.realpath differs from path.resolve.

Common situations: OPENCLAW_STATE_DIR points into a symlinked directory (common on macOS where /var -> /private/var); a symlinked HOME; the native-messaging directory itself is a symlink; deployment on a system with symlinked /home or /tmp.

Related errors


AI-assisted analysis of openclaw/openclaw@01804a7531 (2026-08-12). Data as JSON: /api/errors/5521494841e029c1. Report an issue: GitHub.