nanocoai/nanoclaw · error
chat-sdk bridge instance ${JSON.stringify(config.instance)}
Error message
chat-sdk bridge instance ${JSON.stringify(config.instance)} must be URL-safe: non-empty, only letters, digits, '.', '_' or '-' What it means
Any symlink encountered anywhere inside the plugin tree throws with the offending relative path. Symlinks could escape the containment boundary (e.g. linking to ~/.ssh), so plugins must ship real files only.
Source
Thrown at src/channels/chat-sdk-bridge.ts:432
if (cut <= 0) cut = limit;
chunks.push(remaining.slice(0, cut).trimEnd());
remaining = remaining.slice(cut).trimStart();
}
if (remaining.length > 0) chunks.push(remaining);
return chunks;
}
export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter {
const { adapter } = config;
// The instance name becomes a webhook route segment (the route regex is
// [^/?]+) and ':' is the state-namespace delimiter — reject anything that
// would break either, at construction time rather than at first webhook.
// Positive allow-list (not a deny-list): also rejects '' and
// whitespace-only names, which are config bugs — '' is falsy, so it
// would skip a truthiness guard, dead-end the webhook route, and
// collapse the state namespace into the default instance's keyspace.
if (config.instance !== undefined && !INSTANCE_KEY_RE.test(config.instance)) {
throw new Error(
`chat-sdk bridge instance ${JSON.stringify(config.instance)} must be URL-safe: ` +
`non-empty, only letters, digits, '.', '_' or '-'`,
);
}
const transformText = (t: string): string => (config.transformOutboundText ? config.transformOutboundText(t) : t);
/** Registry/routing key for this bridge — also the app-context cache
* namespace. Default instances key by the platform name. */
const instanceKey = config.instance ?? adapter.name;
let chat: Chat;
let state: SqliteStateAdapter;
let setupConfig: ChannelSetup;
let gatewayAbort: AbortController | null = null;
async function messageToInbound(
message: ChatMessage,
isMention: boolean,
isGroup?: boolean,
): Promise<InboundMessage> {View on GitHub (pinned to 294ef2aee8)
Solutions
- Replace the symlink with a real copy of the target's contents
- If developing, script a sync step that copies instead of links
- Re-package/extract the plugin with symlink dereferencing (e.g. 'cp -rL')
Example fix
# before ln -s ../../shared/skills skills # after cp -r ../../shared/skills skills
Defensive patterns
Strategy: validation
Validate before calling
function hasSymlink(dir: string): boolean {
let found = false;
const walk = (d: string) => {
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
if (e.isSymbolicLink()) { found = true; return; }
const p = path.join(d, e.name);
if (e.isDirectory()) walk(p);
}
};
walk(dir);
return found;
}
if (hasSymlink(dir)) { /* copy -rL to materialize */ } Try / catch
try { walkPluginDir(dir); } catch (e) { if (e instanceof Error && e.message.includes('is a symlink')) { /* cp -rL into a fresh dir and retry */ } else throw e; } Prevention
- Package plugins with 'cp -rL' or an archive step that dereferences symlinks
- Never symlink shared assets into a template; script a copy step instead
When it happens
Trigger: A symlink anywhere in the plugin directory — e.g. skills/ -> ../../shared/skills, or a node_modules-style link — even if it points inside the tree.
Common situations: Developers symlinking shared skill folders into a template during development and forgetting to materialize them; some editors/git workflows creating links; extraction tools preserving stored symlinks.
Related errors
- engage_mode '${w.engage_mode}' can never engage on channel '
- no verb "${verbArg}" on ${res.plural} — run `ncl ${res.plura
- --${column.name.replace(/_/g, '-')} must be a number
- ${NANOCLAW_EXTENSION_NS}/context/instructions.md must be a r
- ${settingsFile} hooks.SessionStart must be an array
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/bdc135f39a1218c4.
Report an issue: GitHub.