mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 22:43:28 +08:00
Compare commits
1 Commits
main
...
fix/bash-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fabf4d2cf |
172
hooks/hooks.json
172
hooks/hooks.json
@@ -12,92 +12,12 @@
|
|||||||
"-e",
|
"-e",
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
"node",
|
"node",
|
||||||
"scripts/hooks/run-with-flags.js",
|
"scripts/hooks/pre-bash-dispatcher.js"
|
||||||
"pre:bash:block-no-verify",
|
|
||||||
"scripts/hooks/block-no-verify.js",
|
|
||||||
"minimal,standard,strict"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Block git hook-bypass flag to protect pre-commit, commit-msg, and pre-push hooks from being skipped",
|
"description": "Consolidated Bash preflight dispatcher for quality, tmux, push, and GateGuard checks",
|
||||||
"id": "pre:bash:block-no-verify"
|
"id": "pre:bash:dispatcher"
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": [
|
|
||||||
"node",
|
|
||||||
"-e",
|
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
|
||||||
"node",
|
|
||||||
"scripts/hooks/auto-tmux-dev.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Auto-start dev servers in tmux with directory-based session names",
|
|
||||||
"id": "pre:bash:auto-tmux-dev"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": [
|
|
||||||
"node",
|
|
||||||
"-e",
|
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
|
||||||
"node",
|
|
||||||
"scripts/hooks/run-with-flags.js",
|
|
||||||
"pre:bash:tmux-reminder",
|
|
||||||
"scripts/hooks/pre-bash-tmux-reminder.js",
|
|
||||||
"strict"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Reminder to use tmux for long-running commands",
|
|
||||||
"id": "pre:bash:tmux-reminder"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": [
|
|
||||||
"node",
|
|
||||||
"-e",
|
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
|
||||||
"node",
|
|
||||||
"scripts/hooks/run-with-flags.js",
|
|
||||||
"pre:bash:git-push-reminder",
|
|
||||||
"scripts/hooks/pre-bash-git-push-reminder.js",
|
|
||||||
"strict"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Reminder before git push to review changes",
|
|
||||||
"id": "pre:bash:git-push-reminder"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": [
|
|
||||||
"node",
|
|
||||||
"-e",
|
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
|
||||||
"node",
|
|
||||||
"scripts/hooks/run-with-flags.js",
|
|
||||||
"pre:bash:commit-quality",
|
|
||||||
"scripts/hooks/pre-bash-commit-quality.js",
|
|
||||||
"strict"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Pre-commit quality check: lint staged files, validate commit message format, detect console.log/debugger/secrets before committing",
|
|
||||||
"id": "pre:bash:commit-quality"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matcher": "Write",
|
"matcher": "Write",
|
||||||
@@ -243,27 +163,6 @@
|
|||||||
],
|
],
|
||||||
"description": "Fact-forcing gate: block first Edit/Write/MultiEdit per file and demand investigation (importers, data schemas, user instruction) before allowing",
|
"description": "Fact-forcing gate: block first Edit/Write/MultiEdit per file and demand investigation (importers, data schemas, user instruction) before allowing",
|
||||||
"id": "pre:edit-write:gateguard-fact-force"
|
"id": "pre:edit-write:gateguard-fact-force"
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": [
|
|
||||||
"node",
|
|
||||||
"-e",
|
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
|
||||||
"node",
|
|
||||||
"scripts/hooks/run-with-flags.js",
|
|
||||||
"pre:bash:gateguard-fact-force",
|
|
||||||
"scripts/hooks/gateguard-fact-force.js",
|
|
||||||
"standard,strict"
|
|
||||||
],
|
|
||||||
"timeout": 5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Fact-forcing gate: block destructive Bash commands and demand rollback plan; quote user instruction on first Bash per session",
|
|
||||||
"id": "pre:bash:gateguard-fact-force"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"PreCompact": [
|
"PreCompact": [
|
||||||
@@ -318,73 +217,14 @@
|
|||||||
"-e",
|
"-e",
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
"node",
|
"node",
|
||||||
"scripts/hooks/post-bash-command-log.js",
|
"scripts/hooks/post-bash-dispatcher.js"
|
||||||
"audit"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Audit log all bash commands to ~/.claude/bash-commands.log",
|
|
||||||
"id": "post:bash:command-log-audit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": [
|
|
||||||
"node",
|
|
||||||
"-e",
|
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
|
||||||
"node",
|
|
||||||
"scripts/hooks/post-bash-command-log.js",
|
|
||||||
"cost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Cost tracker - log bash tool usage with timestamps",
|
|
||||||
"id": "post:bash:command-log-cost"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": [
|
|
||||||
"node",
|
|
||||||
"-e",
|
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
|
||||||
"node",
|
|
||||||
"scripts/hooks/run-with-flags.js",
|
|
||||||
"post:bash:pr-created",
|
|
||||||
"scripts/hooks/post-bash-pr-created.js",
|
|
||||||
"standard,strict"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Log PR URL and provide review command after PR creation",
|
|
||||||
"id": "post:bash:pr-created"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": [
|
|
||||||
"node",
|
|
||||||
"-e",
|
|
||||||
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
|
||||||
"node",
|
|
||||||
"scripts/hooks/run-with-flags.js",
|
|
||||||
"post:bash:build-complete",
|
|
||||||
"scripts/hooks/post-bash-build-complete.js",
|
|
||||||
"standard,strict"
|
|
||||||
],
|
],
|
||||||
"async": true,
|
"async": true,
|
||||||
"timeout": 30
|
"timeout": 30
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Example: async hook for build analysis (runs in background without blocking)",
|
"description": "Consolidated Bash postflight dispatcher for logging, PR, and build notifications",
|
||||||
"id": "post:bash:build-complete"
|
"id": "post:bash:dispatcher"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matcher": "Edit|Write|MultiEdit",
|
"matcher": "Edit|Write|MultiEdit",
|
||||||
|
|||||||
@@ -30,19 +30,10 @@ const { spawnSync } = require('child_process');
|
|||||||
|
|
||||||
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||||
let data = '';
|
let data = '';
|
||||||
process.stdin.setEncoding('utf8');
|
|
||||||
|
|
||||||
process.stdin.on('data', chunk => {
|
function run(rawInput) {
|
||||||
if (data.length < MAX_STDIN) {
|
|
||||||
const remaining = MAX_STDIN - data.length;
|
|
||||||
data += chunk.substring(0, remaining);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
|
||||||
let input;
|
|
||||||
try {
|
try {
|
||||||
input = JSON.parse(data);
|
const input = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
|
||||||
const cmd = input.tool_input?.command || '';
|
const cmd = input.tool_input?.command || '';
|
||||||
|
|
||||||
// Detect dev server commands: npm run dev, pnpm dev, yarn dev, bun run dev
|
// Detect dev server commands: npm run dev, pnpm dev, yarn dev, bun run dev
|
||||||
@@ -60,7 +51,13 @@ process.stdin.on('end', () => {
|
|||||||
// Windows: open in a new cmd window (non-blocking)
|
// Windows: open in a new cmd window (non-blocking)
|
||||||
// Escape double quotes in cmd for cmd /k syntax
|
// Escape double quotes in cmd for cmd /k syntax
|
||||||
const escapedCmd = cmd.replace(/"/g, '""');
|
const escapedCmd = cmd.replace(/"/g, '""');
|
||||||
input.tool_input.command = `start "DevServer-${sessionName}" cmd /k "${escapedCmd}"`;
|
return JSON.stringify({
|
||||||
|
...input,
|
||||||
|
tool_input: {
|
||||||
|
...input.tool_input,
|
||||||
|
command: `start "DevServer-${sessionName}" cmd /k "${escapedCmd}"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Unix (macOS/Linux): Check tmux is available before transforming
|
// Unix (macOS/Linux): Check tmux is available before transforming
|
||||||
const tmuxCheck = spawnSync('which', ['tmux'], { encoding: 'utf8' });
|
const tmuxCheck = spawnSync('which', ['tmux'], { encoding: 'utf8' });
|
||||||
@@ -73,16 +70,38 @@ process.stdin.on('end', () => {
|
|||||||
// 2. Create new detached session with the dev command
|
// 2. Create new detached session with the dev command
|
||||||
// 3. Echo confirmation message with instructions for viewing logs
|
// 3. Echo confirmation message with instructions for viewing logs
|
||||||
const transformedCmd = `SESSION="${sessionName}"; tmux kill-session -t "$SESSION" 2>/dev/null || true; tmux new-session -d -s "$SESSION" '${escapedCmd}' && echo "[Hook] Dev server started in tmux session '${sessionName}'. View logs: tmux capture-pane -t ${sessionName} -p -S -100"`;
|
const transformedCmd = `SESSION="${sessionName}"; tmux kill-session -t "$SESSION" 2>/dev/null || true; tmux new-session -d -s "$SESSION" '${escapedCmd}' && echo "[Hook] Dev server started in tmux session '${sessionName}'. View logs: tmux capture-pane -t ${sessionName} -p -S -100"`;
|
||||||
|
return JSON.stringify({
|
||||||
input.tool_input.command = transformedCmd;
|
...input,
|
||||||
|
tool_input: {
|
||||||
|
...input.tool_input,
|
||||||
|
command: transformedCmd,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// else: tmux not found, pass through original command unchanged
|
// else: tmux not found, pass through original command unchanged
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
process.stdout.write(JSON.stringify(input));
|
|
||||||
|
return JSON.stringify(input);
|
||||||
} catch {
|
} catch {
|
||||||
// Invalid input — pass through original data unchanged
|
// Invalid input — pass through original data unchanged
|
||||||
process.stdout.write(data);
|
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
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,6 +4,25 @@
|
|||||||
const MAX_STDIN = 1024 * 1024;
|
const MAX_STDIN = 1024 * 1024;
|
||||||
let raw = '';
|
let raw = '';
|
||||||
|
|
||||||
|
function run(rawInput) {
|
||||||
|
try {
|
||||||
|
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)) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
process.stdin.setEncoding('utf8');
|
process.stdin.setEncoding('utf8');
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
if (raw.length < MAX_STDIN) {
|
if (raw.length < MAX_STDIN) {
|
||||||
@@ -13,15 +32,18 @@ process.stdin.on('data', chunk => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
try {
|
const result = run(raw);
|
||||||
const input = JSON.parse(raw);
|
if (result && typeof result === 'object') {
|
||||||
const cmd = String(input.tool_input?.command || '');
|
if (result.stderr) {
|
||||||
if (/(npm run build|pnpm build|yarn build)/.test(cmd)) {
|
process.stderr.write(`${result.stderr}\n`);
|
||||||
console.error('[Hook] Build completed - async analysis running in background');
|
|
||||||
}
|
}
|
||||||
} catch {
|
process.stdout.write(String(result.stdout || ''));
|
||||||
// ignore parse errors and pass through
|
process.exitCode = Number.isInteger(result.exitCode) ? result.exitCode : 0;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(raw);
|
process.stdout.write(String(result));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { run };
|
||||||
|
|||||||
@@ -38,8 +38,24 @@ function appendLine(filePath, line) {
|
|||||||
fs.appendFileSync(filePath, `${line}\n`, 'utf8');
|
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() {
|
function main() {
|
||||||
const config = MODE_CONFIG[process.argv[2]];
|
const mode = process.argv[2];
|
||||||
|
|
||||||
process.stdin.setEncoding('utf8');
|
process.stdin.setEncoding('utf8');
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
@@ -50,17 +66,7 @@ function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
try {
|
process.stdout.write(run(raw, mode));
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,5 +75,6 @@ if (require.main === module) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
run,
|
||||||
sanitizeCommand,
|
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.exitCode = result.exitCode;
|
||||||
|
});
|
||||||
@@ -4,17 +4,9 @@
|
|||||||
const MAX_STDIN = 1024 * 1024;
|
const MAX_STDIN = 1024 * 1024;
|
||||||
let raw = '';
|
let raw = '';
|
||||||
|
|
||||||
process.stdin.setEncoding('utf8');
|
function run(rawInput) {
|
||||||
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', () => {
|
|
||||||
try {
|
try {
|
||||||
const input = JSON.parse(raw);
|
const input = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
|
||||||
const cmd = String(input.tool_input?.command || '');
|
const cmd = String(input.tool_input?.command || '');
|
||||||
|
|
||||||
if (/\bgh\s+pr\s+create\b/.test(cmd)) {
|
if (/\bgh\s+pr\s+create\b/.test(cmd)) {
|
||||||
@@ -24,13 +16,45 @@ process.stdin.on('end', () => {
|
|||||||
const prUrl = match[0];
|
const prUrl = match[0];
|
||||||
const repo = prUrl.replace(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/, '$1');
|
const repo = prUrl.replace(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/, '$1');
|
||||||
const prNum = prUrl.replace(/.+\/pull\/(\d+)/, '$1');
|
const prNum = prUrl.replace(/.+\/pull\/(\d+)/, '$1');
|
||||||
console.error(`[Hook] PR created: ${prUrl}`);
|
return {
|
||||||
console.error(`[Hook] To review: gh pr review ${prNum} --repo ${repo}`);
|
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 {
|
} catch {
|
||||||
// ignore parse errors and pass through
|
// 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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(String(result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { run };
|
||||||
|
|||||||
@@ -380,7 +380,11 @@ function evaluate(rawInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function run(rawInput) {
|
function run(rawInput) {
|
||||||
return evaluate(rawInput).output;
|
const result = evaluate(rawInput);
|
||||||
|
return {
|
||||||
|
stdout: result.output,
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── stdin entry point ────────────────────────────────────────────
|
// ── stdin entry point ────────────────────────────────────────────
|
||||||
|
|||||||
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.exitCode = result.exitCode;
|
||||||
|
});
|
||||||
@@ -4,6 +4,28 @@
|
|||||||
const MAX_STDIN = 1024 * 1024;
|
const MAX_STDIN = 1024 * 1024;
|
||||||
let raw = '';
|
let raw = '';
|
||||||
|
|
||||||
|
function run(rawInput) {
|
||||||
|
try {
|
||||||
|
const input = typeof rawInput === 'string' ? JSON.parse(rawInput) : 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: [
|
||||||
|
'[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
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
process.stdin.setEncoding('utf8');
|
process.stdin.setEncoding('utf8');
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
if (raw.length < MAX_STDIN) {
|
if (raw.length < MAX_STDIN) {
|
||||||
@@ -13,16 +35,18 @@ process.stdin.on('data', chunk => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
try {
|
const result = run(raw);
|
||||||
const input = JSON.parse(raw);
|
if (result && typeof result === 'object') {
|
||||||
const cmd = String(input.tool_input?.command || '');
|
if (result.stderr) {
|
||||||
if (/\bgit\s+push\b/.test(cmd)) {
|
process.stderr.write(`${result.stderr}\n`);
|
||||||
console.error('[Hook] Review changes before push...');
|
|
||||||
console.error('[Hook] Continuing with push (remove this hook to add interactive review)');
|
|
||||||
}
|
}
|
||||||
} catch {
|
process.stdout.write(String(result.stdout || ''));
|
||||||
// ignore parse errors and pass through
|
process.exitCode = Number.isInteger(result.exitCode) ? result.exitCode : 0;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(raw);
|
process.stdout.write(String(result));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { run };
|
||||||
|
|||||||
@@ -4,6 +4,33 @@
|
|||||||
const MAX_STDIN = 1024 * 1024;
|
const MAX_STDIN = 1024 * 1024;
|
||||||
let raw = '';
|
let raw = '';
|
||||||
|
|
||||||
|
function run(rawInput) {
|
||||||
|
try {
|
||||||
|
const input = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
|
||||||
|
const cmd = String(input.tool_input?.command || '');
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.platform !== 'win32' &&
|
||||||
|
!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)
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof rawInput === 'string' ? rawInput : JSON.stringify(rawInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
process.stdin.setEncoding('utf8');
|
process.stdin.setEncoding('utf8');
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
if (raw.length < MAX_STDIN) {
|
if (raw.length < MAX_STDIN) {
|
||||||
@@ -13,21 +40,18 @@ process.stdin.on('data', chunk => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
try {
|
const result = run(raw);
|
||||||
const input = JSON.parse(raw);
|
if (result && typeof result === 'object') {
|
||||||
const cmd = String(input.tool_input?.command || '');
|
if (result.stderr) {
|
||||||
|
process.stderr.write(`${result.stderr}\n`);
|
||||||
if (
|
|
||||||
process.platform !== 'win32' &&
|
|
||||||
!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');
|
|
||||||
}
|
}
|
||||||
} catch {
|
process.stdout.write(String(result.stdout || ''));
|
||||||
// ignore parse errors and pass through
|
process.exitCode = Number.isInteger(result.exitCode) ? result.exitCode : 0;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(raw);
|
process.stdout.write(String(result));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { run };
|
||||||
|
|||||||
114
tests/hooks/bash-hook-dispatcher.test.js
Normal file
114
tests/hooks/bash-hook-dispatcher.test.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Tests for consolidated Bash hook dispatchers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const preDispatcher = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'pre-bash-dispatcher.js');
|
||||||
|
const postDispatcher = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'post-bash-dispatcher.js');
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` ✓ ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ✗ ${name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runScript(scriptPath, input, env = {}) {
|
||||||
|
return spawnSync('node', [scriptPath], {
|
||||||
|
input: typeof input === 'string' ? input : JSON.stringify(input),
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...env,
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('\n=== Testing Bash hook dispatchers ===\n');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
if (test('pre dispatcher blocks --no-verify before other Bash checks', () => {
|
||||||
|
const input = { tool_input: { command: 'git commit --no-verify -m "x"' } };
|
||||||
|
const result = runScript(preDispatcher, input, { ECC_HOOK_PROFILE: 'strict' });
|
||||||
|
assert.strictEqual(result.status, 2, 'Expected dispatcher to block git hook bypass');
|
||||||
|
assert.ok(result.stderr.includes('--no-verify'), 'Expected block-no-verify reason in stderr');
|
||||||
|
assert.strictEqual(result.stdout, '', 'Blocking hook should not pass through stdout');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('pre dispatcher still honors per-hook disable flags', () => {
|
||||||
|
const input = { tool_input: { command: 'git push origin main' } };
|
||||||
|
|
||||||
|
const enabled = runScript(preDispatcher, input, { ECC_HOOK_PROFILE: 'strict' });
|
||||||
|
assert.strictEqual(enabled.status, 0);
|
||||||
|
assert.ok(enabled.stderr.includes('Review changes before push'), 'Expected git push reminder when enabled');
|
||||||
|
|
||||||
|
const disabled = runScript(preDispatcher, input, {
|
||||||
|
ECC_HOOK_PROFILE: 'strict',
|
||||||
|
ECC_DISABLED_HOOKS: 'pre:bash:git-push-reminder',
|
||||||
|
});
|
||||||
|
assert.strictEqual(disabled.status, 0);
|
||||||
|
assert.ok(!disabled.stderr.includes('Review changes before push'), 'Disabled hook should not emit reminder');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('pre dispatcher respects hook profiles inside the consolidated path', () => {
|
||||||
|
const input = { tool_input: { command: 'git push origin main' } };
|
||||||
|
const result = runScript(preDispatcher, input, { ECC_HOOK_PROFILE: 'minimal' });
|
||||||
|
assert.strictEqual(result.status, 0);
|
||||||
|
assert.strictEqual(result.stderr, '', 'Strict-only reminders should stay disabled in minimal profile');
|
||||||
|
assert.strictEqual(result.stdout, JSON.stringify(input));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('post dispatcher writes both bash audit and cost logs in one pass', () => {
|
||||||
|
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-bash-dispatcher-'));
|
||||||
|
const payload = { tool_input: { command: 'npm publish --token=$PUBLISH_TOKEN' } };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = runScript(postDispatcher, payload, {
|
||||||
|
HOME: homeDir,
|
||||||
|
USERPROFILE: homeDir,
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.status, 0);
|
||||||
|
assert.strictEqual(result.stdout, JSON.stringify(payload));
|
||||||
|
|
||||||
|
const auditLog = fs.readFileSync(path.join(homeDir, '.claude', 'bash-commands.log'), 'utf8');
|
||||||
|
const costLog = fs.readFileSync(path.join(homeDir, '.claude', 'cost-tracker.log'), 'utf8');
|
||||||
|
|
||||||
|
assert.ok(auditLog.includes('--token=<REDACTED>'));
|
||||||
|
assert.ok(costLog.includes('tool=Bash command=npm publish --token=<REDACTED>'));
|
||||||
|
assert.ok(!auditLog.includes('$PUBLISH_TOKEN'));
|
||||||
|
assert.ok(!costLog.includes('$PUBLISH_TOKEN'));
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('post dispatcher preserves PR-created hints after consolidated execution', () => {
|
||||||
|
const payload = {
|
||||||
|
tool_input: { command: 'gh pr create --title "Fix bug" --body "desc"' },
|
||||||
|
tool_output: { output: 'https://github.com/owner/repo/pull/42\n' },
|
||||||
|
};
|
||||||
|
const result = runScript(postDispatcher, payload);
|
||||||
|
assert.strictEqual(result.status, 0);
|
||||||
|
assert.ok(result.stderr.includes('PR created: https://github.com/owner/repo/pull/42'));
|
||||||
|
assert.ok(result.stderr.includes('gh pr review 42 --repo owner/repo'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
||||||
@@ -1888,6 +1888,33 @@ async function runTests() {
|
|||||||
passed++;
|
passed++;
|
||||||
else failed++;
|
else failed++;
|
||||||
|
|
||||||
|
if (
|
||||||
|
test('hooks.json consolidates Bash hooks into one pre and one post dispatcher', () => {
|
||||||
|
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
||||||
|
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
||||||
|
|
||||||
|
const preBash = hooks.hooks.PreToolUse.filter(entry => entry.matcher === 'Bash');
|
||||||
|
const postBash = hooks.hooks.PostToolUse.filter(entry => entry.matcher === 'Bash');
|
||||||
|
|
||||||
|
assert.strictEqual(preBash.length, 1, 'Should have exactly one PreToolUse Bash dispatcher');
|
||||||
|
assert.strictEqual(postBash.length, 1, 'Should have exactly one PostToolUse Bash dispatcher');
|
||||||
|
assert.strictEqual(preBash[0].id, 'pre:bash:dispatcher');
|
||||||
|
assert.strictEqual(postBash[0].id, 'post:bash:dispatcher');
|
||||||
|
|
||||||
|
const preCommand = Array.isArray(preBash[0].hooks[0].command)
|
||||||
|
? preBash[0].hooks[0].command.join(' ')
|
||||||
|
: preBash[0].hooks[0].command;
|
||||||
|
const postCommand = Array.isArray(postBash[0].hooks[0].command)
|
||||||
|
? postBash[0].hooks[0].command.join(' ')
|
||||||
|
: postBash[0].hooks[0].command;
|
||||||
|
|
||||||
|
assert.ok(preCommand.includes('pre-bash-dispatcher.js'), 'PreToolUse Bash hook should use the pre dispatcher');
|
||||||
|
assert.ok(postCommand.includes('post-bash-dispatcher.js'), 'PostToolUse Bash hook should use the post dispatcher');
|
||||||
|
})
|
||||||
|
)
|
||||||
|
passed++;
|
||||||
|
else failed++;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
test('SessionEnd marker hook is async and cleanup-safe', () => {
|
test('SessionEnd marker hook is async and cleanup-safe', () => {
|
||||||
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
||||||
|
|||||||
@@ -256,6 +256,14 @@ function getHookCommandByDescription(hooks, lifecycle, descriptionText) {
|
|||||||
return hookGroup.hooks[0].command;
|
return hookGroup.hooks[0].command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHookCommandById(hooks, lifecycle, hookId) {
|
||||||
|
const hookGroup = hooks.hooks[lifecycle]?.find(entry => entry.id === hookId);
|
||||||
|
|
||||||
|
assert.ok(hookGroup, `Expected ${lifecycle} hook with id "${hookId}"`);
|
||||||
|
assert.ok(hookGroup.hooks?.[0]?.command, `Expected ${lifecycle} hook command for id "${hookId}"`);
|
||||||
|
return hookGroup.hooks[0].command;
|
||||||
|
}
|
||||||
|
|
||||||
// Test suite
|
// Test suite
|
||||||
async function runTests() {
|
async function runTests() {
|
||||||
console.log('\n=== Hook Integration Tests ===\n');
|
console.log('\n=== Hook Integration Tests ===\n');
|
||||||
@@ -340,12 +348,7 @@ async function runTests() {
|
|||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (await asyncTest('dev server hook transforms command to tmux session', async () => {
|
if (await asyncTest('dev server hook transforms command to tmux session', async () => {
|
||||||
// Test the auto-tmux dev hook — transforms dev commands to run in tmux
|
const hookCommand = getHookCommandById(hooks, 'PreToolUse', 'pre:bash:dispatcher');
|
||||||
const hookCommand = getHookCommandByDescription(
|
|
||||||
hooks,
|
|
||||||
'PreToolUse',
|
|
||||||
'Auto-start dev servers in tmux'
|
|
||||||
);
|
|
||||||
const result = await runHookCommand(hookCommand, {
|
const result = await runHookCommand(hookCommand, {
|
||||||
tool_input: { command: 'npm run dev' }
|
tool_input: { command: 'npm run dev' }
|
||||||
});
|
});
|
||||||
@@ -526,12 +529,7 @@ async function runTests() {
|
|||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (await asyncTest('dev server hook transforms yarn dev to tmux session', async () => {
|
if (await asyncTest('dev server hook transforms yarn dev to tmux session', async () => {
|
||||||
// The auto-tmux dev hook transforms dev commands (yarn dev, npm run dev, etc.)
|
const hookCommand = getHookCommandById(hooks, 'PreToolUse', 'pre:bash:dispatcher');
|
||||||
const hookCommand = getHookCommandByDescription(
|
|
||||||
hooks,
|
|
||||||
'PreToolUse',
|
|
||||||
'Auto-start dev servers in tmux'
|
|
||||||
);
|
|
||||||
const result = await runHookCommand(hookCommand, {
|
const result = await runHookCommand(hookCommand, {
|
||||||
tool_input: { command: 'yarn dev' }
|
tool_input: { command: 'yarn dev' }
|
||||||
});
|
});
|
||||||
@@ -663,14 +661,8 @@ async function runTests() {
|
|||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (await asyncTest('PostToolUse PR hook extracts PR URL', async () => {
|
if (await asyncTest('PostToolUse PR hook extracts PR URL', async () => {
|
||||||
// Find the PR logging hook
|
const hookCommand = getHookCommandById(hooks, 'PostToolUse', 'post:bash:dispatcher');
|
||||||
const prHook = hooks.hooks.PostToolUse.find(h =>
|
const result = await runHookCommand(hookCommand, {
|
||||||
h.description && h.description.includes('PR URL')
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.ok(prHook, 'PR hook should exist');
|
|
||||||
|
|
||||||
const result = await runHookCommand(prHook.hooks[0].command, {
|
|
||||||
tool_input: { command: 'gh pr create --title "Test"' },
|
tool_input: { command: 'gh pr create --title "Test"' },
|
||||||
tool_output: { output: 'Creating pull request...\nhttps://github.com/owner/repo/pull/123' }
|
tool_output: { output: 'Creating pull request...\nhttps://github.com/owner/repo/pull/123' }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -361,23 +361,27 @@ function runTests() {
|
|||||||
const claudeRoot = path.join(homeDir, '.claude');
|
const claudeRoot = path.join(homeDir, '.claude');
|
||||||
const installedHooks = readJson(path.join(claudeRoot, 'hooks', 'hooks.json'));
|
const installedHooks = readJson(path.join(claudeRoot, 'hooks', 'hooks.json'));
|
||||||
|
|
||||||
const installedAutoTmuxEntry = installedHooks.hooks.PreToolUse.find(entry => entry.id === 'pre:bash:auto-tmux-dev');
|
const installedBashDispatcherEntry = installedHooks.hooks.PreToolUse.find(entry => entry.id === 'pre:bash:dispatcher');
|
||||||
assert.ok(installedAutoTmuxEntry, 'hooks/hooks.json should include the auto tmux hook');
|
assert.ok(installedBashDispatcherEntry, 'hooks/hooks.json should include the consolidated Bash dispatcher hook');
|
||||||
assert.ok(Array.isArray(installedAutoTmuxEntry.hooks[0].command), 'hooks/hooks.json should install argv-form commands for cross-platform safety');
|
assert.ok(Array.isArray(installedBashDispatcherEntry.hooks[0].command), 'hooks/hooks.json should install argv-form commands for cross-platform safety');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
installedAutoTmuxEntry.hooks[0].command[0] === 'node' && installedAutoTmuxEntry.hooks[0].command[1] === '-e',
|
installedBashDispatcherEntry.hooks[0].command[0] === 'node' && installedBashDispatcherEntry.hooks[0].command[1] === '-e',
|
||||||
'hooks/hooks.json should use the inline node bootstrap contract'
|
'hooks/hooks.json should use the inline node bootstrap contract'
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
installedAutoTmuxEntry.hooks[0].command.some(part => String(part).includes('plugin-hook-bootstrap.js')),
|
installedBashDispatcherEntry.hooks[0].command.some(part => String(part).includes('plugin-hook-bootstrap.js')),
|
||||||
'hooks/hooks.json should route plugin-managed hooks through the shared bootstrap'
|
'hooks/hooks.json should route plugin-managed hooks through the shared bootstrap'
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
installedAutoTmuxEntry.hooks[0].command.some(part => String(part).includes('CLAUDE_PLUGIN_ROOT')),
|
installedBashDispatcherEntry.hooks[0].command.some(part => String(part).includes('CLAUDE_PLUGIN_ROOT')),
|
||||||
'hooks/hooks.json should still consult CLAUDE_PLUGIN_ROOT for runtime resolution'
|
'hooks/hooks.json should still consult CLAUDE_PLUGIN_ROOT for runtime resolution'
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!installedAutoTmuxEntry.hooks[0].command.some(part => String(part).includes('${CLAUDE_PLUGIN_ROOT}')),
|
installedBashDispatcherEntry.hooks[0].command.some(part => String(part).includes('pre-bash-dispatcher.js')),
|
||||||
|
'hooks/hooks.json should point the Bash preflight contract at the consolidated dispatcher'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!installedBashDispatcherEntry.hooks[0].command.some(part => String(part).includes('${CLAUDE_PLUGIN_ROOT}')),
|
||||||
'hooks/hooks.json should not retain raw CLAUDE_PLUGIN_ROOT shell placeholders after install'
|
'hooks/hooks.json should not retain raw CLAUDE_PLUGIN_ROOT shell placeholders after install'
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user