Skip to content

memory-wiki: "Refusing to write imported source page through symlink" fires for non-symlink FsSafeError causes #83740

@cknzraposo

Description

@cknzraposo

Summary

wiki_status (and any bridge import write) reports Refusing to write imported source page through symlink: <path> even when no symlink exists anywhere on the page path. The catch in writeImportedSourcePage re-wraps every FsSafeError with a "through symlink" message, hiding the real cause (in my case, a restrictive-permissions/atomic-replace failure on a bridge-imported page written at mode 600).

Where

extensions/memory-wiki/src/source-page-shared.ts, in writeImportedSourcePage:

try {
  if (pageStat && pageStat.nlink > 1) {
    await vault.remove(params.pagePath);
  }
  await vault.write(params.pagePath, rendered);
} catch (error) {
  if (error instanceof FsSafeError) {
    throw new Error(
      `Refusing to write imported source page through symlink: ${params.pagePath}`,
      { cause: error },
    );
  }
  throw error;
}

FsSafeError covers a family of codes (invalid-path, outside-workspace, path-alias, plus the symlink-parent / perms hardening from #66636, #69797/8, #80331). Any of them surface as "through symlink", which is misleading.

extensions/memory-wiki/src/bridge.test.ts asserts on the exact "through symlink" string, so changing the message has a test impact — flagging in case maintainers want to decide direction.

Repro

  1. Run a wiki bridge import that produces an imported source page (e.g. memory bridge from another workspace).
  2. Ensure the resulting page file ends up at mode 600 in an otherwise normal (non-symlinked) directory tree. In my case this happened on first write because the source file (workspace-bulletin/memory/.dreams/events.jsonl) was 600 and the writer inherited restrictive perms.
  3. Trigger a re-render of that page (e.g. wiki_status, which re-runs imported-source writes).

Result: Error: Refusing to write imported source page through symlink: wiki/main/sources/<page>.md — even though realpath of every component shows no symlink.

In my case:

  • Path: wiki/main/sources/bridge-workspace-bulletin-075c90ca-memory-dreams-events-jsonl-38a7dd37.md
  • Mode: -rw------- (only file in sources/ with that mode; all others 664)
  • Verified: no symlink on any ancestor.

Expected

Either:

  • The error message reflects the actual FsSafeError.code (e.g. Refusing to write imported source page (perms-denied): <path>), or
  • The catch is tightened to wrap only the symlink-parent / path-alias codes with the "through symlink" message and lets other FsSafeErrors bubble with their own message.

Either way: surface the real reason so users can fix it (in my case, chmod 644 resolves it instantly once you know what's happening).

Sketch fix

if (error instanceof FsSafeError) {
  const reason =
    error.code === "symlink-parent" || error.code === "path-alias"
      ? "through symlink"
      : `(${error.code})`;
  throw new Error(
    `Refusing to write imported source page ${reason}: ${params.pagePath}`,
    { cause: error },
  );
}

bridge.test.ts would need its assertion updated alongside.

Environment

  • OpenClaw: installed via npm i -g openclaw (whichever was current on 2026-05-18 NZT)
  • Host: WSL2 (Ubuntu) on Windows 11
  • Vault root: workspace bridge importing from a sibling workspace

Happy to send a PR if maintainers confirm the preferred direction (message rewrite vs. tightened catch).

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions