Commit Graph

106 Commits

  • perf(hooks): batch format+typecheck at Stop instead of per Edit (#746)
    * perf(hooks): batch format+typecheck at Stop instead of per Edit
    
    Fixes #735. The per-edit post:edit:format and post:edit:typecheck hooks
    ran synchronously after every Edit call, adding 15-30s of latency per
    file — up to 7.5 minutes for a 10-file refactor.
    
    New approach:
    - post-edit-accumulator.js (PostToolUse/Edit): lightweight hook that
      records each edited JS/TS path to a session-scoped temp file in
      os.tmpdir(). No formatters, no tsc — exits in microseconds.
    - stop-format-typecheck.js (Stop): reads the accumulator once per
      response, groups files by project root and runs the formatter in
      one batched invocation per root, then groups .ts/.tsx files by
      tsconfig dir and runs tsc once per tsconfig. Clears the accumulator
      immediately on read so repeated Stop calls don't double-process.
    
    For a 10-file refactor: was 10 × (15s + 30s) = 7.5 min overhead,
    now 1 × (batch format + batch tsc) = ~5-30s total.
    
    * fix(hooks): address race condition, spawn timeout, and Windows path guard
    
    Three issues raised in code review:
    
    1. Race condition: switched accumulator from non-atomic JSON
       read-modify-write to appendFileSync (one path per line). Concurrent
       Edit hook processes each append independently without clobbering each
       other. Deduplication moved to the Stop hook at read time.
    
    2. Effective timeout: added run() export to stop-format-typecheck.js so
       run-with-flags.js uses the direct require() path instead of falling
       through to spawnSync (which has a hardcoded 30s cap). The 120s
       timeout in hooks.json now governs the full batch as intended.
    
    3. Windows path guard: added spaces and parentheses to UNSAFE_PATH_CHARS
       so paths like "C:\Users\John Doe\project\file.ts" are caught before
       being passed to cmd.exe with shell: true.
    
    * fix(hooks): fix session fallback, stale comment, trim verbose comments
    
    - Replace 'default' session ID fallback with a cwd-based sha1 hash so
      concurrent sessions in different projects don't share the same
      accumulator file when CLAUDE_SESSION_ID is unset
    - Remove stale "JSON file" reference in accumulator header (format is
      now newline-delimited plain text)
    - Remove redundant/verbose inline comments throughout both files
    
    * fix(hooks): sanitize session ID, fix Windows tsc, proportional timeouts
    
    - Sanitize CLAUDE_SESSION_ID with /[^a-zA-Z0-9_-]/g before embedding in
      the temp filename so crafted separators or '..' sequences cannot escape
      os.tmpdir() (cubic P1)
    - Fix typecheckBatch on Windows: npx.cmd requires shell:true like
      formatBatch already does; use spawnSync and extract stdout/stderr from
      the result object (coderabbit P1)
    - Proportional per-batch timeouts: divide 270s budget across all format
      and typecheck batches so sequential runs in monorepos stay within the
      Stop hook wall-clock limit (greptile P2)
    - Raise Stop hook timeout from 120s to 300s to give large monorepos
      adequate headroom (cubic P2)
    
    * fix(hooks): extend accumulator to Write|MultiEdit, fix tests
    
    - Extend matcher from Edit to Edit|Write|MultiEdit so files created with
      Write and all files in a MultiEdit batch are included in the Stop-time
      format+typecheck pass (cubic P1)
    - Handle tool_input.edits[] array in accumulator for MultiEdit support
    - Rename misleading 'concurrent writes' test to clarify it tests append
      preservation, not true concurrency (cubic P2)
    - Add Stop hook dedup test: writes duplicate paths to accumulator and
      verifies the hook clears it cleanly (cubic P2)
    - Add Write and MultiEdit accumulation tests
    
    * fix(hooks): move timeout to command level, add dedup unit tests
    
    - Move timeout: 300 from the matcher object to the hook command object
      where it is actually enforced; the previous position was a no-op
      (cubic P2)
    - Extract parseAccumulator() and export it so tests can assert dedup
      behavior directly without relying only on side effects (cubic P2)
    - Add two unit tests for parseAccumulator: deduplication and blank-line
      handling; rename the integration test to match its scope
    
    * fix(hooks): replace removed format/typecheck hooks with accumulator in cursor adapter
  • fix: filter session-start injection by cwd/project to prevent cross-project contamination (#1054)
    * fix: filter session-start injection by cwd/project to prevent cross-project contamination
    
    The SessionStart hook previously selected the most recent session file
    purely by timestamp, ignoring the current working directory. This caused
    Claude to receive a previous project's session context when switching
    between projects, leading to incorrect file reads and project analysis.
    
    session-end.js already writes **Project:** and **Worktree:** header
    fields into each session file. This commit adds selectMatchingSession()
    which uses those fields with the following priority:
    
    1. Exact worktree (cwd) match — most recent
    2. Same project name match — most recent
    3. Fallback to overall most recent (preserves backward compatibility)
    
    No new dependencies. Gracefully falls back to original behavior when
    no matching session exists.
    
    * fix: address review feedback — eliminate duplicate I/O, add null guards, improve docstrings
    
    - Return { session, content, matchReason } from selectMatchingSession()
      to avoid reading the same file twice (coderabbitai, greptile P2)
    - Add empty array guard: return null when sessions.length === 0 (coderabbitai)
    - Stop mutating input objects — no more session._matchReason (coderabbitai)
    - Add null check on result before accessing properties (coderabbitai)
    - Only log "selected" after confirming content is readable (cubic-dev-ai P3)
    - Add full JSDoc with @param/@returns (docstring coverage)
    
    * fix: track fallback session object to prevent session/content mismatch
    
    When sessions[0] is unreadable, fallbackContent came from a later
    session (e.g. sessions[1]) while the returned session object still
    pointed to sessions[0]. This caused misleading logs and injected
    content from the wrong session — the exact problem this PR fixes.
    
    Now tracks fallbackSession alongside fallbackContent so the returned
    pair is always consistent.
    
    Addresses greptile-apps P1 review feedback.
    
    * fix: normalize worktree paths to handle symlinks and case differences
    
    On macOS /var is a symlink to /private/var, and on Windows paths may
    differ in casing (C:\repo vs c:\repo). Use fs.realpathSync() to
    resolve both sides before comparison so worktree matching is reliable
    across symlinked and case-insensitive filesystems.
    
    cwd is normalized once outside the loop to avoid repeated syscalls.
    
    Addresses coderabbitai Major review feedback.
    
    ---------
    
    Co-authored-by: kuqili <kuqili@tencent.com>
  • fix: extract inline SessionStart bootstrap to separate file (#1035)
    Inline `node -e "..."` in hooks.json contained `!` characters (e.g.
    `!org.isDirectory()`) that bash history expansion in certain shell
    environments would misinterpret, producing syntax errors and the
    "SessionStart:startup hook error" banner in the Claude Code CLI header.
    
    Extract the bootstrap logic to `scripts/hooks/session-start-bootstrap.js`
    so the shell never sees the JS source. Behaviour is identical: the script
    reads stdin, resolves the ECC plugin root via CLAUDE_PLUGIN_ROOT or a set
    of well-known fallback paths, then delegates to run-with-flags.js.
    
    Update the test that asserted the old inline pattern to verify the new
    file-based approach instead.
    
    Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
  • fix(hooks): pass phase argument from hook ID to observe.sh (#1042)
    The shell wrapper run-with-flags-shell.sh was not extracting the phase
    prefix from the hook ID (e.g., "pre:observe" -> "pre") and passing it
    as $1 to the invoked script. This caused observe.sh to always default
    to "post", recording all observations as tool_complete events with no
    tool_start events captured.
    
    Fixes #1018
    
    Co-authored-by: Millectable <noreply@github.com>
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • 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>
  • fix(lint): prefix unused options parameter with underscore
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
  • refactor: address reviewer feedback
    - Add options={} parameter to run() to match run-with-flags.js contract
    - Remove case-insensitive flag from extension pre-filter for consistency
      with ADHOC_FILENAMES regex (both now case-sensitive)
    - Expand warning text to list more structured paths
    - Add test cases for uppercase extensions (TODO.MD, NOTES.TXT)
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
  • fix(hooks): port doc-file-warning denylist policy to current hook runtime
    Replace the broad allowlist approach with a targeted denylist that only
    warns on known ad-hoc filenames (NOTES, TODO, SCRATCH, TEMP, DRAFT,
    BRAINSTORM, SPIKE, DEBUG, WIP) outside structured directories. This
    eliminates false positives for legitimate markdown-heavy workflows while
    still catching impulse documentation files.
    
    Closes #988
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
  • feat(hooks): add pre-commit quality check hook
    - Add pre-bash-commit-quality.js hook script
    - Runs quality checks before git commit commands:
      - Lints staged files (ESLint, Pylint, golint)
      - Validates commit message format (conventional commits)
      - Detects console.log/debugger statements
      - Warns about TODO/FIXME without issue references
      - Detects potential hardcoded secrets
    - Updates hooks.json with new hook configuration
    - Updates README.md with hook documentation
    
    Cross-platform (Windows, macOS, Linux)
  • Merge pull request #846 from pythonstrup/feat/desktop-notify-hook
    feat: add macOS desktop notification Stop hook
  • fix: add spawnSync error logging and restore 5s timeout
    - Check spawnSync result and log warning on failure via stderr
    - Restore osascript timeout to 5000ms, increase hook deadline to 10s
      for sufficient headroom
  • fix: use AppleScript-safe escaping and reduce spawnSync timeout
    - Replace JSON.stringify with curly quote substitution for AppleScript
      compatibility (AppleScript does not support \" backslash escapes)
    - Reduce spawnSync timeout from 5000ms to 3000ms to leave headroom
      within the 5s hook deadline
  • feat: add macOS desktop notification Stop hook
    Add a new Stop hook that sends a native macOS notification with the
    task summary (first line of last_assistant_message) when Claude finishes
    responding. Uses osascript via spawnSync for shell injection safety.
    Supports run-with-flags fast require() path. Only active on standard
    and strict profiles; silently skips on non-macOS platforms.
  • feat(hooks): add config protection hook to block linter config manipulation (#758)
    * feat(hooks): add config protection hook to block linter config manipulation
    
    Agents frequently modify linter/formatter configs (.eslintrc, biome.json,
    .prettierrc, .ruff.toml, etc.) to make checks pass instead of fixing
    the actual code.
    
    This PreToolUse hook intercepts Write/Edit/MultiEdit calls targeting
    known config files and blocks them with a steering message that directs
    the agent to fix the source code instead.
    
    Covers: ESLint, Prettier, Biome, Ruff, ShellCheck, Stylelint, and
    Markdownlint configs.
    
    Fixes #733
    
    * Address review: fix dead code, add missing configs, export run()
    
    - Removed pyproject.toml from PROTECTED_FILES (was dead code since
      it was also in PARTIAL_CONFIG_FILES). Added comment explaining why
      it's intentionally excluded.
    - Removed PARTIAL_CONFIG_FILES entirely (no longer needed).
    - Added missing ESLint v9 TypeScript flat configs: eslint.config.ts,
      eslint.config.mts, eslint.config.cts
    - Added missing Prettier ESM config: prettier.config.mjs
    - Exported run() function for in-process execution via run-with-flags,
      avoiding the spawnSync overhead (~50-100ms per call).
    
    * Handle stdin truncation gracefully, log warning instead of fail-open
    
    If stdin exceeds 1MB, the JSON would be malformed and the catch
    block would silently pass through. Now we detect truncation and
    log a warning. The in-process run() path is not affected.
  • feat: agent compression, inspection logic, governance hooks (#491, #485, #482) (#688)
    Implements three roadmap features:
    
    - Agent description compression (#491): New `agent-compress` module with
      catalog/summary/full compression modes and lazy-loading. Reduces ~26k
      token agent descriptions to ~2-3k catalog entries for context efficiency.
    
    - Inspection logic (#485): New `inspection` module that detects recurring
      failure patterns in skill_runs. Groups by skill + normalized failure
      reason, generates structured reports with suggested remediation actions.
      Configurable threshold (default: 3 failures).
    
    - Governance event capture hook (#482): PreToolUse/PostToolUse hook that
      detects secrets, policy violations, approval-required commands, and
      elevated privilege usage. Gated behind ECC_GOVERNANCE_CAPTURE=1 flag.
      Writes to governance_events table via JSON-line stderr output.
    
    59 new tests (16 + 16 + 27), all passing.
  • fix: strip ANSI escape codes from session persistence hooks (#642) (#684)
    Windows terminals emit control sequences (cursor movement, screen
    clearing) that leaked into session.tmp files and were injected
    verbatim into Claude's context on the next session start.
    
    Add a comprehensive stripAnsi() to utils.js that handles CSI, OSC,
    charset selection, and bare ESC sequences. Apply it in session-end.js
    (when extracting user messages from the transcript) and in
    session-start.js (safety net before injecting session content).
  • fix: export run() to avoid Windows spawnSync issues (#431)
    - session-end-marker.js now exports run() function
    - Enables in-process execution via run-with-flags.js
    - Avoids spawnSync cross-platform issues on Windows
    - Maintains backward compatibility with direct CLI execution
    
    Fixes #429
    
    Co-authored-by: 阳虎 <yanghu@yanghudeMacBook-Pro.local>
  • refactor: deduplicate config lists and unify resolveFormatterBin branches
    Extract BIOME_CONFIGS and PRETTIER_CONFIGS as shared constants to eliminate
    duplication between PROJECT_ROOT_MARKERS and detectFormatter(). Unify the
    biome/prettier branches in resolveFormatterBin() via a FORMATTER_PACKAGES
    map. Remove redundant path.resolve() in quality-gate.js.
  • fix(hooks): add Windows .cmd support with shell injection guard
    Handle Windows .cmd shim resolution via spawnSync with strict path
    validation. Removes shell:true injection risk, uses strict equality,
    and restores .cmd support with path injection guard.