Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions src/adapters/claudeCode.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,31 @@ const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts,
const HOOK_SCRIPTS_DIR_NAME = 'hooks';
const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';

// Resolve an evolver hook script from the PROJECT's .claude/hooks first, then
// fall back to the user-home install. The evolver can be installed at the
// home/global scope (one shared install), but Claude Code resolves a bare
// `.claude/hooks/...` command path against each *project* dir — so a bare
// relative command silently fails in every project except the install root.
// This emits a project-or-home fallback, cross-platform, so the hook fires no
// matter where the scripts were copied.
function buildHookCommand(scriptName) {
if (process.platform === 'win32') {
const proj = `%CLAUDE_PROJECT_DIR%\\.claude\\hooks\\${scriptName}`;
const home = `%USERPROFILE%\\.claude\\hooks\\${scriptName}`;
return `cmd /c "IF EXIST "${proj}" (node "${proj}") ELSE (node "${home}")"`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows hook cmd quoting broken

High Severity

The win32 branch of buildHookCommand uses nested double quotes within the cmd /c "..." command. cmd.exe misinterprets the quote after IF EXIST as closing the outer command, which causes the project-or-home fallback logic to parse incorrectly and fail silently for global Windows installs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1bb856b. Configure here.

}
return `sh -c 'f="$CLAUDE_PROJECT_DIR/.claude/hooks/${scriptName}"; [ -f "$f" ] || f="$HOME/.claude/hooks/${scriptName}"; node "$f"'`;
}

function buildClaudeHooks(evolverRoot) {
const scriptsBase = '.claude/hooks';
return {
hooks: {
SessionStart: [
{
hooks: [
{
type: 'command',
command: `node ${scriptsBase}/evolver-session-start.js`,
command: buildHookCommand('evolver-session-start.js'),
timeout: 3,
},
],
Expand All @@ -32,7 +47,7 @@ function buildClaudeHooks(evolverRoot) {
// ABOVE that watchdog, so the host never kills the script
// mid-write. A stuck/slow recall can never block or erase the
// user's prompt.
command: `node ${scriptsBase}/evolver-task-recall.js`,
command: buildHookCommand('evolver-task-recall.js'),
timeout: 5,
},
],
Expand All @@ -44,7 +59,7 @@ function buildClaudeHooks(evolverRoot) {
hooks: [
{
type: 'command',
command: `node ${scriptsBase}/evolver-signal-detect.js`,
command: buildHookCommand('evolver-signal-detect.js'),
timeout: 2,
},
],
Expand All @@ -55,7 +70,7 @@ function buildClaudeHooks(evolverRoot) {
hooks: [
{
type: 'command',
command: `node ${scriptsBase}/evolver-session-end.js`,
command: buildHookCommand('evolver-session-end.js'),
timeout: 8,
},
],
Expand Down
23 changes: 23 additions & 0 deletions test/adapters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,29 @@ describe('claudeCode adapter', () => {
}
assert.equal(hooks.hooks.PostToolUse[0].matcher, 'Write');
});

it('buildClaudeHooks commands resolve from project OR home scope', () => {
// Regression: a bare `node .claude/hooks/...` command is resolved by
// Claude Code against each project dir, so a home/global-scoped install
// silently fails everywhere except the install root. Every emitted command
// must reference both the project scope and a home-scope fallback, and stay
// recognizable to the uninstall/merge matcher.
const hooks = claudeAdapter.buildClaudeHooks('/evolver');
const commands = [];
for (const event of Object.keys(hooks.hooks)) {
for (const matcher of hooks.hooks[event]) {
for (const cmd of matcher.hooks) commands.push(cmd.command);
}
}
assert.ok(commands.length >= 4, 'expected at least 4 hook commands');
const homeMarker = process.platform === 'win32' ? 'USERPROFILE' : 'HOME';
for (const cmd of commands) {
assert.ok(cmd.includes('CLAUDE_PROJECT_DIR'), `command must try project scope: ${cmd}`);
assert.ok(cmd.includes(homeMarker), `command must fall back to home scope: ${cmd}`);
assert.ok(/evolver-[a-z-]+\.js/.test(cmd), `command must invoke an evolver hook script: ${cmd}`);
assert.ok(hookAdapter.isEvolverHookCommand(cmd), `command must stay recognizable to uninstall: ${cmd}`);
}
});
});

// -- Codex adapter --
Expand Down