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
23 changes: 10 additions & 13 deletions packages/installer/src/wizard/ide-config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -703,36 +703,37 @@ async function copyClaudeHooksFolder(projectRoot) {
}

/**
* Hook event mapping: fileName → { event, matcher, timeout }
* Hook event mapping: fileName → { event, matcher }
* Maps each .cjs hook file to its correct Claude Code event.
* Extensible: add new hooks here as they are created.
*
* NOTE: timeout is intentionally omitted — Claude Code manages hook timeouts
* natively (default 60s). Overriding with low values (e.g. 10) caused
* premature kills on Windows. Each hook has its own internal safety timeout
* (e.g. synapse-engine.cjs uses 5s via setTimeout + unref).
*
* @see Story MIS-3.1 - Fix Session-Digest Hook Registration
* @see https://code.claude.com/docs/en/hooks (Claude Code Hooks Documentation)
*/
const HOOK_EVENT_MAP = {
'synapse-engine.cjs': {
event: 'UserPromptSubmit',
matcher: null,
timeout: 10,
},
'code-intel-pretool.cjs': {
event: 'PreToolUse',
matcher: 'Write|Edit',
timeout: 10,
},
'precompact-session-digest.cjs': {
event: 'PreCompact',
matcher: null,
timeout: 10,
},
};

/** Default event config for unmapped hooks (backwards compatible). */
const DEFAULT_HOOK_CONFIG = {
event: 'UserPromptSubmit',
matcher: null,
timeout: 10,
};

/**
Expand All @@ -759,8 +760,6 @@ async function createClaudeSettingsLocal(projectRoot) {
return null;
}

const isWindows = process.platform === 'win32';

let settings = {};

// Merge with existing settings if present
Expand All @@ -781,7 +780,6 @@ async function createClaudeSettingsLocal(projectRoot) {

// Register each .cjs hook file under its correct event
for (const hookFileName of hookFiles) {
const hookFilePath = path.join(hooksDir, hookFileName);
const hookConfig = HOOK_EVENT_MAP[hookFileName] || DEFAULT_HOOK_CONFIG;
const eventName = hookConfig.event;

Expand All @@ -790,10 +788,10 @@ async function createClaudeSettingsLocal(projectRoot) {
settings.hooks[eventName] = [];
}

// Windows workaround: $CLAUDE_PROJECT_DIR has known bug on Windows (GH #6023/#5814)
const hookCommand = isWindows
? `node "${hookFilePath.replace(/\\/g, '\\\\')}"` // Absolute path with escaped backslashes
: `node "$CLAUDE_PROJECT_DIR/.claude/hooks/${hookFileName}"`;
// Use relative path — works on all platforms, no $CLAUDE_PROJECT_DIR bugs
// (GH #6023/#5814), no fragile absolute Windows paths with escaped backslashes.
// Claude Code resolves relative paths from the project root (cwd).
const hookCommand = `node .claude/hooks/${hookFileName}`;

Comment on lines +791 to 795

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Quote and sanitize hook filenames before building shell commands.

On Line 794, hookFileName is interpolated directly into a shell command. Filenames with spaces or shell metacharacters can break execution or cause command injection.

Proposed fix
-  const hookFiles = allFiles.filter(f => f.endsWith('.cjs'));
+  const SAFE_HOOK_FILE_RE = /^[A-Za-z0-9._-]+\.cjs$/;
+  const hookFiles = allFiles.filter((f) => SAFE_HOOK_FILE_RE.test(f));

@@
-    const hookCommand = `node .claude/hooks/${hookFileName}`;
+    const hookCommand = `node ".claude/hooks/${hookFileName}"`;

As per coding guidelines, "Look for potential security vulnerabilities."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/installer/src/wizard/ide-config-generator.js` around lines 791 -
795, The current construction of hookCommand interpolates hookFileName directly
and is vulnerable to spaces or shell metacharacters; instead sanitize or avoid
the shell: validate hookFileName against a safe pattern (e.g., allow only
alphanumerics, dots, dashes, underscores) and/or escape quotes/metacharacters in
hookFileName, then either wrap the sanitized name in quotes when building
hookCommand or—preferably—stop using a single shell string and invoke node with
an argv array (e.g., use child_process.spawn/execFile with arguments
['.claude/hooks/<sanitizedName>']) so the value of hookFileName cannot be
interpreted by the shell. Ensure changes target the hookCommand construction
that uses hookFileName.

// Check if this hook is already registered under this event
const hookBaseName = hookFileName.replace('.cjs', '');
Expand All @@ -810,7 +808,6 @@ async function createClaudeSettingsLocal(projectRoot) {
{
type: 'command',
command: hookCommand,
timeout: hookConfig.timeout,
},
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,23 +174,23 @@ describe('artifact-copy-pipeline (Story INS-4.3)', () => {
expect(config).toBeDefined();
expect(config.event).toBe('UserPromptSubmit');
expect(config.matcher).toBeNull();
expect(config.timeout).toBe(10);
expect(config.timeout).toBeUndefined(); // Claude Code manages timeouts
});

test('maps code-intel-pretool.cjs to PreToolUse with Write|Edit matcher', () => {
const config = HOOK_EVENT_MAP['code-intel-pretool.cjs'];
expect(config).toBeDefined();
expect(config.event).toBe('PreToolUse');
expect(config.matcher).toBe('Write|Edit');
expect(config.timeout).toBe(10);
expect(config.timeout).toBeUndefined(); // Claude Code manages timeouts
});

test('maps precompact-session-digest.cjs to PreCompact', () => {
const config = HOOK_EVENT_MAP['precompact-session-digest.cjs'];
expect(config).toBeDefined();
expect(config.event).toBe('PreCompact');
expect(config.matcher).toBeNull();
expect(config.timeout).toBe(10);
expect(config.timeout).toBeUndefined(); // Claude Code manages timeouts
});

test('covers all 3 known hooks', () => {
Expand All @@ -204,7 +204,7 @@ describe('artifact-copy-pipeline (Story INS-4.3)', () => {
test('DEFAULT_HOOK_CONFIG falls back to UserPromptSubmit', () => {
expect(DEFAULT_HOOK_CONFIG.event).toBe('UserPromptSubmit');
expect(DEFAULT_HOOK_CONFIG.matcher).toBeNull();
expect(DEFAULT_HOOK_CONFIG.timeout).toBe(10);
expect(DEFAULT_HOOK_CONFIG.timeout).toBeUndefined(); // Claude Code manages timeouts
});
});

Expand Down
Loading