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
- Run a wiki bridge import that produces an imported source page (e.g. memory bridge from another workspace).
- 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.
- 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).
Summary
wiki_status(and any bridge import write) reportsRefusing to write imported source page through symlink: <path>even when no symlink exists anywhere on the page path. The catch inwriteImportedSourcePagere-wraps everyFsSafeErrorwith 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, inwriteImportedSourcePage:FsSafeErrorcovers 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.tsasserts on the exact "through symlink" string, so changing the message has a test impact — flagging in case maintainers want to decide direction.Repro
600in 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.wiki_status, which re-runs imported-source writes).Result:
Error: Refusing to write imported source page through symlink: wiki/main/sources/<page>.md— even thoughrealpathof every component shows no symlink.In my case:
wiki/main/sources/bridge-workspace-bulletin-075c90ca-memory-dreams-events-jsonl-38a7dd37.md-rw-------(only file insources/with that mode; all others 664)Expected
Either:
FsSafeError.code(e.g.Refusing to write imported source page (perms-denied): <path>), orFsSafeErrors bubble with their own message.Either way: surface the real reason so users can fix it (in my case,
chmod 644resolves it instantly once you know what's happening).Sketch fix
bridge.test.tswould need its assertion updated alongside.Environment
npm i -g openclaw(whichever was current on 2026-05-18 NZT)Happy to send a PR if maintainers confirm the preferred direction (message rewrite vs. tightened catch).