mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-01 14:43:28 +08:00
fix: extract inline SessionStart bootstrap to separate file (#1035)
Inline `node -e "..."` in hooks.json contained `!` characters (e.g. `!org.isDirectory()`) that bash history expansion in certain shell environments would misinterpret, producing syntax errors and the "SessionStart:startup hook error" banner in the Claude Code CLI header. Extract the bootstrap logic to `scripts/hooks/session-start-bootstrap.js` so the shell never sees the JS source. Behaviour is identical: the script reads stdin, resolves the ECC plugin root via CLAUDE_PLUGIN_ROOT or a set of well-known fallback paths, then delegates to run-with-flags.js. Update the test that asserted the old inline pattern to verify the new file-based approach instead. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
149
scripts/hooks/session-start-bootstrap.js
Normal file
149
scripts/hooks/session-start-bootstrap.js
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* session-start-bootstrap.js
|
||||
*
|
||||
* Bootstrap loader for the ECC SessionStart hook.
|
||||
*
|
||||
* Problem this solves: the previous approach embedded this logic as an inline
|
||||
* `node -e "..."` string inside hooks.json. Characters like `!` (used in
|
||||
* `!org.isDirectory()`) can trigger bash history expansion or other shell
|
||||
* interpretation issues depending on the environment, causing
|
||||
* "SessionStart:startup hook error" to appear in the Claude Code CLI header.
|
||||
*
|
||||
* By extracting to a standalone file, the shell never sees the JavaScript
|
||||
* source and the `!` characters are safe. Behaviour is otherwise identical.
|
||||
*
|
||||
* How it works:
|
||||
* 1. Reads the raw JSON event from stdin (passed by Claude Code).
|
||||
* 2. Resolves the ECC plugin root directory (via CLAUDE_PLUGIN_ROOT env var
|
||||
* or a set of well-known fallback paths).
|
||||
* 3. Delegates to `scripts/hooks/run-with-flags.js` with the `session:start`
|
||||
* event, which applies hook-profile gating and then runs session-start.js.
|
||||
* 4. Passes stdout/stderr through and forwards the child exit code.
|
||||
* 5. If the plugin root cannot be found, emits a warning and passes stdin
|
||||
* through unchanged so Claude Code can continue normally.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
// Read the raw JSON event from stdin
|
||||
const raw = fs.readFileSync(0, 'utf8');
|
||||
|
||||
// Path (relative to plugin root) to the hook runner
|
||||
const rel = path.join('scripts', 'hooks', 'run-with-flags.js');
|
||||
|
||||
/**
|
||||
* Returns true when `candidate` looks like a valid ECC plugin root, i.e. the
|
||||
* run-with-flags.js runner exists inside it.
|
||||
*
|
||||
* @param {unknown} candidate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasRunnerRoot(candidate) {
|
||||
const value = typeof candidate === 'string' ? candidate.trim() : '';
|
||||
return value.length > 0 && fs.existsSync(path.join(path.resolve(value), rel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the ECC plugin root using the following priority order:
|
||||
* 1. CLAUDE_PLUGIN_ROOT environment variable
|
||||
* 2. ~/.claude (direct install)
|
||||
* 3. Several well-known plugin sub-paths under ~/.claude/plugins/
|
||||
* 4. Versioned cache directories under ~/.claude/plugins/cache/everything-claude-code/
|
||||
* 5. Falls back to ~/.claude if nothing else matches
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolvePluginRoot() {
|
||||
const envRoot = process.env.CLAUDE_PLUGIN_ROOT || '';
|
||||
if (hasRunnerRoot(envRoot)) {
|
||||
return path.resolve(envRoot.trim());
|
||||
}
|
||||
|
||||
const home = require('os').homedir();
|
||||
const claudeDir = path.join(home, '.claude');
|
||||
|
||||
if (hasRunnerRoot(claudeDir)) {
|
||||
return claudeDir;
|
||||
}
|
||||
|
||||
const knownPaths = [
|
||||
path.join(claudeDir, 'plugins', 'everything-claude-code'),
|
||||
path.join(claudeDir, 'plugins', 'everything-claude-code@everything-claude-code'),
|
||||
path.join(claudeDir, 'plugins', 'marketplace', 'everything-claude-code'),
|
||||
];
|
||||
|
||||
for (const candidate of knownPaths) {
|
||||
if (hasRunnerRoot(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk versioned cache: ~/.claude/plugins/cache/everything-claude-code/<org>/<version>/
|
||||
try {
|
||||
const cacheBase = path.join(claudeDir, 'plugins', 'cache', 'everything-claude-code');
|
||||
for (const org of fs.readdirSync(cacheBase, { withFileTypes: true })) {
|
||||
if (!org.isDirectory()) continue;
|
||||
for (const version of fs.readdirSync(path.join(cacheBase, org.name), { withFileTypes: true })) {
|
||||
if (!version.isDirectory()) continue;
|
||||
const candidate = path.join(cacheBase, org.name, version.name);
|
||||
if (hasRunnerRoot(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// cache directory may not exist; that's fine
|
||||
}
|
||||
|
||||
return claudeDir;
|
||||
}
|
||||
|
||||
const root = resolvePluginRoot();
|
||||
const script = path.join(root, rel);
|
||||
|
||||
if (fs.existsSync(script)) {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[script, 'session:start', 'scripts/hooks/session-start.js', 'minimal,standard,strict'],
|
||||
{
|
||||
input: raw,
|
||||
encoding: 'utf8',
|
||||
env: process.env,
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
}
|
||||
);
|
||||
|
||||
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
|
||||
if (stdout) {
|
||||
process.stdout.write(stdout);
|
||||
} else {
|
||||
process.stdout.write(raw);
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
|
||||
if (result.error || result.status === null || result.signal) {
|
||||
const reason = result.error
|
||||
? result.error.message
|
||||
: result.signal
|
||||
? 'signal ' + result.signal
|
||||
: 'missing exit status';
|
||||
process.stderr.write('[SessionStart] ERROR: session-start hook failed: ' + reason + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(Number.isInteger(result.status) ? result.status : 0);
|
||||
}
|
||||
|
||||
process.stderr.write(
|
||||
'[SessionStart] WARNING: could not resolve ECC plugin root; skipping session-start hook\n'
|
||||
);
|
||||
process.stdout.write(raw);
|
||||
Reference in New Issue
Block a user