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>
- 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
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
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>
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>
- 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>
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>