Skip to content
Closed
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, let's verify the file exists and check the content around lines 791-795
echo "=== Checking file existence and content ==="
wc -l packages/installer/src/wizard/ide-config-generator.js

echo -e "\n=== Content around lines 791-795 ==="
sed -n '785,805p' packages/installer/src/wizard/ide-config-generator.js

Repository: SynkraAI/aios-core

Length of output: 1069


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for hook filename filtering and validation
echo "=== Searching for hook file filtering and validation ==="
rg -n -B5 -A5 "hookFiles.*filter|hookFileName|\.cjs" packages/installer/src/wizard/ide-config-generator.js | head -100

Repository: SynkraAI/aios-core

Length of output: 3110


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what happens with hookCommand after it's created
echo "=== Searching for hookCommand usage ==="
rg -n -A10 "const hookCommand" packages/installer/src/wizard/ide-config-generator.js

Repository: SynkraAI/aios-core

Length of output: 614


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for hookCommand usage downstream
echo "=== Looking for hookCommand usage ==="
rg -n "hookCommand" packages/installer/src/wizard/ide-config-generator.js

Repository: SynkraAI/aios-core

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get more context after line 794
echo "=== Content from line 794 to end of function ==="
sed -n '794,850p' packages/installer/src/wizard/ide-config-generator.js

Repository: SynkraAI/aios-core

Length of output: 1871


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if this file uses shell execution functions and how
echo "=== Searching for shell execution patterns ==="
rg -n "exec|spawn|child_process|shell.*true" packages/installer/src/wizard/ide-config-generator.js

Repository: SynkraAI/aios-core

Length of output: 583


Escape and validate hook filenames before composing command

Line 794 interpolates hookFileName directly into a command string without escaping or validation. Although .cjs files are filtered by extension (line 757), this alone is insufficient to prevent shell injection. A filename containing spaces or shell metacharacters (e.g., hook$(whoami).cjs or hook; cmd.cjs) would pass the filter and be embedded in the command string. Even though hookCommand is stored as a string for later execution rather than directly executed here, the command is eventually passed to Claude Code's hook runner, where it may be executed in a shell context.

Harden by validating filenames against an allowlist pattern and safely quoting the path:

-    const hookCommand = `node .claude/hooks/${hookFileName}`;
+    if (!/^[a-zA-Z0-9._-]+\.cjs$/.test(hookFileName)) {
+      continue;
+    }
+    const safeHookPath = JSON.stringify(path.posix.join('.claude', 'hooks', hookFileName));
+    const hookCommand = `node ${safeHookPath}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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}`;
// 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).
if (!/^[a-zA-Z0-9._-]+\.cjs$/.test(hookFileName)) {
continue;
}
const safeHookPath = JSON.stringify(path.posix.join('.claude', 'hooks', hookFileName));
const hookCommand = `node ${safeHookPath}`;
🤖 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, hookCommand currently interpolates hookFileName directly (const hookCommand
= `node .claude/hooks/${hookFileName}`), which allows filenames with spaces or
shell metacharacters to slip through; validate hookFileName against a strict
allowlist regex (e.g., /^[A-Za-z0-9._-]+\.cjs$/) in the code that constructs
hookCommand and reject or sanitize anything that doesn't match, then safely
quote or escape the path when composing hookCommand (e.g., wrap the relative
path in single quotes and escape contained single quotes) so the final string
cannot inject shell metacharacters before it is passed to the Claude Code hook
runner.

// 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