feat: execute bounded local Hookify rules in lifecycle hooks

Carry forward PR #3055 at 92f53c4c onto ef648e01, preserving contributor attribution and current main. Focused maintainer fixes reuse shared input ceilings, preserve late root context through outer bootstrap, bound frontmatter before immutable parsing, and assert real oversized input in the limits workflow.
This commit is contained in:
wellkilo authored and haelyra committed 2026-10-09 15:59:20 -04:00
1 parent ef648e0189
commit a50ee13cac
23 files changed
+2846 -41

No files matched your search

+26 -1
View File
@@ -9,7 +9,8 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes
```
- **PreToolUse** hooks run before the tool executes. They can **block** (exit code 2) or **warn** (stderr without blocking).
- **PostToolUse** hooks run after the tool completes. They can analyze output but cannot block.
- **PostToolUse** hooks run after the tool completes. They can analyze output and return feedback before Claude continues.
- **UserPromptSubmit** hooks run before Claude processes a user prompt.
- **Stop** hooks run after each Claude response.
- **SessionStart/SessionEnd** hooks run at session lifecycle boundaries.
- **PreCompact** hooks run before context compaction, useful for saving state.
@@ -53,6 +54,7 @@ idempotent updates and safe uninstall. On Windows, the Claude config root is
| **Pre-commit quality check** | `Bash` | Runs quality checks before `git commit`: lints staged files, validates commit message format when provided via `-m/--message`, detects console.log/debugger/secrets | 2 (blocks critical) / 0 (warns) |
| **Doc file warning** | `Write` | Warns about non-standard `.md`/`.txt` files (allows README, CLAUDE, CONTRIBUTING, CHANGELOG, LICENSE, SKILL, docs/, skills/); cross-platform path handling | 0 (warns) |
| **Strategic compact** | `Edit\|Write` | Suggests manual `/compact` at logical intervals (every ~50 tool calls) | 0 (warns) |
| **Hookify runtime** | `.*` | Evaluates enabled project-local Hookify rules before shell and file tools | 0 (structured warn/block decision) |
### PostToolUse Hooks
@@ -65,14 +67,17 @@ idempotent updates and safe uninstall. On Windows, the Claude config root is
| **Prettier format** | `Edit` | Auto-formats JS/TS files with Prettier after edits |
| **TypeScript check** | `Edit` | Runs `tsc --noEmit` after editing `.ts`/`.tsx` files |
| **console.log warning** | `Edit` | Warns about `console.log` statements in edited files |
| **Hookify runtime** | `.*` | Evaluates project-local Hookify rules against completed shell and file tools |
### Lifecycle Hooks
| Hook | Event | What It Does |
|------|-------|-------------|
| **Session start** | `SessionStart` | Loads previous context and detects package manager |
| **Hookify prompt rules** | `UserPromptSubmit` | Evaluates prompt rules before the user prompt reaches Claude |
| **Plan Canvas sessions** | `SessionStart` | Surfaces open Plan Canvas browser reviews so a fresh session can resume the loop |
| **Pre-compact** | `PreCompact` | Saves state before context compaction |
| **Hookify completion rules** | `Stop` | Warns or requests another turn when an enabled completion rule matches |
| **Console.log audit** | `Stop` | Checks all modified files for `console.log` after each response |
| **Session summary** | `Stop` | Persists session state when transcript path is available |
| **Pattern extraction** | `Stop` | Evaluates session for extractable patterns (continuous learning) |
@@ -157,6 +162,26 @@ The Claude plugin exposes the same choices as the personal `hooks_enabled` and
`hook_profile` settings. Run `ecc setup --mode claude-plugin` to install or
update the plugin and change those preferences.
### Hookify Rule Runtime
ECC loads enabled `.claude/hookify.*.local.md` files directly; no separate
Hookify plugin or Python runtime is required. Rules support `bash`, `file`,
`prompt`, `stop`, and `all` events, plus either a simple regex `pattern` or an
AND-combined `conditions` list. `warn` adds visible context for Claude while
allowing the action. `block` denies a matching `PreToolUse` call and uses the
event's blocking decision contract for prompt, post-tool, and stop events.
Rule loading is project-scoped and bounded. Symlinked, oversized, malformed,
or unsafe-regex rule files are skipped with a diagnostic instead of breaking
the entire hook chain. Every regex evaluation also has a 25 ms hard timeout.
Stop rules ignore `stop_hook_active: true` to avoid continuation loops.
Files tracked by Git are disabled by default because opening a repository must
not activate repository-authored instructions. Locally created, gitignored
rules continue to work automatically. To review and explicitly trust tracked
rules, set `ECC_HOOKIFY_ALLOW_TRACKED=1`; messages are still labeled as
untrusted local rule data when passed to Claude.
### Writing Your Own Hook
Hooks are shell commands that receive tool input as JSON on stdin. A hook with
+31
View File
@@ -87,6 +87,27 @@
"timeout": 5
}
]
},
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}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:hookify-runtime scripts/hooks/hookify-runtime.js minimal,standard,strict",
"timeout": 10
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}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 prompt:hookify-runtime scripts/hooks/hookify-runtime.js minimal,standard,strict",
"timeout": 10
}
]
}
],
"PreCompact": [
@@ -164,6 +185,16 @@
}
],
"Stop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const n=process.platform==='win32'&&r.charAt(0)==='/'&&/[a-zA-Z]/.test(r.charAt(1))&&(r.length===2||r.charAt(2)==='/')?r.charAt(1).toUpperCase()+':/'+r.slice(3):r;const s=p.join(n,'scripts/hooks/lifecycle-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=n;if(require('fs').existsSync(s)){process.argv.splice(1,0,s);require(s).cli()}else{process.stderr.write('[Hook] lifecycle bootstrap unavailable; skipping hook'+String.fromCharCode(10))}\" stop:hookify-runtime scripts/hooks/hookify-runtime.js minimal,standard,strict 10000",
"timeout": 10
}
]
},
{
"matcher": ".*",
"hooks": [
+17
View File
@@ -46,6 +46,18 @@
"id": "pre:edit-write:gateguard-fact-force",
"description": "Fact-forcing gate: block first Edit/Write/MultiEdit per file and demand investigation (importers, data schemas, user instruction) before allowing",
"fingerprint": "32c4a312b4c2"
},
{
"id": "pre:hookify-runtime",
"description": "Evaluate project-local Hookify rules before Bash, PowerShell, and file tools run",
"fingerprint": "65b5a5d6bbd5"
}
],
"UserPromptSubmit": [
{
"id": "prompt:hookify-runtime",
"description": "Evaluate project-local Hookify prompt rules before a user prompt is processed",
"fingerprint": "89b4809a5a7e"
}
],
"PreCompact": [
@@ -92,6 +104,11 @@
}
],
"Stop": [
{
"id": "stop:hookify-runtime",
"description": "Evaluate project-local Hookify completion rules before Claude stops",
"fingerprint": "7dd7cbc0df5f"
},
{
"id": "stop:plan-canvas-pending",
"description": "Deliver undelivered Plan Canvas browser feedback before the agent stops",