hooks/README.md
Hooks are event-driven automations that fire before or after Claude Code tool executions. They enforce code quality, catch mistakes early, and automate repetitive checks.
User request → Claude picks a tool → PreToolUse hook runs → Tool executes → PostToolUse hook runs
Memory persistence lifecycle definitions live in hooks/memory-persistence/.
The executable hook graph remains hooks/hooks.json; the memory persistence directory is the stable contract for SessionStart, PreCompact, observation, activity tracking, and SessionEnd behavior.
For Claude Code manual installs, do not paste the raw repo hooks.json into ~/.claude/settings.json or copy it directly into ~/.claude/hooks/hooks.json. The checked-in file is plugin/repo-oriented and is meant to be installed through the ECC installer or loaded as a plugin.
Use the installer instead so hook commands are rewritten against your actual Claude root:
bash ./install.sh --target claude --modules hooks-runtime
pwsh -File .\install.ps1 --target claude --modules hooks-runtime
That installs resolved hooks to ~/.claude/hooks/hooks.json. On Windows, the Claude config root is %USERPROFILE%\\.claude.
| Hook | Matcher | Behavior | Exit Code |
|---|---|---|---|
| Dev server blocker | Bash | Blocks npm run dev etc. outside tmux — ensures log access | 2 (blocks) |
| Tmux reminder | Bash | Suggests tmux for long-running commands (npm test, cargo build, docker) | 0 (warns) |
| Git push reminder | Bash | Reminds to review changes before git push | 0 (warns) |
| 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) |
| Hook | Matcher | What It Does |
|---|---|---|
| PR logger | Bash | Logs PR URL and review command after gh pr create |
| Build analysis | Bash | Background analysis after build commands (async, non-blocking) |
| Quality gate | Edit|Write|MultiEdit | Runs fast quality checks after edits |
| Design quality check | Edit|Write|MultiEdit | Warns when frontend edits drift toward generic template-looking UI |
| 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 |
| Hook | Event | What It Does |
|---|---|---|
| Session start | SessionStart | Loads previous context and detects package manager |
| 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 |
| 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) |
| Cost tracker | Stop | Emits lightweight run-cost telemetry markers |
| Desktop notify | Stop | Sends macOS desktop notification with task summary (standard+) |
| Session end marker | SessionEnd | Lifecycle marker and cleanup log |
Remove or comment out the hook entry in hooks.json. If installed as a plugin, override in your ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [],
"description": "Override: allow all .md file creation"
}
]
}
}
Use environment variables to control hook behavior without editing hooks.json:
# Master switch. Explicit environment values override plugin preferences.
export ECC_HOOKS_ENABLED=true
# minimal | standard | strict (default: standard)
export ECC_HOOK_PROFILE=standard
# Disable specific hook IDs (comma-separated)
export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck"
# Disable only GateGuard during setup or recovery
export ECC_GATEGUARD=off
# Cap SessionStart additional context (default: 8000 chars)
export ECC_SESSION_START_MAX_CHARS=4000
# Disable SessionStart additional context entirely
export ECC_SESSION_START_CONTEXT=off
# Keep context/scope/loop warnings but suppress API-rate cost estimates
export ECC_CONTEXT_MONITOR_COST_WARNINGS=off
Windows PowerShell:
[Environment]::SetEnvironmentVariable('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'off', 'User')
Claude setup-only value:
off — disables local ECC hook work through ecc setup; it is not a runtime hook profile.Runtime hook profiles:
minimal — keep essential lifecycle and safety hooks only.standard — default; balanced quality + safety checks.strict — enables additional reminders and stricter guardrails.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.
Hooks are shell commands that receive tool input as JSON on stdin and must output JSON on stdout.
Basic structure:
// my-hook.js
let data = '';
process.stdin.on('data', chunk => data += chunk);
process.stdin.on('end', () => {
const input = JSON.parse(data);
// Access tool info
const toolName = input.tool_name; // "Edit", "Bash", "Write", etc.
const toolInput = input.tool_input; // Tool-specific parameters
const toolOutput = input.tool_output; // Only available in PostToolUse
// Warn (non-blocking): write to stderr
console.error('[Hook] Warning message shown to Claude');
// Block (PreToolUse only): exit with code 2
// process.exit(2);
// Always output the original data to stdout
console.log(data);
});
Exit codes:
0 — Success (continue execution)2 — Block the tool call (PreToolUse only)interface HookInput {
tool_name: string; // "Bash", "Edit", "Write", "Read", etc.
tool_input: {
command?: string; // Bash: the command being run
file_path?: string; // Edit/Write/Read: target file
old_string?: string; // Edit: text being replaced
new_string?: string; // Edit: replacement text
content?: string; // Write: file content
};
tool_output?: { // PostToolUse only
output?: string; // Command/tool output
};
}
For hooks that should not block the main flow (e.g., background analysis):
{
"type": "command",
"command": "node my-slow-hook.js",
"async": true,
"timeout": 30
}
Async hooks run in the background. They cannot block tool execution.
{
"matcher": "Edit",
"hooks": [{
"type": "command",
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const ns=i.tool_input?.new_string||'';if(/TODO|FIXME|HACK/.test(ns)){console.error('[Hook] New TODO/FIXME added - consider creating an issue')}console.log(d)})\""
}],
"description": "Warn when adding TODO/FIXME comments"
}
{
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[Hook] BLOCKED: File exceeds 800 lines ('+lines+' lines)');console.error('[Hook] Split into smaller, focused modules');process.exit(2)}console.log(d)})\""
}],
"description": "Block creation of files larger than 800 lines"
}
{
"matcher": "Edit",
"hooks": [{
"type": "command",
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/\\.py$/.test(p)){const{execFileSync}=require('child_process');try{execFileSync('ruff',['format',p],{stdio:'pipe'})}catch(e){}}console.log(d)})\""
}],
"description": "Auto-format Python files with ruff after edits"
}
{
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/src\\/.*\\.(ts|js)$/.test(p)&&!/\\.test\\.|\\.spec\\./.test(p)){const testPath=p.replace(/\\.(ts|js)$/,'.test.$1');if(!fs.existsSync(testPath)){console.error('[Hook] No test file found for: '+p);console.error('[Hook] Expected: '+testPath);console.error('[Hook] Consider writing tests first (/tdd)')}}console.log(d)})\""
}],
"description": "Remind to create tests when adding new source files"
}
Hook logic is implemented in Node.js scripts for cross-platform behavior on Windows, macOS, and Linux. The continuous-learning observer is exposed as a Node-mode hook and delegates to its existing observe.sh implementation through a profile-gated runner with Windows-safe fallback behavior.