Feedback sent from the canvas only reached an agent through a live
/api/await long poll. When a turn ended with no await parked,
queueFeedback wrote the message to sessions.json and nothing ever
consumed it, so sending appeared to do nothing at all. The presence pill
made it worse: workingKeys had no expiry and the feedback handler never
broadcast presence, so it froze on "agent working" while nobody was
listening.
Delivery:
- Add the stop:plan-canvas-pending hook. It drains undelivered feedback
and blocks the Stop, handing the messages to the agent, so a canvas
message lands even when no await is running. Scoped to sessions under
cwd so parallel agents cannot swallow each other's feedback; set
ECC_PLAN_CANVAS_STOP_SCOPE=all to widen. Honors stop_hook_active and
fails open on every error path.
- run-with-flags.js did not await a hook's run(), so any async hook
silently degraded to pass-through. Fixed; plan-canvas-pending is the
only async hook today.
Presence and indicators:
- Presence is now ended/typing/thinking/listening/queued/waiting.
thinking and typing self-expire (90s/30s) and a 5s sweep pushes the
decay to an idle browser, so the pill can no longer stick.
- Broadcast presence when feedback is queued, and clear the activity
state when an agent reply lands.
- Add POST /api/session/:key/typing so agents can drive the indicator.
- Chat shows an animated dots bubble for thinking and typing, plus an
explicit note when a message is queued with nobody listening.
Respects prefers-reduced-motion.
- Send status reports what actually happened instead of always claiming
the agent will pick it up.
CLI and skill:
- Add `ecc-plan-canvas pending` and `typing <file> --state ...`.
- SKILL.md documents background await as the primary pattern and makes
replying in the canvas mandatory.
Tests: 6 new server cases covering queued presence, the typing endpoint,
state expiry and the sweep, plus a new hook suite covering delivery,
drain-once, stop_hook_active, cwd scoping and fail-open.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Preserve complete Stop-hook stdout through lifecycle wrappers, wait for queued output to flush before exiting, bound child output with a larger explicit buffer, and add end-to-end regressions for large, multibyte, dry-run, and failure cases.
* refactor(hooks): consolidate PostToolUse hooks into sync/async dispatchers
Replace 10 individual PostToolUse entries in hooks.json with two
consolidated dispatcher entries (post:dispatcher:sync /
post:dispatcher:async). The dispatcher's internal registry preserves
every hook ID, matcher, and profile, so ECC_DISABLED_HOOKS and
ECC_HOOK_PROFILE gating behave exactly as before.
Performance (Edit event, actual hooks.json commands spawned in
parallel like the harness does, median of 7 runs):
- Blocking hook latency: 81ms -> 49ms (~40% faster; 7 blocking
processes -> 1 sync dispatcher)
- Node processes per tool call: 10 -> 2 (7 blocking + 3 async
-> 1 sync + 1 async)
- observe-runner now runs in-process (~370ms) inside the async
dispatcher, which stays backgrounded (async: true, timeout 45s),
so it adds no user-facing latency.
Also:
- dashboard-web lists dispatcher-managed child hooks so the hook
inventory stays complete
- post-edit-console-warn refactored to export run() for in-process
dispatch while keeping standalone stdin behavior
- dispatcher stdin reading is multi-byte safe (StringDecoder) and
child hook exit codes propagate to the dispatcher exit code
* test(hooks): replace emoji literal with unicode escape for CI unicode safety check
* fix(hooks): adopt explicit cli() entrypoint and merge multi-hook stdout
Address Greptile review on #2494:
- Replace the non-standard 'require.main === undefined' guard with an
explicit exported cli(). The hooks.json bootstraps now call
require(s).cli(), so merely requiring the module (dashboard-web,
test runners, Jest, worker threads) can never trigger dispatch,
attach stdin listeners, or set process.exitCode.
- Replace last-writer-wins stdout with mergeHookStdout(): when several
hooks emit additionalContext envelopes they merge into a single
PostToolUse envelope; non-mergeable raw stdout keeps the last hook's
output and emits a stderr warning naming the dropped hook IDs, so
nothing is lost silently.
Also includes local formatter reformatting of the dispatcher and its
test file (no behavioral changes beyond the above).
* fix(hooks): keep post:bash:dispatcher phase reachable in minimal profile
The Greptile P1 premise was partially incorrect: sub-hooks without
explicit profiles default to standard,strict via parseProfiles()
(scripts/lib/hook-flags.js), so audit/cost logs never ran under the
minimal profile on main either — there is no user-visible regression.
However, main did spawn the bash dispatcher phase unconditionally and
let each sub-hook gate itself. Restore that semantic by opening the
outer registry gate to minimal,standard,strict so a future sub-hook
that opts into minimal is not silently blocked at the phase level.
Adds the previously missing minimal-profile async dry-run test.
* test(hooks): assert failing hook exit code propagates to real process status
Spawns the actual dispatcher subprocess with an injected failing hook
and asserts the OS-level exit status, stderr diagnostic, and suppressed
pass-through — closing the E2E gap CodeRabbit flagged on #2494.
* chore: retrigger CI (flaky windows powershell bootstrap test)
* feat: add Plan Canvas - browser annotate-and-approve review for plan artifacts
- scripts/plan-canvas.js CLI (open/await/end/stop/server; bin ecc-plan-canvas)
- loopback server + ECC-styled chrome + annotation SDK + zero-dep markdown renderer
- Approve/Request-changes verdicts wired to the /plan confirmation gate
- plan-canvas skill, /plan-canvas command, SessionStart hook surfacing open reviews
- shared scripts/lib/loopback-guard.js extracted from control-pane (API re-exported)
- 121 new tests incl. full-workflow E2E; registered in manifests, catalog, registry
Inspired by lavish-axi (https://github.com/kunchenguid/lavish-axi) by @kunchenguid;
original ECC-native implementation, not a port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(plan-canvas): invoke via ecc-plan-canvas bin so the skill works from any project
Skill/command referenced a cwd-relative `node scripts/plan-canvas.js`, unusable
outside the ECC root. Switch to the ecc-plan-canvas bin (and $CLAUDE_PLUGIN_ROOT
fallback) and align CLI next_step hints so an agent can run it as a skill in any repo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(plan-canvas): render Mermaid diagrams + ship Codex cross-harness surface
- markdown renderer emits <pre class="mermaid"> for ```mermaid blocks (source
entity-escaped so the browser decodes it for the renderer while blocking injection)
- artifact template loads a pinned Mermaid build only when a diagram is present,
themed to ECC dark, securityLevel strict, graceful offline fallback to source
(ECC_PLAN_CANVAS_MERMAID_URL overrides for a local mirror)
- skill teaches Mermaid-for-diagrams and states the CLI+JSON loop is harness-agnostic
- add .agents/skills/plan-canvas (Codex) with agents/openai.yaml interface manifest
- register in install-modules workflow-quality paths; docs updated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(plan-canvas): add demo screenshot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): sync yarn.lock with new bin; add contributor checklist
- yarn.lock records the ecc-plan-canvas bin so Yarn hardened-mode install no
longer wants to modify the lockfile on public PRs
- PR template + CONTRIBUTING gain a pre-push checklist covering the lockfile
trap and the full skill/command/CLI registration surfaces
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Haley Chen <2022hachen@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 17:12:48 -04:00
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>affaanDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(ci): resync lockfiles with package.json (eslint 10) + migrate yarn.lock to Yarn 4 format
package.json requires eslint@^10.6.0 but the committed locks pinned 9.39.2, so
npm ci aborted and Yarn 4 hardened mode rejected the stale v1-classic yarn.lock
(YN0028). Regenerate package-lock.json and rewrite yarn.lock in Yarn 4 (berry)
format so npm ci and immutable yarn installs both pass.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(ci): require clean probe exit for Windows shell/bash detection; add pyyaml dev dep
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor: consolidate duplicated hook-root resolver into shared resolveEccRoot() (#2368)
The inline node -e resolver blob was duplicated ~60x across hooks.json,
command docs, and translations. Each copy inlined the full ~700-char
plugin-root search using a spread over nested array literals
(p.join(d,'plugins',...s) over [['ecc'],...]), which breaks Windows hook
execution due to shell quoting (#2368).
Collapse every copy to a 250-char locator that loads the committed
resolve-ecc-root module and delegates to resolveEccRoot() — no spread, no
nested array literals, no escaped double quotes. The real search logic now
lives in one tested module. Also route session-start-bootstrap.js through
resolveEccRoot() instead of its own duplicated reimplementation, and fix
the auto-update.md 'marketplace' (singular) typo along the way.
Guard tests updated: discovery behavior is asserted against resolveEccRoot();
the inline is asserted to delegate and to contain no Windows-fragile
constructs.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(resolve-ecc-root): restore full env-unset discovery in inline resolver
Address Greptile review on #2410: when CLAUDE_PLUGIN_ROOT is unset the
delegating inline could only load the resolver module from ~/.claude,
returning ~/.claude without ever reaching the plugin/cache search. Restore
the old inline's discovery breadth (exact plugin roots + versioned cache)
Windows-safely (no spread, nested arrays, or escaped quotes), then delegate
the authoritative decision to resolveEccRoot(). Add regression tests for
plugin-subdir and versioned-cache bootstrap with env unset.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: affaan <affaan@itomarkets.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generate the inline hook root resolver with single-quoted JavaScript literals so Windows Git Bash does not choke on nested escaped double quotes before Node starts. Refresh hooks.json and add regression coverage for parsed hook commands and installed hook manifests.
Salvages the useful statusline/context monitor work from stale PR #1504 while preserving the current continuous-learning hook runner wiring.
Adds the metrics bridge, context monitor, statusline script, shared cost/session bridge utilities, and tests. Fixes the reviewed false loop-detection hash collision for non-file tools, avoids default-session cost inflation, sanitizes statusline task lookup, and records hook payload session IDs in cost-tracker.
- Add `minimal` profile so the security hook runs in all profiles
- Scope -n/--no-verify flag check to the detected subcommand region,
preventing false positives on chained commands (e.g. `git log -n 10`)
- Guard stdin listeners with `require.main === module` so require()
from run-with-flags.js does not register unnecessary listeners
- Verify subcommand token is preceded only by flags/flag-args after
"git", preventing misclassification of argument values as subcommands
- Add integration tests for block-no-verify hook
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline `npx block-no-verify@1.1.2` with a standalone Node.js
script routed through `run-with-flags.js`, matching every other hook.
Fixes two bugs:
1. npx inherits the project cwd and triggers EBADDEVENGINES in
pnpm-only projects that set devEngines.packageManager.onFail=error.
2. The hook bypassed run-with-flags.js so ECC_DISABLED_HOOKS had no
effect — the isHookEnabled() check never ran.
The new script replicates the full block-no-verify@1.1.2 detection
logic (--no-verify, -n shorthand for commit, core.hooksPath override)
with zero external dependencies.
Closes#1378
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>
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>
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
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(hooks): collapse multi-line commands in bash audit logs
Add gsub("\\n"; " ") to jq filters in bash audit log and cost-tracker
hooks so multi-line commands produce single-line log entries, preventing
breakage in downstream line-based parsing.
Fixes#734
* fix: forward stdin to downstream hooks using echo pattern
Addresses review feedback: PostToolUse hooks now preserve stdin
for subsequent hooks by echoing $INPUT back to stdout after
processing. Changed ; to && for proper error propagation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: make stdin passthrough unconditional and broaden secret redaction
- Use semicolons instead of && so printf passthrough always runs
even if jq fails
- Add || true after jq to prevent non-zero exit on parse errors
- Use printf '%s\n' instead of echo for safe binary passthrough
- Fix Authorization pattern to handle 'Bearer <token>' with space
- Add ASIA (STS temp credentials) alongside AKIA redaction
- Add GitHub token patterns (ghp_, gho_, ghs_, github_pat_)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use [: ]* instead of s* for Authorization whitespace matching
jq's ONIG regex engine interprets s* as literal 's' zero-or-more,
not \s* (whitespace). This caused 'Authorization: Bearer <token>'
to only redact 'Authorization:' and leak the actual token.
Using [: ]* avoids the JSON/jq double-escape issue entirely and
correctly matches both 'Authorization: Bearer xyz' and
'Authorization:xyz' patterns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.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>
* 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>
- Check spawnSync result and log warning on failure via stderr
- Restore osascript timeout to 5000ms, increase hook deadline to 10s
for sufficient headroom
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.
* perf(hooks): move post-edit-format and post-edit-typecheck to strict-only
These hooks fire synchronously on every Edit call with 15-30s timeouts
each. During multi-file refactors this adds 5-10 minutes of overhead.
Moving them from standard,strict to strict-only means they won't fire
in the default profile but are still available for users who want the
extra validation.
Fixes#735
* Also update OpenCode plugin to strict-only for format/typecheck
The OpenCode plugin had the same standard,strict profile for
post:edit:format and post:edit:typecheck, so OpenCode users on the
default profile would still get the per-edit overhead.
* 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.
Adds npx block-no-verify@1.1.2 as a PreToolUse Bash hook in hooks/hooks.json
and a beforeShellExecution hook in .cursor/hooks.json to prevent AI agents
from bypassing git hooks via the hook-bypass flag.
This closes the last enforcement gap in the ECC security stack — the bypass
flag silently skips pre-commit, commit-msg, and pre-push hooks.
Closes#648
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>