test-hook.sh reports success for a hook that allows what it was written to block
plugin-dev/skills/hook-development/scripts/test-hook.sh reports success whether the hook allows or denies, so it cannot detect a hook that permits exactly what it was written to block.
Root cause
Lines 245 to 252:
if [ $exit_code -eq 0 ] || [ $exit_code -eq 2 ]; then
echo "✅ Test completed successfully"
exit 0
else
echo "❌ Test failed"
exit 1
fi
Exit 0 is allow and exit 2 is deny, and both print success. The script also accepts no expected outcome anywhere: usage is test-hook.sh [options] <hook-script> <test-input.json>.
Reproduction
A PreToolUse hook whose stated job is to block writes under infra/, with a guard that never fires:
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
const ev = JSON.parse(readFileSync(0, 'utf8'));
const p = (ev.tool_input && ev.tool_input.file_path) || '';
if (process.env.NEVER_SET === '1') { // defect: guard never fires
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' } }));
}
process.exit(0);
Input: a PreToolUse event writing infra/main.tf.
$ bash test-hook.sh guard.mjs input.json
Exit Code: 0
✅ Hook approved/succeeded
✅ Test completed successfully
$ echo $?
0
The hook allowed the write it exists to block, and the tester passed it.
Expected
A hook tester should compare the observed decision against an expected one and fail on mismatch.
Suggested fix
Accept an expected outcome, for example a third argument or an --expect allow|deny|ask flag, and compare against the actual decision (exit 2, or exit 0 with hookSpecificOutput.permissionDecision). Without it the script verifies only that the hook ran, never that it decided correctly.
Environment: Claude Code 2.1.219, Windows 11, Git Bash, Node 24.14.1.