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
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().
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.
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
- 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
- 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
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
`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
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>
- 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>
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>
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>
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>