Commit Graph

112 Commits

  • fix: session-scoped state to prevent cross-session race
    Addresses reviewer feedback from @affaan-m:
    
    1. State keyed by CLAUDE_SESSION_ID / ECC_SESSION_ID
       - Falls back to pid-based isolation when env vars absent
       - State file: state-{sessionId}.json (was .session_state.json)
    
    2. Atomic write+rename semantics
       - Write to temp file, then fs.renameSync to final path
       - Prevents partial reads from concurrent hooks
    
    3. Bounded checked list (MAX_CHECKED_ENTRIES = 500)
       - Prunes to last 500 entries when cap exceeded
       - Stale session files auto-deleted after 1 hour
    
    9/9 tests pass.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • fix: gate MultiEdit tool alongside Edit/Write
    MultiEdit was bypassing the fact-forcing gate because only Edit and
    Write were checked. Now MultiEdit triggers the same edit gate (list
    importers, public API, data schemas) before allowing file modifications.
    
    Updated both the hook logic and hooks.json matcher pattern.
    
    Addresses coderabbit/greptile/cubic-dev: "MultiEdit bypasses gate"
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • fix: allow destructive bash retry after facts presented
    Destructive bash gate previously denied every invocation with no
    isChecked call, creating an infinite deny loop. Now gates per-command
    on first attempt and allows retry after the model presents the required
    facts (targets, rollback plan, user instruction).
    
    Addresses greptile P1: "Destructive bash gate permanently blocks"
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • fix: address P2 review feedback (coderabbitai, cubic-dev-ai)
    - GATEGUARD_STATE_DIR env var for test isolation (hook + tests)
    - Exit code assertions on all 9 tests (no vacuous passes)
    - Non-vacuous allow-path assertions (verify pass-through preserves input)
    - Robust newline-injection assertion
    - clearState() now reports errors instead of swallowing
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • fix: address P1 review feedback from greptile bot
    1. Use run-with-flags.js wrapper (supports ECC_HOOK_PROFILE, ECC_DISABLED_HOOKS)
    2. Add session timeout (30min inactivity = state reset, fixes "once ever" bug)
    3. Add 9 integration tests (deny/allow/timeout/sanitize/disable)
    
    Refactored hook to module.exports.run() pattern for direct require() by
    run-with-flags.js (~50-100ms faster per invocation).
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • feat(hooks,skills): add gateguard fact-forcing pre-action gate
    A PreToolUse hook that forces Claude to investigate before editing.
    Instead of self-evaluation ("are you sure?"), it demands concrete facts:
    importers, public API, data schemas, user instruction.
    
    A/B tested: +2.25 quality points (9.0 vs 6.75) across two independent tasks.
    
    - scripts/hooks/gateguard-fact-force.js — standalone Node.js hook
    - skills/gateguard/SKILL.md — skill documentation
    - hooks/hooks.json — PreToolUse entries for Edit|Write and Bash
    
    Full package with config: pip install gateguard-ai
    Repo: https://github.com/zunoworks/gateguard
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • 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>