mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-16 06:53:27 +08:00
fix: consolidate bash hooks to avoid fork storms
This commit is contained in:
@@ -30,19 +30,11 @@ const { spawnSync } = require('child_process');
|
||||
|
||||
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||
let data = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
function run(rawInput) {
|
||||
let input;
|
||||
try {
|
||||
input = JSON.parse(data);
|
||||
input = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
|
||||
const cmd = input.tool_input?.command || '';
|
||||
|
||||
// Detect dev server commands: npm run dev, pnpm dev, yarn dev, bun run dev
|
||||
@@ -79,10 +71,27 @@ process.stdin.on('end', () => {
|
||||
// else: tmux not found, pass through original command unchanged
|
||||
}
|
||||
}
|
||||
process.stdout.write(JSON.stringify(input));
|
||||
|
||||
return JSON.stringify(input);
|
||||
} catch {
|
||||
// Invalid input — pass through original data unchanged
|
||||
process.stdout.write(data);
|
||||
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
process.stdout.write(run(data));
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
|
||||
177
scripts/hooks/bash-hook-dispatcher.js
Normal file
177
scripts/hooks/bash-hook-dispatcher.js
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { isHookEnabled } = require('../lib/hook-flags');
|
||||
|
||||
const { run: runBlockNoVerify } = require('./block-no-verify');
|
||||
const { run: runAutoTmuxDev } = require('./auto-tmux-dev');
|
||||
const { run: runTmuxReminder } = require('./pre-bash-tmux-reminder');
|
||||
const { run: runGitPushReminder } = require('./pre-bash-git-push-reminder');
|
||||
const { run: runCommitQuality } = require('./pre-bash-commit-quality');
|
||||
const { run: runGateGuard } = require('./gateguard-fact-force');
|
||||
const { run: runCommandLog } = require('./post-bash-command-log');
|
||||
const { run: runPrCreated } = require('./post-bash-pr-created');
|
||||
const { run: runBuildComplete } = require('./post-bash-build-complete');
|
||||
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
|
||||
const PRE_BASH_HOOKS = [
|
||||
{
|
||||
id: 'pre:bash:block-no-verify',
|
||||
profiles: 'minimal,standard,strict',
|
||||
run: rawInput => runBlockNoVerify(rawInput),
|
||||
},
|
||||
{
|
||||
id: 'pre:bash:auto-tmux-dev',
|
||||
run: rawInput => runAutoTmuxDev(rawInput),
|
||||
},
|
||||
{
|
||||
id: 'pre:bash:tmux-reminder',
|
||||
profiles: 'strict',
|
||||
run: rawInput => runTmuxReminder(rawInput),
|
||||
},
|
||||
{
|
||||
id: 'pre:bash:git-push-reminder',
|
||||
profiles: 'strict',
|
||||
run: rawInput => runGitPushReminder(rawInput),
|
||||
},
|
||||
{
|
||||
id: 'pre:bash:commit-quality',
|
||||
profiles: 'strict',
|
||||
run: rawInput => runCommitQuality(rawInput),
|
||||
},
|
||||
{
|
||||
id: 'pre:bash:gateguard-fact-force',
|
||||
profiles: 'standard,strict',
|
||||
run: rawInput => runGateGuard(rawInput),
|
||||
},
|
||||
];
|
||||
|
||||
const POST_BASH_HOOKS = [
|
||||
{
|
||||
id: 'post:bash:command-log-audit',
|
||||
run: rawInput => runCommandLog(rawInput, 'audit'),
|
||||
},
|
||||
{
|
||||
id: 'post:bash:command-log-cost',
|
||||
run: rawInput => runCommandLog(rawInput, 'cost'),
|
||||
},
|
||||
{
|
||||
id: 'post:bash:pr-created',
|
||||
profiles: 'standard,strict',
|
||||
run: rawInput => runPrCreated(rawInput),
|
||||
},
|
||||
{
|
||||
id: 'post:bash:build-complete',
|
||||
profiles: 'standard,strict',
|
||||
run: rawInput => runBuildComplete(rawInput),
|
||||
},
|
||||
];
|
||||
|
||||
function readStdinRaw() {
|
||||
return new Promise(resolve => {
|
||||
let raw = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
process.stdin.on('end', () => resolve(raw));
|
||||
process.stdin.on('error', () => resolve(raw));
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeHookResult(previousRaw, output) {
|
||||
if (typeof output === 'string' || Buffer.isBuffer(output)) {
|
||||
return {
|
||||
raw: String(output),
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (output && typeof output === 'object') {
|
||||
const nextRaw = Object.prototype.hasOwnProperty.call(output, 'stdout')
|
||||
? String(output.stdout ?? '')
|
||||
: !Number.isInteger(output.exitCode) || output.exitCode === 0
|
||||
? previousRaw
|
||||
: '';
|
||||
|
||||
return {
|
||||
raw: nextRaw,
|
||||
stderr: typeof output.stderr === 'string' ? output.stderr : '',
|
||||
exitCode: Number.isInteger(output.exitCode) ? output.exitCode : 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
raw: previousRaw,
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function runHooks(rawInput, hooks) {
|
||||
let currentRaw = rawInput;
|
||||
let stderr = '';
|
||||
|
||||
for (const hook of hooks) {
|
||||
if (!isHookEnabled(hook.id, { profiles: hook.profiles })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = normalizeHookResult(currentRaw, hook.run(currentRaw));
|
||||
currentRaw = result.raw;
|
||||
if (result.stderr) {
|
||||
stderr += result.stderr.endsWith('\n') ? result.stderr : `${result.stderr}\n`;
|
||||
}
|
||||
if (result.exitCode !== 0) {
|
||||
return { output: currentRaw, stderr, exitCode: result.exitCode };
|
||||
}
|
||||
} catch (error) {
|
||||
stderr += `[Hook] ${hook.id} failed: ${error.message}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return { output: currentRaw, stderr, exitCode: 0 };
|
||||
}
|
||||
|
||||
function runPreBash(rawInput) {
|
||||
return runHooks(rawInput, PRE_BASH_HOOKS);
|
||||
}
|
||||
|
||||
function runPostBash(rawInput) {
|
||||
return runHooks(rawInput, POST_BASH_HOOKS);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const mode = process.argv[2];
|
||||
const raw = await readStdinRaw();
|
||||
|
||||
const result = mode === 'post'
|
||||
? runPostBash(raw)
|
||||
: runPreBash(raw);
|
||||
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
process.stdout.write(result.output);
|
||||
process.exit(result.exitCode);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
process.stderr.write(`[Hook] bash-hook-dispatcher failed: ${error.message}\n`);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PRE_BASH_HOOKS,
|
||||
POST_BASH_HOOKS,
|
||||
runPreBash,
|
||||
runPostBash,
|
||||
};
|
||||
@@ -4,24 +4,45 @@
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
let raw = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
function run(rawInput) {
|
||||
try {
|
||||
const input = JSON.parse(raw);
|
||||
const input = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
|
||||
const cmd = String(input.tool_input?.command || '');
|
||||
if (/(npm run build|pnpm build|yarn build)/.test(cmd)) {
|
||||
console.error('[Hook] Build completed - async analysis running in background');
|
||||
return {
|
||||
stdout: typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput),
|
||||
stderr: '[Hook] Build completed - async analysis running in background',
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors and pass through
|
||||
}
|
||||
|
||||
process.stdout.write(raw);
|
||||
});
|
||||
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const result = run(raw);
|
||||
if (result && typeof result === 'object') {
|
||||
if (result.stderr) {
|
||||
process.stderr.write(`${result.stderr}\n`);
|
||||
}
|
||||
process.stdout.write(String(result.stdout || ''));
|
||||
process.exit(Number.isInteger(result.exitCode) ? result.exitCode : 0);
|
||||
}
|
||||
|
||||
process.stdout.write(String(result));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
|
||||
@@ -38,8 +38,24 @@ function appendLine(filePath, line) {
|
||||
fs.appendFileSync(filePath, `${line}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function run(rawInput, mode = 'audit') {
|
||||
const config = MODE_CONFIG[mode];
|
||||
|
||||
try {
|
||||
if (config) {
|
||||
const input = String(rawInput || '').trim() ? JSON.parse(String(rawInput)) : {};
|
||||
const command = sanitizeCommand(input.tool_input?.command || '?');
|
||||
appendLine(path.join(os.homedir(), '.claude', config.fileName), config.format(command));
|
||||
}
|
||||
} catch {
|
||||
// Logging must never block the calling hook.
|
||||
}
|
||||
|
||||
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const config = MODE_CONFIG[process.argv[2]];
|
||||
const mode = process.argv[2];
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
@@ -50,17 +66,7 @@ function main() {
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
if (config) {
|
||||
const input = raw.trim() ? JSON.parse(raw) : {};
|
||||
const command = sanitizeCommand(input.tool_input?.command || '?');
|
||||
appendLine(path.join(os.homedir(), '.claude', config.fileName), config.format(command));
|
||||
}
|
||||
} catch {
|
||||
// Logging must never block the calling hook.
|
||||
}
|
||||
|
||||
process.stdout.write(raw);
|
||||
process.stdout.write(run(raw, mode));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,5 +75,6 @@ if (require.main === module) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
run,
|
||||
sanitizeCommand,
|
||||
};
|
||||
|
||||
24
scripts/hooks/post-bash-dispatcher.js
Normal file
24
scripts/hooks/post-bash-dispatcher.js
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { runPostBash } = require('./bash-hook-dispatcher');
|
||||
|
||||
let raw = '';
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const result = runPostBash(raw);
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
process.stdout.write(result.output);
|
||||
process.exit(result.exitCode);
|
||||
});
|
||||
@@ -4,17 +4,9 @@
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
let raw = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
function run(rawInput) {
|
||||
try {
|
||||
const input = JSON.parse(raw);
|
||||
const input = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
|
||||
const cmd = String(input.tool_input?.command || '');
|
||||
|
||||
if (/\bgh\s+pr\s+create\b/.test(cmd)) {
|
||||
@@ -24,13 +16,44 @@ process.stdin.on('end', () => {
|
||||
const prUrl = match[0];
|
||||
const repo = prUrl.replace(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/, '$1');
|
||||
const prNum = prUrl.replace(/.+\/pull\/(\d+)/, '$1');
|
||||
console.error(`[Hook] PR created: ${prUrl}`);
|
||||
console.error(`[Hook] To review: gh pr review ${prNum} --repo ${repo}`);
|
||||
return {
|
||||
stdout: typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput),
|
||||
stderr: [
|
||||
`[Hook] PR created: ${prUrl}`,
|
||||
`[Hook] To review: gh pr review ${prNum} --repo ${repo}`,
|
||||
].join('\n'),
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors and pass through
|
||||
}
|
||||
|
||||
process.stdout.write(raw);
|
||||
});
|
||||
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const result = run(raw);
|
||||
if (result && typeof result === 'object') {
|
||||
if (result.stderr) {
|
||||
process.stderr.write(`${result.stderr}\n`);
|
||||
}
|
||||
process.stdout.write(String(result.stdout || ''));
|
||||
process.exit(Number.isInteger(result.exitCode) ? result.exitCode : 0);
|
||||
}
|
||||
|
||||
process.stdout.write(String(result));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
|
||||
24
scripts/hooks/pre-bash-dispatcher.js
Normal file
24
scripts/hooks/pre-bash-dispatcher.js
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { runPreBash } = require('./bash-hook-dispatcher');
|
||||
|
||||
let raw = '';
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const result = runPreBash(raw);
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
process.stdout.write(result.output);
|
||||
process.exit(result.exitCode);
|
||||
});
|
||||
@@ -4,25 +4,48 @@
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
let raw = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
function run(rawInput) {
|
||||
try {
|
||||
const input = JSON.parse(raw);
|
||||
const input = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
|
||||
const cmd = String(input.tool_input?.command || '');
|
||||
if (/\bgit\s+push\b/.test(cmd)) {
|
||||
console.error('[Hook] Review changes before push...');
|
||||
console.error('[Hook] Continuing with push (remove this hook to add interactive review)');
|
||||
return {
|
||||
stdout: typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput),
|
||||
stderr: [
|
||||
'[Hook] Review changes before push...',
|
||||
'[Hook] Continuing with push (remove this hook to add interactive review)',
|
||||
].join('\n'),
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors and pass through
|
||||
}
|
||||
|
||||
process.stdout.write(raw);
|
||||
});
|
||||
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const result = run(raw);
|
||||
if (result && typeof result === 'object') {
|
||||
if (result.stderr) {
|
||||
process.stderr.write(`${result.stderr}\n`);
|
||||
}
|
||||
process.stdout.write(String(result.stdout || ''));
|
||||
process.exit(Number.isInteger(result.exitCode) ? result.exitCode : 0);
|
||||
}
|
||||
|
||||
process.stdout.write(String(result));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
|
||||
@@ -4,17 +4,9 @@
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
let raw = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
function run(rawInput) {
|
||||
try {
|
||||
const input = JSON.parse(raw);
|
||||
const input = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
|
||||
const cmd = String(input.tool_input?.command || '');
|
||||
|
||||
if (
|
||||
@@ -22,12 +14,43 @@ process.stdin.on('end', () => {
|
||||
!process.env.TMUX &&
|
||||
/(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\b|docker\b|pytest|vitest|playwright)/.test(cmd)
|
||||
) {
|
||||
console.error('[Hook] Consider running in tmux for session persistence');
|
||||
console.error('[Hook] tmux new -s dev | tmux attach -t dev');
|
||||
return {
|
||||
stdout: typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput),
|
||||
stderr: [
|
||||
'[Hook] Consider running in tmux for session persistence',
|
||||
'[Hook] tmux new -s dev | tmux attach -t dev',
|
||||
].join('\n'),
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors and pass through
|
||||
}
|
||||
|
||||
process.stdout.write(raw);
|
||||
});
|
||||
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const result = run(raw);
|
||||
if (result && typeof result === 'object') {
|
||||
if (result.stderr) {
|
||||
process.stderr.write(`${result.stderr}\n`);
|
||||
}
|
||||
process.stdout.write(String(result.stdout || ''));
|
||||
process.exit(Number.isInteger(result.exitCode) ? result.exitCode : 0);
|
||||
}
|
||||
|
||||
process.stdout.write(String(result));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
|
||||
Reference in New Issue
Block a user