Skip to content

Bridge silently no-ops (exit 0, no output) when plugin root is a symlink #22

Description

@benngermin

Summary

Every /grok-build:* command is a silent no-op when the Claude Code plugin root is reached through a symlink. The bridge script exits 0 and prints nothing, so it looks like the command ran and produced no findings rather than like a failure.

Cause

scripts/grok-bridge.mjs guards its entrypoint by comparing the raw process.argv[1] against the module URL:

const isMain =
  process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);

path.resolve() normalizes but does not resolve symlinks, while import.meta.url is the realpath Node resolved when loading the module. If any component of the invoking path is a symlink, the two strings differ, isMain is false, main() never runs, and the process exits 0 with no output.

This is not exotic: Claude Code supports multiple config homes (CLAUDE_CONFIG_DIR), and a common setup is to symlink plugins/ between them so one install serves every profile. The slash commands invoke the bridge via $CLAUDE_PLUGIN_ROOT, which then points at the symlinked path.

Reproduction

ln -s ~/.claude ~/.claude-alt

# silent, exit 0, no output
node ~/.claude-alt/plugins/cache/xai-grok-build/grok-build/0.2.1/scripts/grok-bridge.mjs check --json

# works
node ~/.claude/plugins/cache/xai-grok-build/grok-build/0.2.1/scripts/grok-bridge.mjs check --json

Observed on plugin 0.2.1, Node v24.18.0, grok 0.2.118, macOS.

Suggested fix

Resolve symlinks on both sides before comparing, with a fallback for paths that cannot be realpath'd:

const resolveEntrypoint = (candidate) => {
  try {
    return fs.realpathSync(candidate);
  } catch {
    return path.resolve(candidate);
  }
};

const isMain =
  process.argv[1] &&
  resolveEntrypoint(process.argv[1]) === resolveEntrypoint(fileURLToPath(import.meta.url));

fs is already imported in the module. Patching this locally makes every command work through the symlinked root.

Separately, it may be worth having an unknown/failed entrypoint state exit non-zero rather than 0, so a future variant of this fails loudly instead of looking like an empty result.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions