mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 22:43:28 +08:00
fix: bootstrap plugin-installed hook commands safely
This commit is contained in:
@@ -110,24 +110,70 @@ function runHookCommand(command, input = {}, env = {}, timeoutMs = 10000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const mergedEnv = { ...process.env, CLAUDE_PLUGIN_ROOT: REPO_ROOT, ...env };
|
||||
if (Array.isArray(command)) {
|
||||
const [program, ...args] = command;
|
||||
const proc = spawn(program, args, { env: mergedEnv, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timer;
|
||||
|
||||
proc.stdout.on('data', data => stdout += data);
|
||||
proc.stderr.on('data', data => stderr += data);
|
||||
|
||||
proc.stdin.on('error', (err) => {
|
||||
if (err.code !== 'EPIPE' && err.code !== 'EOF') {
|
||||
if (timer) clearTimeout(timer);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
if (input && Object.keys(input).length > 0) {
|
||||
proc.stdin.write(JSON.stringify(input));
|
||||
}
|
||||
proc.stdin.end();
|
||||
|
||||
timer = setTimeout(() => {
|
||||
proc.kill(isWindows ? undefined : 'SIGKILL');
|
||||
reject(new Error(`Hook command timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
proc.on('close', code => {
|
||||
clearTimeout(timer);
|
||||
resolve({ code, stdout, stderr });
|
||||
});
|
||||
|
||||
proc.on('error', err => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedCommand = command.replace(
|
||||
/\$\{([A-Z_][A-Z0-9_]*)\}/g,
|
||||
(_, name) => String(mergedEnv[name] || '')
|
||||
);
|
||||
|
||||
const nodeMatch = resolvedCommand.match(/^node\s+"([^"]+)"\s*(.*)$/);
|
||||
const useDirectNodeSpawn = Boolean(nodeMatch);
|
||||
const inlineNodeMatch = resolvedCommand.match(/^node -e "((?:[^"\\]|\\.)*)"(?:\s+(.*))?$/s);
|
||||
const fileNodeMatch = resolvedCommand.match(/^node\s+"([^"]+)"\s*(.*)$/);
|
||||
const useDirectNodeSpawn = Boolean(inlineNodeMatch || fileNodeMatch);
|
||||
const shell = isWindows ? 'cmd' : 'bash';
|
||||
const shellArgs = isWindows ? ['/d', '/s', '/c', resolvedCommand] : ['-lc', resolvedCommand];
|
||||
const nodeArgs = nodeMatch
|
||||
? [
|
||||
nodeMatch[1],
|
||||
...Array.from(
|
||||
nodeMatch[2].matchAll(/"([^"]*)"|(\S+)/g),
|
||||
m => m[1] !== undefined ? m[1] : m[2]
|
||||
)
|
||||
]
|
||||
: [];
|
||||
const splitArgs = value => Array.from(
|
||||
String(value || '').matchAll(/"([^"]*)"|(\S+)/g),
|
||||
m => m[1] !== undefined ? m[1] : m[2]
|
||||
);
|
||||
const unescapeInlineJs = value => value
|
||||
.replace(/\\\\/g, '\\')
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\t/g, '\t');
|
||||
const nodeArgs = inlineNodeMatch
|
||||
? ['-e', unescapeInlineJs(inlineNodeMatch[1]), ...splitArgs(inlineNodeMatch[2])]
|
||||
: fileNodeMatch
|
||||
? [fileNodeMatch[1], ...splitArgs(fileNodeMatch[2])]
|
||||
: [];
|
||||
|
||||
const proc = useDirectNodeSpawn
|
||||
? spawn('node', nodeArgs, { env: mergedEnv, stdio: ['pipe', 'pipe', 'pipe'] })
|
||||
@@ -899,16 +945,22 @@ async function runTests() {
|
||||
assert.ok(asyncHook.hooks[0].timeout > 0, 'Timeout should be positive');
|
||||
|
||||
const command = asyncHook.hooks[0].command;
|
||||
const isNodeInline = command.startsWith('node -e');
|
||||
const isNodeScript = command.startsWith('node "');
|
||||
const commandText = Array.isArray(command) ? command.join(' ') : command;
|
||||
const isNodeInline =
|
||||
(Array.isArray(command) && command[0] === 'node' && command[1] === '-e') ||
|
||||
commandText.startsWith('node -e');
|
||||
const isNodeScript =
|
||||
(Array.isArray(command) && command[0] === 'node' && typeof command[1] === 'string' && command[1].endsWith('.js')) ||
|
||||
commandText.startsWith('node "');
|
||||
const isShellWrapper =
|
||||
command.startsWith('bash "') ||
|
||||
command.startsWith('sh "') ||
|
||||
command.startsWith('bash -lc ') ||
|
||||
command.startsWith('sh -c ');
|
||||
(Array.isArray(command) && (command[0] === 'bash' || command[0] === 'sh')) ||
|
||||
commandText.startsWith('bash "') ||
|
||||
commandText.startsWith('sh "') ||
|
||||
commandText.startsWith('bash -lc ') ||
|
||||
commandText.startsWith('sh -c ');
|
||||
assert.ok(
|
||||
isNodeInline || isNodeScript || isShellWrapper,
|
||||
`Async hook command should be runnable (node -e, node script, or shell wrapper), got: ${command.substring(0, 80)}`
|
||||
`Async hook command should be runnable (node -e, node script, or shell wrapper), got: ${commandText.substring(0, 80)}`
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -920,19 +972,28 @@ async function runTests() {
|
||||
for (const hook of hookDef.hooks) {
|
||||
assert.ok(hook.command, `Hook in ${hookType} should have command field`);
|
||||
|
||||
const isInline = hook.command.startsWith('node -e');
|
||||
const isFilePath = hook.command.startsWith('node "');
|
||||
const isNpx = hook.command.startsWith('npx ');
|
||||
const command = hook.command;
|
||||
const commandText = Array.isArray(command) ? command.join(' ') : command;
|
||||
const isInline =
|
||||
(Array.isArray(command) && command[0] === 'node' && command[1] === '-e') ||
|
||||
commandText.startsWith('node -e');
|
||||
const isFilePath =
|
||||
(Array.isArray(command) && command[0] === 'node' && typeof command[1] === 'string' && command[1].endsWith('.js')) ||
|
||||
commandText.startsWith('node "');
|
||||
const isNpx = (Array.isArray(command) && command[0] === 'npx') || commandText.startsWith('npx ');
|
||||
const isShellWrapper =
|
||||
hook.command.startsWith('bash "') ||
|
||||
hook.command.startsWith('sh "') ||
|
||||
hook.command.startsWith('bash -lc ') ||
|
||||
hook.command.startsWith('sh -c ');
|
||||
const isShellScriptPath = hook.command.endsWith('.sh');
|
||||
(Array.isArray(command) && (command[0] === 'bash' || command[0] === 'sh')) ||
|
||||
commandText.startsWith('bash "') ||
|
||||
commandText.startsWith('sh "') ||
|
||||
commandText.startsWith('bash -lc ') ||
|
||||
commandText.startsWith('sh -c ');
|
||||
const isShellScriptPath =
|
||||
(Array.isArray(command) && typeof command[0] === 'string' && command[0].endsWith('.sh')) ||
|
||||
commandText.endsWith('.sh');
|
||||
|
||||
assert.ok(
|
||||
isInline || isFilePath || isNpx || isShellWrapper || isShellScriptPath,
|
||||
`Hook command in ${hookType} should be node -e, node script, npx, or shell wrapper/script, got: ${hook.command.substring(0, 80)}`
|
||||
`Hook command in ${hookType} should be node -e, node script, npx, or shell wrapper/script, got: ${commandText.substring(0, 80)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user