435 Commits

  • fix: install native Cursor hook and MCP config (#1543)
    * fix: install native cursor hook and MCP config
    
    * fix: avoid false healthy stdio mcp probes
  • Merge pull request #1495 from ratorin/fix/session-end-transcript-path-isolation
    fix(hooks): isolate session-end.js filename using transcript_path UUID (#1494)
  • fix(hooks): wrap SessionStart summary with stale-replay guard (#1536)
    The SessionStart hook injects the most recent *-session.tmp as
    additionalContext labelled only with 'Previous session summary:'.
    After a /compact boundary, the model frequently re-executes stale
    slash-skill invocations it finds inside that summary, re-running
    ARGUMENTS-bearing skills (e.g. /fw-task-new, /fw-raise-pr) with the
    last ARGUMENTS they saw.
    
    Observed on claude-opus-4-7 with ECC v1.9.0 on a firmware project:
    after compaction resume, the model spontaneously re-enters the prior
    skill with stale ARGUMENTS, duplicating GitHub issues, Notion tasks,
    and branches for work that is already merged.
    
    ECC cannot fix Claude Code's skill-state replay across compactions,
    but it can stop amplifying it. Wrap the injected summary in an
    explicit HISTORICAL REFERENCE ONLY preamble with a STALE-BY-DEFAULT
    contract and delimit the block with BEGIN/END markers so the model
    treats everything inside as frozen reference material.
    
    Tests: update the two hooks.test.js cases that asserted on the old
    'Previous session summary' literal to assert on the new guard
    preamble, the STALE-BY-DEFAULT contract, and both delimiters. 219/219
    tests pass locally.
    
    Tracked at: #1534
  • fix(gateguard): rewrite routineBashMsg to use fact-presentation pattern (#1531)
    * fix(gateguard): rewrite routineBashMsg to use fact-presentation pattern
    
    The imperative 'Quote user's instruction verbatim. Then retry.' phrasing
    triggers Claude Code's runtime anti-prompt-injection filter, deadlocking
    the first Bash call of every session. The sibling gates (edit, write,
    destructive) use multi-point fact-list framing that the runtime accepts.
    
    Align routineBashMsg with that pattern to restore the gate's intended
    behavior without changing run(), state schema, or any public API.
    
    Closes #1530
    
    * docs(gateguard): sync SKILL.md routine gate spec with new message format
    
    CodeRabbit flagged that skills/gateguard/SKILL.md still described the
    pre-fix imperative message. Update the Routine Bash Gate section to
    match the numbered fact-list format used by the new routineBashMsg().
  • fix(scripts): resolve claude.cmd on Windows by enabling shell for spawn (#1471)
    Fixes #1469.
    
    On Windows the `claude` binary installed via `npm i -g @anthropic-ai/claude-code`
    is `claude.cmd`, and Node's spawn() cannot resolve .cmd wrappers via PATH
    without shell: true. The call failed with `spawn claude ENOENT` and claw.js
    returned an error string to the caller.
    
    Mirrors the fix pattern applied in PR #1456 for the MCP health-check hook.
    'claude' is a hardcoded literal (not user input), so enabling shell on Windows
    only is safe.
  • review: broaden CLAUDE_TRANSCRIPT_PATH fallback to cover missing/empty JSON fields
    Previously the env fallback ran only when JSON.parse threw. If stdin was valid
    JSON but omitted transcript_path or provided a non-string/empty value, the
    script dropped to the getSessionIdShort() fallback path, re-introducing the
    collision this PR targets.
    
    Validate the parsed transcript_path and apply the env-var fallback for any
    unusable value, not just malformed JSON. Matches coderabbit's outside-diff
    suggestion and keeps both input-source paths equivalent.
    
    Refs #1494
  • review: apply sanitizeSessionId to UUID shortId, fix test comment
    - Route the transcript-derived shortId through sanitizeSessionId so the
      fallback and transcript branches remain byte-for-byte equivalent for any
      non-UUID session IDs that still land in CLAUDE_SESSION_ID (greptile P1).
    - Clarify the inline comment in the first regression test: clearing
      CLAUDE_SESSION_ID exercises the transcript_path branch, not the
      getSessionIdShort() fallback (coderabbit P2).
    
    Refs #1494
  • review: address P1/P2 bot feedback on shortId derivation
    - Use last-8 chars of transcript UUID instead of first-8, matching
      getSessionIdShort()'s .slice(-8) convention. Same session now produces the
      same filename whether shortId comes from CLAUDE_SESSION_ID or transcript_path,
      so existing .tmp files are not orphaned on upgrade.
    - Normalize extracted hex prefix to lowercase to avoid case-driven filename
      divergence from sanitizeSessionId()'s lowercase output.
    - Explicitly clear CLAUDE_SESSION_ID in the first regression test so the env
      leak from parent test runs cannot hide the fallback path.
    - Add regression tests for the lowercase-normalization path and for the case
      where CLAUDE_SESSION_ID and transcript_path refer to the same UUID (backward
      compat guarantee).
    
    Refs #1494
  • fix(hooks): isolate session-end.js filename using transcript_path UUID
    When session-end.js runs and CLAUDE_SESSION_ID is unset, getSessionIdShort()
    falls back to the project/worktree name. If any other Stop-hook in the chain
    spawns a claude subprocess (e.g. an AI-summary generator using 'claude -p'),
    the subprocess also fires the full Stop chain and writes to the same project-
    name-based filename, clobbering the parent's valid session summary with a
    summary of the summarization prompt itself.
    
    Fix: when stdin JSON (or CLAUDE_TRANSCRIPT_PATH) provides a transcript_path,
    extract the first 8 hex chars of the session UUID from the filename and use
    that as shortId. Falls back to the original getSessionIdShort() when no
    transcript_path is available, so existing behavior is preserved for all
    callers that do not set it.
    
    Adds a regression test in tests/hooks/hooks.test.js.
    
    Refs #1494
  • Merge pull request #1445 from affaan-m/fix/plugin-installed-hook-root-resolution
    fix: resolve plugin-installed hook root on marketplace installs
  • fix(harness-audit): detect ECC plugin under marketplaces/ subdirectory
    `findPluginInstall()` in `scripts/harness-audit.js` scans two candidate
    roots:
    
      {rootDir}/.claude/plugins/
      {HOME}/.claude/plugins/
    
    Current Claude Code marketplace installs live one directory deeper:
    
      {HOME}/.claude/plugins/marketplaces/{ecc,everything-claude-code}/...
    
    As a result, running `node scripts/harness-audit.js repo` on any
    consumer project reports `consumer-plugin-install: false` even when ECC
    is fully installed via marketplace, costing 4 points from Tool Coverage.
    
    Add the `marketplaces/` intermediate directory to `candidateRoots` so
    both legacy and current install layouts are recognized. The change is
    purely additive: existing candidate paths still resolve, and the new
    ones only match when the marketplace layout is present.
    
    Reproduction:
      1. Install ECC via Claude Code plugin marketplace
      2. cd into any consumer project
      3. node ~/.claude/plugins/marketplaces/everything-claude-code/scripts/harness-audit.js repo
      4. Observe consumer-plugin-install=false despite a working install
  • Merge pull request #1367 from ozoz5/feat/gateguard
    feat(hooks,skills): add gateguard fact-forcing pre-action gate
  • fix: 5 bugs + 2 tests from 3-agent deep bughunt
    Bugs fixed:
    - B1: JS gate messages still said "cat one real record" -> redacted/synthetic
    - B2: Destructive bash key used 200-char truncation (collision bypass) -> SHA256 hash
    - B3: sanitizePath only stripped \n\r -> now strips null bytes, bidi overrides, all control chars
    - B4: Tool name matching was case-sensitive (latent bypass) -> lookup map normalization
    - B5: SKILL.md Gate Types missing MultiEdit -> added with explanation
    
    Tests added:
    - T1: MultiEdit gate denies first unchecked file (CRITICAL - was untested)
    - T2: MultiEdit allows after all files gated
    
    11/11 tests pass.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • fix: cubic-dev-ai round 2 — 3 issues across SKILL.md + pruning
    P1: Gate message asked for raw production data records — changed to
        "redacted or synthetic values" to prevent sensitive data exfiltration
    
    P2: SKILL.md description now includes MultiEdit (was missing after
        MultiEdit gate was added in previous commit)
    
    P2: Session key pruning now caps __prefixed keys at 50 to prevent
        unbounded growth even in theoretical edge cases
    
    9/9 tests pass.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • fix: remove unnecessary disk I/O + fix test cleanup
    - isChecked() no longer calls saveState() — read-only operation
      should not write to disk (was causing 3x writes per tool call)
    - Test cleanup uses fs.rmSync(recursive) instead of fs.rmdirSync
      which failed with ENOTEMPTY when .tmp files remained
    
    9/9 tests pass.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • fix: P1 test state-file PID mismatch + P2 session key eviction
    P1 (cubic-dev-ai): Test process PID differs from spawned hook PID,
    so test was seeding/clearing wrong state file. Fix: pass fixed
    CLAUDE_SESSION_ID='gateguard-test-session' to spawned hooks.
    
    P2 (cubic-dev-ai): Pruning checked array could evict __bash_session__
    and other session keys, causing gates to re-fire mid-session. Fix:
    preserve __prefixed keys during pruning, only evict file-path entries.
    
    9/9 tests pass.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • fix: MultiEdit gate bypass — handle edits[].file_path correctly
    P1 bug reported by greptile-apps: MultiEdit uses toolInput.edits[].file_path,
    not toolInput.file_path. The gate was silently allowing all MultiEdit calls.
    
    Fix: separate MultiEdit into its own branch that iterates edits array
    and gates on the first unchecked file_path.
    
    9/9 tests pass.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • 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>
  • Merge pull request #1384 from KeWang0622/fix/lint-md028-eqeqeq
    fix: resolve markdownlint MD028 + ESLint eqeqeq lint failures
  • Merge pull request #1385 from KeWang0622/fix/block-no-verify-hook
    fix: route block-no-verify hook through run-with-flags.js
  • fix: harden release surface version and packaging sync (#1388)
    * fix: keep ecc release surfaces version-synced
    
    * fix: keep lockfile release version in sync
    
    * fix: remove release version drift from locks and tests
    
    * fix: keep root release metadata version-synced
    
    * fix: keep codex marketplace metadata version-synced
    
    * fix: gate release workflows on full metadata sync
    
    * fix: ship all versioned release metadata
    
    * fix: harden manual release path
    
    * fix: keep localized release docs version-synced
    
    * fix: sync install architecture version examples
    
    * test: cover shipped plugin metadata in npm pack
    
    * fix: verify final npm payload in release script
    
    * fix: ship opencode lockfile in npm package
    
    * docs: sync localized release highlights
    
    * fix: stabilize windows ci portability
    
    * fix: tighten release script version sync
    
    * fix: prefer repo-relative hook file paths
    
    * fix: make npm pack test shell-safe on windows
  • fix: detach ecc2 background session runners (#1387)
    * fix: detach ecc2 background session runners
    
    * fix: stabilize windows ci portability
    
    * fix: persist detached runner startup stderr
    
    * fix: prefer repo-relative hook file paths
    
    * fix: make npm pack test shell-safe on windows
  • fix: address PR review comments on block-no-verify hook
    - 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>
  • fix: route block-no-verify hook through run-with-flags.js
    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
  • fix: resolve markdownlint MD028 and ESLint eqeqeq warnings
    Fix two lint issues that cause `npm run lint` to exit non-zero:
    
    1. README.md (MD028): Two consecutive blockquotes separated by a bare
       blank line. Markdownlint treats this as one blockquote with an
       illegal blank line inside. Replace the blank line with a `>`
       continuation so both paragraphs stay in the same blockquote.
    
    2. session-activity-tracker.js (eqeqeq): Three instances of `== null`
       replaced with explicit `=== null || === undefined` guards to satisfy
       the repo's `eqeqeq: warn` ESLint rule.
    
    Closes #1366
  • 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>