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

  1. Replace the symlink with a real copy of the target's contents
  2. If developing, script a sync step that copies instead of links
  3. 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

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


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/bdc135f39a1218c4. Report an issue: GitHub.