Files
everything-claude-code/hooks
QWsin 118e57e14b feat(hooks): add WSL desktop notification support via PowerShell + BurntToast (#1019)
* fix(hooks): add WSL desktop notification support via PowerShell + BurntToast

Adds WSL (Windows Subsystem for Linux) desktop notification support to the
existing desktop-notify hook. The hook now detects WSL, finds available
PowerShell (7 or Windows PowerShell), checks for BurntToast module, and
sends Windows toast notifications.

New functions:
- isWSL(): detects WSL environment
- findPowerShell(): finds PowerShell 7 or Windows PowerShell on WSL
- isBurntToastAvailable(): checks if BurntToast module is installed
- notifyWindows(): sends Windows toast notification via BurntToast

If BurntToast is not installed, logs helpful tip for installation.
Falls back silently on non-WSL/non-macOS platforms.

* docs(hooks): update desktop-notify description to include WSL

Updates the hook description in hooks.json to reflect the newly
added WSL notification support alongside macOS.

* fix(hooks): capture stderr properly in notifyWindows

Change stdio to ['ignore', 'pipe', 'pipe'] so stderr is captured
and can be logged on errors. Without this, result.stderr is null
and error logs show 'undefined' instead of the actual error.

* fix(hooks): quote PowerShell path in install tip for shell safety

The PowerShell path contains spaces and needs to be quoted
when displayed as a copy-pasteable command.

* fix(hooks): remove external repo URL from tip message

BurntToast module is a well-known Microsoft module but per project
policy avoiding unvetted external links in user-facing output.

* fix(hooks): probe WSL interop PATH before hardcoded paths

Adds 'pwsh.exe' and 'powershell.exe' as candidates to leverage
WSL's Windows interop PATH resolution, making the hook work with
non-default WSL mount prefixes or Windows drives.

* perf(hooks): memoize isWSL detection at module load

Avoids reading /proc/version twice (once in run(), once in findPowerShell())
by computing the result once when the module loads.

* perf(hooks): reduce PowerShell spawns from 3 to 1 per notification

Merge findPowerShell version check and isBurntToastAvailable check
into a single notifyWindows call. Now just tries to send directly;
if it fails, tries next PowerShell path. Version field was unused.

Net effect: up to 3 spawns reduced to 1 in the happy path.

* fix(hooks): remove duplicate notifyWindows declaration

There were two notifyWindows function declarations due to incomplete
refactoring. Keeps only the version that returns true/false for the
call site. Node.js would throw SyntaxError with 'use strict'.

* fix(hooks): improve error handling and detection robustness

- Increase PowerShell detection timeout from 1s to 3s to avoid false
  negatives on slower/cold WSL interop startup
- Return error reason from notifyWindows to distinguish BurntToast
  module not found vs other PowerShell errors
- Log actionable error details instead of always showing install tip

---------

Co-authored-by: boss <boss@example.com>
2026-03-30 03:14:49 -04:00
..

Hooks

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.

How Hooks Work

User request → Claude picks a tool → PreToolUse hook runs → Tool executes → PostToolUse hook runs
  • 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.
  • 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.

Hooks in This Plugin

PreToolUse Hooks

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)
InsAIts security monitor (opt-in) Bash|Write|Edit|MultiEdit Optional security scan for high-signal tool inputs. Disabled unless ECC_ENABLE_INSAITS=1. Blocks on critical findings, warns on non-critical, and writes audit log to .insaits_audit_session.jsonl. Requires pip install insa-its. Details 2 (blocks critical) / 0 (warns)

PostToolUse Hooks

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
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

Lifecycle Hooks

Hook Event What It Does
Session start SessionStart Loads previous context and detects package manager
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

Customizing Hooks

Disabling a Hook

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:

# 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"

Profiles:

  • minimal — keep essential lifecycle and safety hooks only.
  • standard — default; balanced quality + safety checks.
  • strict — enables additional reminders and stricter guardrails.

Writing Your Own Hook

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)
  • Other non-zero — Error (logged but does not block)

Hook Input Schema

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
  };
}

Async Hooks

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.

Common Hook Recipes

Warn about TODO comments

{
  "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"
}

Block large file creation

{
  "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"
}

Auto-format Python files with ruff

{
  "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"
}

Require test files alongside new source files

{
  "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"
}

Cross-Platform Notes

Hook logic is implemented in Node.js scripts for cross-platform behavior on Windows, macOS, and Linux. A small number of shell wrappers are retained for continuous-learning observer hooks; those wrappers are profile-gated and have Windows-safe fallback behavior.