fix: surface warn-only PreToolUse hooks (#2084)

This commit is contained in:
Affaan Mustafa
2026-05-28 07:45:46 -04:00
committed by GitHub
parent 04c68e483a
commit 64cd1ba248
9 changed files with 143 additions and 35 deletions

View File

@@ -2,6 +2,10 @@
'use strict';
const { isHookEnabled } = require('../lib/hook-flags');
const {
buildPreToolUseAdditionalContext,
combineAdditionalContext,
} = require('./pretooluse-visible-output');
const { run: runBlockNoVerify } = require('./block-no-verify');
const { run: runAutoTmuxDev } = require('./auto-tmux-dev');
@@ -93,7 +97,9 @@ function normalizeHookResult(previousRaw, output) {
}
if (output && typeof output === 'object') {
const nextRaw = Object.prototype.hasOwnProperty.call(output, 'stdout')
const nextRaw = Object.prototype.hasOwnProperty.call(output, 'additionalContext')
? previousRaw
: Object.prototype.hasOwnProperty.call(output, 'stdout')
? String(output.stdout ?? '')
: !Number.isInteger(output.exitCode) || output.exitCode === 0
? previousRaw
@@ -102,6 +108,7 @@ function normalizeHookResult(previousRaw, output) {
return {
raw: nextRaw,
stderr: typeof output.stderr === 'string' ? output.stderr : '',
additionalContext: output.additionalContext,
exitCode: Number.isInteger(output.exitCode) ? output.exitCode : 0,
};
}
@@ -116,6 +123,7 @@ function normalizeHookResult(previousRaw, output) {
function runHooks(rawInput, hooks) {
let currentRaw = rawInput;
let stderr = '';
let additionalContext = '';
for (const hook of hooks) {
if (!isHookEnabled(hook.id, { profiles: hook.profiles })) {
@@ -128,15 +136,25 @@ function runHooks(rawInput, hooks) {
if (result.stderr) {
stderr += result.stderr.endsWith('\n') ? result.stderr : `${result.stderr}\n`;
}
if (result.additionalContext) {
additionalContext = combineAdditionalContext(additionalContext, result.additionalContext);
}
if (result.exitCode !== 0) {
return { output: currentRaw, stderr, exitCode: result.exitCode };
return { output: currentRaw, stderr, additionalContext, exitCode: result.exitCode };
}
} catch (error) {
stderr += `[Hook] ${hook.id} failed: ${error.message}\n`;
}
}
return { output: currentRaw, stderr, exitCode: 0 };
return {
output: additionalContext
? buildPreToolUseAdditionalContext(additionalContext)
: currentRaw,
stderr,
additionalContext,
exitCode: 0,
};
}
function runPreBash(rawInput) {

View File

@@ -14,6 +14,7 @@
'use strict';
const path = require('path');
const { buildPreToolUseAdditionalContext } = require('./pretooluse-visible-output');
const MAX_STDIN = 1024 * 1024;
let data = '';
@@ -58,10 +59,11 @@ function run(inputOrRaw, _options = {}) {
if (filePath && isSuspiciousDocPath(filePath)) {
return {
exitCode: 0,
stderr:
'[Hook] WARNING: Ad-hoc documentation filename detected\n' +
`[Hook] File: ${filePath}\n` +
additionalContext: [
'[Hook] WARNING: Ad-hoc documentation filename detected',
`[Hook] File: ${filePath}`,
'[Hook] Consider using a structured path (e.g. docs/, .claude/, skills/, .github/, benchmarks/, templates/)',
],
};
}
@@ -86,5 +88,9 @@ process.stdin.on('end', () => {
process.stderr.write(result.stderr + '\n');
}
process.stdout.write(data);
if (Object.prototype.hasOwnProperty.call(result, 'additionalContext')) {
process.stdout.write(buildPreToolUseAdditionalContext(result.additionalContext));
} else {
process.stdout.write(data);
}
});

View File

@@ -2,6 +2,7 @@
'use strict';
const MAX_STDIN = 1024 * 1024;
const { buildPreToolUseAdditionalContext } = require('./pretooluse-visible-output');
let raw = '';
function run(rawInput) {
@@ -10,11 +11,10 @@ function run(rawInput) {
const cmd = String(input.tool_input?.command || '');
if (/\bgit\s+push\b/.test(cmd)) {
return {
stdout: typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput),
stderr: [
additionalContext: [
'[Hook] Review changes before push...',
'[Hook] Continuing with push (remove this hook to add interactive review)',
].join('\n'),
],
exitCode: 0,
};
}
@@ -40,7 +40,11 @@ if (require.main === module) {
if (result.stderr) {
process.stderr.write(`${result.stderr}\n`);
}
process.stdout.write(String(result.stdout || ''));
if (Object.prototype.hasOwnProperty.call(result, 'additionalContext')) {
process.stdout.write(buildPreToolUseAdditionalContext(result.additionalContext));
} else {
process.stdout.write(String(result.stdout || ''));
}
process.exitCode = Number.isInteger(result.exitCode) ? result.exitCode : 0;
return;
}

View File

@@ -2,6 +2,7 @@
'use strict';
const MAX_STDIN = 1024 * 1024;
const { buildPreToolUseAdditionalContext } = require('./pretooluse-visible-output');
let raw = '';
function run(rawInput) {
@@ -15,11 +16,10 @@ function run(rawInput) {
/(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\b|docker\b|pytest|vitest|playwright)/.test(cmd)
) {
return {
stdout: typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput),
stderr: [
additionalContext: [
'[Hook] Consider running in tmux for session persistence',
'[Hook] tmux new -s dev | tmux attach -t dev',
].join('\n'),
],
exitCode: 0,
};
}
@@ -45,7 +45,11 @@ if (require.main === module) {
if (result.stderr) {
process.stderr.write(`${result.stderr}\n`);
}
process.stdout.write(String(result.stdout || ''));
if (Object.prototype.hasOwnProperty.call(result, 'additionalContext')) {
process.stdout.write(buildPreToolUseAdditionalContext(result.additionalContext));
} else {
process.stdout.write(String(result.stdout || ''));
}
process.exitCode = Number.isInteger(result.exitCode) ? result.exitCode : 0;
return;
}

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env node
'use strict';
function normalizeAdditionalContext(value) {
if (Array.isArray(value)) {
return value
.map(item => String(item || '').trim())
.filter(Boolean)
.join('\n');
}
return String(value || '').trim();
}
function combineAdditionalContext(current, next) {
const currentText = normalizeAdditionalContext(current);
const nextText = normalizeAdditionalContext(next);
if (!currentText) return nextText;
if (!nextText) return currentText;
return `${currentText}\n${nextText}`;
}
function buildPreToolUseAdditionalContext(value) {
const additionalContext = normalizeAdditionalContext(value);
if (!additionalContext) return '';
return JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
additionalContext,
},
});
}
module.exports = {
buildPreToolUseAdditionalContext,
combineAdditionalContext,
normalizeAdditionalContext,
};

View File

@@ -12,6 +12,7 @@ const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { isHookEnabled } = require('../lib/hook-flags');
const { buildPreToolUseAdditionalContext } = require('./pretooluse-visible-output');
const MAX_STDIN = 1024 * 1024;
@@ -53,7 +54,9 @@ function emitHookResult(raw, output) {
if (output && typeof output === 'object') {
writeStderr(output.stderr);
if (Object.prototype.hasOwnProperty.call(output, 'stdout')) {
if (Object.prototype.hasOwnProperty.call(output, 'additionalContext')) {
process.stdout.write(buildPreToolUseAdditionalContext(output.additionalContext));
} else if (Object.prototype.hasOwnProperty.call(output, 'stdout')) {
process.stdout.write(String(output.stdout ?? ''));
} else if (!Number.isInteger(output.exitCode) || output.exitCode === 0) {
process.stdout.write(raw);