Skip to content

Linux: SIGSEGV on first tool call caused by node/bun runtime mismatch (with workaround) #564

@sghaskell

Description

@sghaskell

Related to #556, which was closed without a confirmed root cause on Linux.

Environment

  • OS: Linux (Ubuntu, kernel 6.17)
  • Claude Code: 2.1.141
  • context-mode: 1.0.131
  • Node: 20.20.2 (nvm)
  • Bun: 1.3.13

Symptoms

The MCP server connects successfully at session startup (243ms), but crashes with SIGSEGV the first time any ctx_* tool is invoked, roughly 10–12 seconds later. The skill falls back to CLI. Subsequent tool calls in the same session fail with "dynamic tool unavailable" because the server is dead.

Root cause

The plugin loader invokes start.mjs via node (per .claude-plugin/plugin.json's "command": "node"). On Linux, better-sqlite3's native addon segfaults under node's V8 — it requires bun's runtime environment. The doctor's SQLite check passes because it runs under bun; the actual server process doesn't.

Confirmed via a thin wrapper around start.mjs that logs the child process exit signal:

stdin data 306b       ← initialize
stdin data 202b       ← notifications/initialized
stdin data 177b       ← first tool call arrives
child exit code=null sig=SIGSEGV   ← 343ms later

Workaround

Backup the original and replace start.mjs with a wrapper that spawns start.real.mjs using bun:

#!/usr/bin/env node
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __dirname = dirname(fileURLToPath(import.meta.url));
const child = spawn(process.env.HOME + '/.bun/bin/bun', [join(__dirname, 'start.real.mjs')], {
  stdio: ['pipe', 'inherit', 'inherit'],
  env: process.env,
});

process.stdin.on('data', (chunk) => { if (!child.stdin.destroyed) child.stdin.write(chunk); });
process.stdin.on('end', () => {});
const _k = setInterval(() => {}, 2147483647);
child.on('exit', (code) => { clearInterval(_k); process.exit(code ?? 0); });

With bun spawning the real server, ctx_doctor and other tools work correctly.

Suggested fix

The plugin manifest could specify bun as the command (with a node fallback), or start.mjs could detect the active runtime and re-exec itself under bun if it was invoked via node.


Note: this is separate from #559 (zombie processes after upgrade), though both surfaced during the same debugging session.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions