Skip to content

Claude Code hooks use relative paths -> 'Cannot find module' crash when launched outside config root (+ Cursor misdetection) #590

Description

@lyingbird

Summary

The Claude Code adapter generates hook commands using relative script paths (node .claude/hooks/...). Claude Code runs hooks with the directory the CLI was launched from as the working directory — not the user's home. So whenever claude is started from any folder other than the config root (e.g. another drive or a project dir), Node cannot resolve the script and the hook crashes with Cannot find module. On the Stop event this surfaces to the user as a Stop hook error.

A second, related problem: detectPlatform() misidentifies Claude Code as Cursor when both ~/.cursor and ~/.claude exist.

Tested with @evomap/evolver@1.89.17 on Windows 11, Claude Code.

Bug 1 — relative hook paths break outside the config root

src/adapters/claudeCode.js:

function buildClaudeHooks(evolverRoot) {
  const scriptsBase = '.claude/hooks';   // <-- relative
  ...
  command: `node ${scriptsBase}/evolver-session-start.js`,

This produces settings.json entries like node .claude/hooks/evolver-session-end.js. Claude Code executes hooks with cwd = the directory the user launched claude in, so the relative path only resolves when that happens to be the config root (home).

Reproduction

$ cd /d            # any dir without a ./.claude/hooks
$ echo '{}' | node .claude/hooks/evolver-session-end.js
node:internal/modules/cjs/loader:1386
  throw err;
  ^
Error: Cannot find module 'D:\.claude\hooks\evolver-session-end.js'

In a real session the Stop hook then reports:

Stop hook error: Failed with non-blocking status code: node:internal/modules/cjs/loader:1386
  throw err;

Suggested fix

Emit an absolute path. install() already knows configRoot, so thread it into buildClaudeHooks and build the path with it:

function buildClaudeHooks(evolverRoot, configRoot) {
  const scriptsBase = path.join(configRoot, '.claude', 'hooks');
  ...
  command: `node "${path.join(scriptsBase, 'evolver-session-start.js')}"`,

(Quote the path so a config root containing spaces still works.) The statusLine command in a typical Claude Code config already uses an absolute path, so this is consistent with the platform's own conventions.

Bug 2 — Claude Code misdetected as Cursor

src/adapters/hookAdapter.js:

const PLATFORMS = {
  cursor: { ... detector: '.cursor' },
  'claude-code': { ... detector: '.claude' },
  ...
};

function detectPlatform(cwd) {
  ...
  for (const [id, meta] of Object.entries(PLATFORMS)) {
    if (fs.existsSync(path.join(home, meta.detector))) return id;  // first match wins
  }
}

When ~/.cursor and ~/.claude both exist (common — many users have both editors, or a previous run created ~/.cursor), iteration order makes cursor win, so setup-hooks writes to ~/.cursor/hooks even inside a Claude Code session. The hooks then silently don't run under Claude Code.

Suggested fix

Prefer an explicit runtime signal over directory presence. Claude Code sets CLAUDECODE=1 (and CLAUDE_CODE_ENTRYPOINT); Cursor sets CURSOR_TRACE_ID / TERM_PROGRAM=cursor. Check those env vars first, then fall back to directory detection. At minimum, document that --platform=claude-code is required when multiple config dirs coexist.

Environment

  • @evomap/evolver 1.89.17
  • OS: Windows 11
  • Host: Claude Code
  • Node: (relevant) launched from a non-home working directory

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions