* 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
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>
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>
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>
* 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>
- 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>
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>
- Check spawnSync result and log warning on failure via stderr
- Restore osascript timeout to 5000ms, increase hook deadline to 10s
for sufficient headroom
- 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
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
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.
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).
- 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>
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.
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.
Invoke hook scripts directly via require() when they export a
run(rawInput) function, eliminating one Node.js process spawn per
hook invocation (~50-100ms).
Includes path traversal guard, timeouts, error logging, PR review
feedback, legacy hooks guard, normalized filePath, and restored
findProjectRoot config detection with package manager support.
- Add get_anomaly_attr() helper that handles both dict and object
anomalies. The SDK's send_message() returns dicts, so getattr()
was silently returning defaults -- critical blocking never triggered.
- Fix field name: "detail" -> "details" (matches SDK schema).
- Make fail-open/fail-closed configurable via INSAITS_FAIL_MODE env var
(defaults to "open" for backward compatibility).
- Include exception type name in fail-open log for diagnostics.
- Normalize severity comparison with .upper() for case-insensitive matching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cubic-dev-ai P2: dev_mode now defaults to "false" (strict mode).
Users opt in to dev mode by setting INSAITS_DEV_MODE=true.
cubic-dev-ai P2: Move null-status check above stdout/stderr writes
in wrapper so partial/corrupt output is never leaked. Pass through
original raw input on signal kill, matching the result.error path.
coderabbit major: Wrap insAItsMonitor() and send_message() in
try/except so SDK errors don't crash the hook. Logs warning and
exits 0 (fail-open) on exception.
coderabbit nitpick: write_audit now creates a new dict (enriched)
instead of mutating the caller's event dict.
coderabbit nitpick: Extract magic numbers to named constants:
MIN_CONTENT_LENGTH=10, MAX_SCAN_LENGTH=4000, DEFAULT_MODEL.
Also: added env var documentation to module docstring.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Defaults to true (no API key needed) but can be disabled by setting
INSAITS_DEV_MODE=false for production deployments with an API key.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
P1: Log non-ENOENT spawn errors (timeout, signal kill) to stderr
instead of silently exiting 0. Separate handling for result.error
and null result.status so users know when the security monitor
failed to run.
P1: Remove "async": true from hooks.json — async hooks run in the
background and cannot block tool execution. The security hook needs
to be synchronous so exit(2) actually prevents credential exposure
and other critical findings from proceeding.
P2: Remove dead tool_response/tool_result code from extract_content.
In a PreToolUse hook the tool hasn't executed yet, so tool_response
is never populated. Removed the variable and the unreachable branch
that appended its content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical fixes:
- Convert hook from PostToolUse to PreToolUse so exit(2) blocking works
- Change all python references to python3 for cross-platform compat
- Add insaits-security-wrapper.js to bridge run-with-flags.js to Python
Standard fixes:
- Wrap hook with run-with-flags.js so users can disable via
ECC_DISABLED_HOOKS="pre:insaits-security"
- Add "async": true to hooks.json entry
- Add type annotations to all function signatures (Dict, List, Tuple, Any)
- Replace all print() statements with logging module (stderr)
- Fix silent OSError swallow in write_audit — now logs warning
- Remove os.environ.setdefault('INSAITS_DEV_MODE') — pass dev_mode=True
through monitor constructor instead
- Update hooks/README.md: moved to PreToolUse table, "detects" not
"catches", clarify blocking vs non-blocking behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use local node_modules/.bin/biome binary instead of npx (~200-500ms savings)
- Change post-edit-format from `biome format --write` to `biome check --write`
(format + lint in one pass)
- Skip redundant biome check in quality-gate for JS/TS files already
handled by post-edit-format
- Fix quality-gate to use findProjectRoot instead of process.cwd()
- Export run() function from both hooks for direct invocation
- Update tests to match shared resolve-formatter module usage
- Add insaits-security-monitor.py: real-time AI security monitoring
hook that catches credential exposure, prompt injection,
hallucinations, and 20+ other anomaly types
- Update hooks.json with InsAIts PostToolUse entry
- Update hooks/README.md with InsAIts in PostToolUse table
- Add InsAIts MCP server entry to mcp-configs/mcp-servers.json
InsAIts (https://github.com/Nomadu27/InsAIts) is an open-source
runtime security layer for multi-agent AI. It runs 100% locally
and writes tamper-evident audit logs to .insaits_audit_session.jsonl.
Install: pip install insa-its
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: auto-start development servers in tmux instead of blocking
Replace blocking PreToolUse hook that used process.exit(2) with an auto-transform hook that:
- Detects development server commands
- Wraps them in tmux with directory-based session names
- Runs server detached so Claude Code is not blocked
- Provides confirmation message with log viewing instructions
Benefits:
- Development servers no longer block Claude Code execution
- Each project gets its own tmux session (allows multiple projects)
- Logs remain accessible via 'tmux capture-pane -t <session>'
- Non-blocking: if tmux unavailable, command still runs (graceful fallback)
Implementation:
- Created scripts/hooks/auto-tmux-dev.js with transform logic
- Updated hooks.json to reference the script instead of inline node command
- Applied same fix to cached plugin version (1.4.1) for immediate effect
* fix: resolve PR #344 code review issues in auto-tmux-dev.js
Critical fixes:
- Fix variable scope: declare 'input' before try block, not inside
- Fix shell injection: sanitize sessionName and escape cmd for shell
- Replace unused execFileSync import with spawnSync
Improvements:
- Add real Windows support using cmd /k window launcher
- Add tmux availability check with graceful fallback
- Update header comment to accurately describe platform support
Test coverage:
- Valid JSON input: transforms command for respective platform
- Invalid JSON: passes through raw data unchanged
- Unsupported tools: gracefully falls back to original command
- Shell metacharacters: sanitized in sessionName, escaped in cmd
* fix: correct cmd.exe escape sequence for double quotes on Windows
Use double-quote doubling ('""') instead of backslash-escape ('\\\") for cmd.exe syntax.
Backslash escaping is Unix convention and not recognized by cmd.exe. This fixes quoted
arguments in dev server commands on Windows (e.g., 'npm run dev --filter="my-app"').
* fix(hooks): scrub secrets and harden hook security
- Scrub common secret patterns (api_key, token, password, etc.) from
observation logs before persisting to JSONL (observe.sh)
- Auto-purge observation files older than 30 days (observe.sh)
- Strip embedded credentials from git remote URLs before saving to
projects.json (detect-project.sh)
- Add command prefix allowlist to runCommand — only git, node, npx,
which, where are permitted (utils.js)
- Sanitize CLAUDE_SESSION_ID in temp file paths to prevent path
traversal (suggest-compact.js)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(hooks): address review feedback from CodeRabbit and Cubic
- Reject shell command-chaining operators (;|&`) in runCommand, strip
quoted sections before checking to avoid false positives (utils.js)
- Remove command string from blocked error message to avoid leaking
secrets (utils.js)
- Fix Python regex quoting: switch outer shell string from double to
single quotes so regex compiles correctly (observe.sh)
- Add optional auth scheme match (Bearer, Basic) to secret scrubber
regex (observe.sh)
- Scope auto-purge to current project dir and match only archived
files (observations-*.jsonl), not live queue (observe.sh)
- Add second fallback after session ID sanitization to prevent empty
string (suggest-compact.js)
- Preserve backward compatibility when credential stripping changes
project hash — detect and migrate legacy directories
(detect-project.sh)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(hooks): block $() substitution, fix Bearer redaction, add security tests
- Add $ and \n to blocked shell metacharacters in runCommand to prevent
command substitution via $(cmd) and newline injection (utils.js)
- Make auth scheme group capturing so Bearer/Basic is preserved in
redacted output instead of being silently dropped (observe.sh)
- Add 10 unit tests covering runCommand allowlist blocking (rm, curl,
bash prefixes) and metacharacter rejection (;|&`$ chaining), plus
error message leak prevention (utils.test.js)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(hooks): scrub parse-error fallback, strengthen security tests
Address remaining reviewer feedback from CodeRabbit and Cubic:
- Scrub secrets in observe.sh parse-error fallback path (was writing
raw unsanitized input to observations file)
- Remove redundant re.IGNORECASE flag ((?i) inline flag already set)
- Add inline comment documenting quote-stripping limitation trade-off
- Fix misleading test name for error-output test
- Add 5 new security tests: single-quote passthrough, mixed
quoted+unquoted metacharacters, prefix boundary (no trailing space),
npx acceptance, and newline injection
- Improve existing quoted-metacharacter test to actually exercise
quote-stripping logic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): block $() and backtick inside quotes in runCommand
Shell evaluates $() and backticks inside double quotes, so checking
only the unquoted portion was insufficient. Now $ and ` are rejected
anywhere in the command string, while ; | & remain quote-aware.
Addresses CodeRabbit and Cubic review feedback on PR #348.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>