From 76b6e22b4d8b09e8a85abdf668ba1d1485ddbddc Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 14 Apr 2026 19:23:07 -0700 Subject: [PATCH 1/5] fix: unblock urgent install and gateguard regressions --- README.md | 10 +- README.zh-CN.md | 8 +- docs/ja-JP/README.md | 8 +- docs/ja-JP/skills/configure-ecc/SKILL.md | 2 +- docs/ko-KR/README.md | 10 +- docs/pt-BR/README.md | 10 +- docs/tr/README.md | 6 +- docs/zh-CN/README.md | 10 +- docs/zh-CN/skills/configure-ecc/SKILL.md | 2 +- docs/zh-TW/README.md | 8 +- scripts/harness-audit.js | 2 + scripts/hooks/gateguard-fact-force.js | 115 +++++++++++++++-- scripts/lib/install-targets/cursor-project.js | 4 +- skills/configure-ecc/SKILL.md | 2 +- tests/hooks/gateguard-fact-force.test.js | 119 ++++++++++++++++-- tests/lib/install-manifests.test.js | 4 +- tests/lib/install-targets.test.js | 8 +- tests/plugin-manifest.test.js | 45 +++++++ tests/scripts/harness-audit.test.js | 33 +++++ 19 files changed, 337 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index b06254f2..945e4388 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ Get up and running in under 2 minutes: /plugin marketplace add https://github.com/affaan-m/everything-claude-code # Install plugin -/plugin install ecc@ecc +/plugin install everything-claude-code ``` ### Step 2: Install Rules (Required) @@ -236,7 +236,7 @@ For manual install instructions see the README in the `rules/` folder. When copy # /plan "Add user authentication" # Check available commands -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` **That's it!** You now have access to 47 agents, 181 skills, and 79 legacy command shims. @@ -648,7 +648,7 @@ The easiest way to use this repo - install as a Claude Code plugin: /plugin marketplace add https://github.com/affaan-m/everything-claude-code # Install the plugin -/plugin install ecc@ecc +/plugin install everything-claude-code ``` Or add directly to your `~/.claude/settings.json`: @@ -664,7 +664,7 @@ Or add directly to your `~/.claude/settings.json`: } }, "enabledPlugins": { - "ecc@ecc": true + "everything-claude-code@everything-claude-code": true } } ``` @@ -882,7 +882,7 @@ Slash forms below are shown because they are still the fastest familiar entrypoi How do I check which agents/commands are installed? ```bash -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` This shows all available agents, commands, and skills from the plugin. diff --git a/README.zh-CN.md b/README.zh-CN.md index 7e7e9553..75e10a50 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -102,7 +102,7 @@ /plugin marketplace add affaan-m/everything-claude-code # 安装插件 -/plugin install ecc@ecc +/plugin install everything-claude-code ``` ### 第二步:安装规则(必需) @@ -159,7 +159,7 @@ npx ecc-install typescript # /plan "添加用户认证" # 查看可用命令 -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` **完成!** 你现在可以使用 47 个代理、181 个技能和 79 个命令。 @@ -546,7 +546,7 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho /plugin marketplace add affaan-m/everything-claude-code # 安装插件 -/plugin install ecc@ecc +/plugin install everything-claude-code ``` 或直接添加到你的 `~/.claude/settings.json`: @@ -562,7 +562,7 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho } }, "enabledPlugins": { - "ecc@ecc": true + "everything-claude-code@everything-claude-code": true } } ``` diff --git a/docs/ja-JP/README.md b/docs/ja-JP/README.md index d7f1d5dd..ebd6a46e 100644 --- a/docs/ja-JP/README.md +++ b/docs/ja-JP/README.md @@ -110,7 +110,7 @@ /plugin marketplace add https://github.com/affaan-m/everything-claude-code # プラグインをインストール -/plugin install ecc@ecc +/plugin install everything-claude-code ``` ### ステップ2:ルールをインストール(必須) @@ -140,7 +140,7 @@ cp -r everything-claude-code/rules/golang/* ~/.claude/rules/ # /plan "ユーザー認証を追加" # 利用可能なコマンドを確認 -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` **完了です!** これで13のエージェント、43のスキル、31のコマンドにアクセスできます。 @@ -427,7 +427,7 @@ Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded /plugin marketplace add https://github.com/affaan-m/everything-claude-code # プラグインをインストール -/plugin install ecc@ecc +/plugin install everything-claude-code ``` または、`~/.claude/settings.json` に直接追加: @@ -443,7 +443,7 @@ Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded } }, "enabledPlugins": { - "ecc@ecc": true + "everything-claude-code@everything-claude-code": true } } ``` diff --git a/docs/ja-JP/skills/configure-ecc/SKILL.md b/docs/ja-JP/skills/configure-ecc/SKILL.md index 6dd670b4..1097c31c 100644 --- a/docs/ja-JP/skills/configure-ecc/SKILL.md +++ b/docs/ja-JP/skills/configure-ecc/SKILL.md @@ -17,7 +17,7 @@ Everything Claude Code プロジェクトのインタラクティブなステッ ## 前提条件 このスキルは起動前に Claude Code からアクセス可能である必要があります。ブートストラップには2つの方法があります: -1. **プラグイン経由**: `/plugin install ecc@ecc` — プラグインがこのスキルを自動的にロードします +1. **プラグイン経由**: `/plugin install everything-claude-code` — プラグインがこのスキルを自動的にロードします 2. **手動**: このスキルのみを `~/.claude/skills/configure-ecc/SKILL.md` にコピーし、"configure ecc" と言って起動します --- diff --git a/docs/ko-KR/README.md b/docs/ko-KR/README.md index 73df584d..6ee320c7 100644 --- a/docs/ko-KR/README.md +++ b/docs/ko-KR/README.md @@ -115,7 +115,7 @@ /plugin marketplace add https://github.com/affaan-m/everything-claude-code # 플러그인 설치 -/plugin install ecc@ecc +/plugin install everything-claude-code ``` ### 2단계: 룰 설치 (필수) @@ -147,7 +147,7 @@ cd everything-claude-code # /plan "사용자 인증 추가" # 사용 가능한 커맨드 확인 -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` **끝!** 이제 16개 에이전트, 65개 스킬, 40개 커맨드를 사용할 수 있습니다. @@ -359,7 +359,7 @@ Claude Code v2.1+는 설치된 플러그인의 `hooks/hooks.json`을 **자동으 /plugin marketplace add https://github.com/affaan-m/everything-claude-code # 플러그인 설치 -/plugin install ecc@ecc +/plugin install everything-claude-code ``` 또는 `~/.claude/settings.json`에 직접 추가: @@ -375,7 +375,7 @@ Claude Code v2.1+는 설치된 플러그인의 `hooks/hooks.json`을 **자동으 } }, "enabledPlugins": { - "ecc@ecc": true + "everything-claude-code@everything-claude-code": true } } ``` @@ -535,7 +535,7 @@ rules/ 설치된 에이전트/커맨드 확인은 어떻게 하나요? ```bash -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` 플러그인에서 사용할 수 있는 모든 에이전트, 커맨드, 스킬을 보여줍니다. diff --git a/docs/pt-BR/README.md b/docs/pt-BR/README.md index 9e1c9145..e2ca54dd 100644 --- a/docs/pt-BR/README.md +++ b/docs/pt-BR/README.md @@ -124,7 +124,7 @@ Comece em menos de 2 minutos: /plugin marketplace add https://github.com/affaan-m/everything-claude-code # Instalar plugin -/plugin install ecc@ecc +/plugin install everything-claude-code ``` ### Passo 2: Instalar as Regras (Obrigatório) @@ -167,7 +167,7 @@ npx ecc-install typescript # /plan "Adicionar autenticação de usuário" # Verificar comandos disponíveis -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` **Pronto!** Você agora tem acesso a 28 agentes, 116 skills e 59 comandos. @@ -313,7 +313,7 @@ claude --version /plugin marketplace add https://github.com/affaan-m/everything-claude-code # Instalar o plugin -/plugin install ecc@ecc +/plugin install everything-claude-code ``` Ou adicione diretamente ao seu `~/.claude/settings.json`: @@ -329,7 +329,7 @@ Ou adicione diretamente ao seu `~/.claude/settings.json`: } }, "enabledPlugins": { - "ecc@ecc": true + "everything-claude-code@everything-claude-code": true } } ``` @@ -452,7 +452,7 @@ Regras são diretrizes sempre seguidas, organizadas em `common/` (agnóstico à Como verificar quais agentes/comandos estão instalados? ```bash -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` diff --git a/docs/tr/README.md b/docs/tr/README.md index 812e172e..adfc6ab7 100644 --- a/docs/tr/README.md +++ b/docs/tr/README.md @@ -125,7 +125,7 @@ Bu repository yalnızca ham kodu içerir. Rehberler her şeyi açıklıyor. /plugin marketplace add https://github.com/affaan-m/everything-claude-code # Plugin'i kur -/plugin install ecc@ecc +/plugin install everything-claude-code ``` ### Adım 2: Rule'ları Kurun (Gerekli) @@ -170,7 +170,7 @@ Manuel kurulum talimatları için `rules/` klasöründeki README'ye bakın. # /plan "Kullanıcı kimlik doğrulaması ekle" # Mevcut command'ları kontrol edin -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` **Bu kadar!** Artık 28 agent, 116 skill ve 59 command'a erişiminiz var. @@ -352,7 +352,7 @@ Nereden başlayacağınızdan emin değil misiniz? Bu hızlı referansı kullan Hangi agent/command'ların kurulu olduğunu nasıl kontrol ederim? ```bash -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` Bu, plugin'den mevcut tüm agent'ları, command'ları ve skill'leri gösterir. diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 851e9697..99ccdeef 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -161,7 +161,7 @@ /plugin marketplace add https://github.com/affaan-m/everything-claude-code # Install plugin -/plugin install ecc@ecc +/plugin install everything-claude-code ``` ### 步骤 2:安装规则(必需) @@ -206,7 +206,7 @@ npx ecc-install typescript # /plan "Add user authentication" # Check available commands -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` **搞定!** 你现在可以使用 47 个智能体、181 项技能和 79 个命令了。 @@ -585,7 +585,7 @@ Claude Code v2.1+ **会自动加载** 任何已安装插件中的 `hooks/hooks.j /plugin marketplace add https://github.com/affaan-m/everything-claude-code # Install the plugin -/plugin install ecc@ecc +/plugin install everything-claude-code ``` 或者直接添加到您的 `~/.claude/settings.json`: @@ -601,7 +601,7 @@ Claude Code v2.1+ **会自动加载** 任何已安装插件中的 `hooks/hooks.j } }, "enabledPlugins": { - "ecc@ecc": true + "everything-claude-code@everything-claude-code": true } } ``` @@ -793,7 +793,7 @@ rules/ 如何检查已安装的代理/命令? ```bash -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` 这会显示插件中所有可用的代理、命令和技能。 diff --git a/docs/zh-CN/skills/configure-ecc/SKILL.md b/docs/zh-CN/skills/configure-ecc/SKILL.md index 369c5a3b..54b8f6df 100644 --- a/docs/zh-CN/skills/configure-ecc/SKILL.md +++ b/docs/zh-CN/skills/configure-ecc/SKILL.md @@ -19,7 +19,7 @@ origin: ECC 此技能必须在激活前对 Claude Code 可访问。有两种引导方式: -1. **通过插件**: `/plugin install ecc@ecc` — 插件会自动加载此技能 +1. **通过插件**: `/plugin install everything-claude-code` — 插件会自动加载此技能 2. **手动**: 仅将此技能复制到 `~/.claude/skills/configure-ecc/SKILL.md`,然后通过说 "configure ecc" 激活 *** diff --git a/docs/zh-TW/README.md b/docs/zh-TW/README.md index bb0e3360..7a8b3225 100644 --- a/docs/zh-TW/README.md +++ b/docs/zh-TW/README.md @@ -70,7 +70,7 @@ /plugin marketplace add https://github.com/affaan-m/everything-claude-code # 安裝外掛程式 -/plugin install ecc@ecc +/plugin install everything-claude-code ``` ### 第二步:安裝規則(必需) @@ -95,7 +95,7 @@ cp -r everything-claude-code/rules/* ~/.claude/rules/ # /plan "新增使用者認證" # 查看可用指令 -/plugin list ecc@ecc +/plugin list everything-claude-code@everything-claude-code ``` **完成!** 您現在使用 15+ 代理程式、30+ 技能和 20+ 指令。 @@ -270,7 +270,7 @@ everything-claude-code/ /plugin marketplace add https://github.com/affaan-m/everything-claude-code # 安裝外掛程式 -/plugin install ecc@ecc +/plugin install everything-claude-code ``` 或直接新增到您的 `~/.claude/settings.json`: @@ -286,7 +286,7 @@ everything-claude-code/ } }, "enabledPlugins": { - "ecc@ecc": true + "everything-claude-code@everything-claude-code": true } } ``` diff --git a/scripts/harness-audit.js b/scripts/harness-audit.js index 6180eb48..4eb6b7b2 100644 --- a/scripts/harness-audit.js +++ b/scripts/harness-audit.js @@ -196,7 +196,9 @@ function findPluginInstall(rootDir) { ]; const candidateRoots = [ path.join(rootDir, '.claude', 'plugins'), + path.join(rootDir, '.claude', 'plugins', 'marketplaces'), homeDir && path.join(homeDir, '.claude', 'plugins'), + homeDir && path.join(homeDir, '.claude', 'plugins', 'marketplaces'), ].filter(Boolean); const candidates = candidateRoots.flatMap((pluginsDir) => pluginDirs.flatMap((pluginDir) => [ diff --git a/scripts/hooks/gateguard-fact-force.js b/scripts/hooks/gateguard-fact-force.js index b8a0fcc8..754fff80 100644 --- a/scripts/hooks/gateguard-fact-force.js +++ b/scripts/hooks/gateguard-fact-force.js @@ -27,13 +27,12 @@ const fs = require('fs'); const path = require('path'); // Session state — scoped per session to avoid cross-session races. -// Uses CLAUDE_SESSION_ID (set by Claude Code) or falls back to PID-based isolation. const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard'); -const SESSION_ID = process.env.CLAUDE_SESSION_ID || process.env.ECC_SESSION_ID || `pid-${process.ppid || process.pid}`; -const STATE_FILE = path.join(STATE_DIR, `state-${SESSION_ID.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`); +let activeStateFile = null; // State expires after 30 minutes of inactivity const SESSION_TIMEOUT_MS = 30 * 60 * 1000; +const READ_HEARTBEAT_MS = 60 * 1000; // Maximum checked entries to prevent unbounded growth const MAX_CHECKED_ENTRIES = 500; @@ -44,13 +43,65 @@ const DESTRUCTIVE_BASH = /\b(rm\s+-rf|git\s+reset\s+--hard|git\s+checkout\s+--|g // --- State management (per-session, atomic writes, bounded) --- +function sanitizeSessionKey(value) { + const raw = String(value || '').trim(); + if (!raw) { + return ''; + } + + const sanitized = raw.replace(/[^a-zA-Z0-9_-]/g, '_'); + if (sanitized && sanitized.length <= 64) { + return sanitized; + } + + return ''; +} + +function hashSessionKey(prefix, value) { + return `${prefix}-${crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, 24)}`; +} + +function resolveSessionKey(data) { + const directCandidates = [ + data && data.session_id, + data && data.sessionId, + data && data.session && data.session.id, + process.env.CLAUDE_SESSION_ID, + process.env.ECC_SESSION_ID, + ]; + + for (const candidate of directCandidates) { + const sanitized = sanitizeSessionKey(candidate); + if (sanitized) { + return sanitized; + } + } + + const transcriptPath = (data && (data.transcript_path || data.transcriptPath)) || process.env.CLAUDE_TRANSCRIPT_PATH; + if (transcriptPath && String(transcriptPath).trim()) { + return hashSessionKey('tx', path.resolve(String(transcriptPath).trim())); + } + + const projectFingerprint = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + return hashSessionKey('proj', path.resolve(projectFingerprint)); +} + +function getStateFile(data) { + if (!activeStateFile) { + const sessionKey = resolveSessionKey(data); + activeStateFile = path.join(STATE_DIR, `state-${sessionKey}.json`); + } + return activeStateFile; +} + function loadState() { + const stateFile = getStateFile(); try { - if (fs.existsSync(STATE_FILE)) { - const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); + if (fs.existsSync(stateFile)) { + const state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); const lastActive = state.last_active || 0; if (Date.now() - lastActive > SESSION_TIMEOUT_MS) { - try { fs.unlinkSync(STATE_FILE); } catch (_) { /* ignore */ } + try { fs.unlinkSync(stateFile); } catch (_) { /* ignore */ } return { checked: [], last_active: Date.now() }; } return state; @@ -75,15 +126,30 @@ function pruneCheckedEntries(checked) { } function saveState(state) { + const stateFile = getStateFile(); + let tmpFile = null; try { state.last_active = Date.now(); state.checked = pruneCheckedEntries(state.checked); fs.mkdirSync(STATE_DIR, { recursive: true }); // Atomic write: temp file + rename prevents partial reads - const tmpFile = STATE_FILE + '.tmp.' + process.pid; + tmpFile = stateFile + '.tmp.' + process.pid; fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), 'utf8'); - fs.renameSync(tmpFile, STATE_FILE); - } catch (_) { /* ignore */ } + try { + fs.renameSync(tmpFile, stateFile); + } catch (error) { + if (error && (error.code === 'EEXIST' || error.code === 'EPERM')) { + try { fs.unlinkSync(stateFile); } catch (_) { /* ignore */ } + fs.renameSync(tmpFile, stateFile); + } else { + throw error; + } + } + } catch (_) { + if (tmpFile) { + try { fs.unlinkSync(tmpFile); } catch (_) { /* ignore */ } + } + } } function markChecked(key) { @@ -97,7 +163,9 @@ function markChecked(key) { function isChecked(key) { const state = loadState(); const found = state.checked.includes(key); - saveState(state); + if (found && Date.now() - (state.last_active || 0) > READ_HEARTBEAT_MS) { + saveState(state); + } return found; } @@ -124,6 +192,24 @@ function sanitizePath(filePath) { return filePath.replace(/[\x00-\x1f\x7f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, ' ').trim().slice(0, 500); } +function normalizeForMatch(value) { + return String(value || '').replace(/\\/g, '/').toLowerCase(); +} + +function isClaudeSettingsPath(filePath) { + const normalized = normalizeForMatch(filePath); + return /(^|\/)\.claude\/settings(?:\.[^/]+)?\.json$/.test(normalized); +} + +function isReadOnlyGitIntrospection(command) { + const trimmed = String(command || '').trim(); + if (!trimmed || /[;&|><`$()]/.test(trimmed)) { + return false; + } + + return /^(git\s+(status|diff|log|show|branch(?:\s+--show-current)?|rev-parse(?:\s+--abbrev-ref\s+head)?)(\s|$))/i.test(trimmed); +} + // --- Gate messages --- function editGateMsg(filePath) { @@ -205,6 +291,8 @@ function run(rawInput) { } catch (_) { return rawInput; // allow on parse error } + activeStateFile = null; + getStateFile(data); const rawToolName = data.tool_name || ''; const toolInput = data.tool_input || {}; @@ -214,7 +302,7 @@ function run(rawInput) { if (toolName === 'Edit' || toolName === 'Write') { const filePath = toolInput.file_path || ''; - if (!filePath) { + if (!filePath || isClaudeSettingsPath(filePath)) { return rawInput; // allow } @@ -230,7 +318,7 @@ function run(rawInput) { const edits = toolInput.edits || []; for (const edit of edits) { const filePath = edit.file_path || ''; - if (filePath && !isChecked(filePath)) { + if (filePath && !isClaudeSettingsPath(filePath) && !isChecked(filePath)) { markChecked(filePath); return denyResult(editGateMsg(filePath)); } @@ -240,6 +328,9 @@ function run(rawInput) { if (toolName === 'Bash') { const command = toolInput.command || ''; + if (isReadOnlyGitIntrospection(command)) { + return rawInput; + } if (DESTRUCTIVE_BASH.test(command)) { // Gate destructive commands on first attempt; allow retry after facts presented diff --git a/scripts/lib/install-targets/cursor-project.js b/scripts/lib/install-targets/cursor-project.js index 03d19b1d..ceba1cb7 100644 --- a/scripts/lib/install-targets/cursor-project.js +++ b/scripts/lib/install-targets/cursor-project.js @@ -53,11 +53,11 @@ module.exports = createInstallTargetAdapter({ })); }).sort((left, right) => { const getPriority = value => { - if (value === 'rules') { + if (value === '.cursor') { return 0; } - if (value === '.cursor') { + if (value === 'rules') { return 1; } diff --git a/skills/configure-ecc/SKILL.md b/skills/configure-ecc/SKILL.md index 62d2aac1..b5966d36 100644 --- a/skills/configure-ecc/SKILL.md +++ b/skills/configure-ecc/SKILL.md @@ -18,7 +18,7 @@ An interactive, step-by-step installation wizard for the Everything Claude Code ## Prerequisites This skill must be accessible to Claude Code before activation. Two ways to bootstrap: -1. **Via Plugin**: `/plugin install ecc@ecc` — the plugin loads this skill automatically +1. **Via Plugin**: `/plugin install everything-claude-code` — the plugin loads this skill automatically 2. **Manual**: Copy only this skill to `~/.claude/skills/configure-ecc/SKILL.md`, then activate by saying "configure ecc" --- diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index 2f0837c3..a7af8cda 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -14,6 +14,7 @@ const stateDir = externalStateDir || fs.mkdtempSync(path.join(tmpRoot, 'gateguar // Use a fixed session ID so test process and spawned hook process share the same state file const TEST_SESSION_ID = 'gateguard-test-session'; const stateFile = path.join(stateDir, `state-${TEST_SESSION_ID}.json`); +const READ_HEARTBEAT_MS = 60 * 1000; function test(name, fn) { try { @@ -29,11 +30,15 @@ function test(name, fn) { function clearState() { try { - if (fs.existsSync(stateFile)) { - fs.unlinkSync(stateFile); + if (fs.existsSync(stateDir)) { + for (const entry of fs.readdirSync(stateDir)) { + if (entry.startsWith('state-') && entry.endsWith('.json')) { + fs.unlinkSync(path.join(stateDir, entry)); + } + } } } catch (err) { - console.error(` [clearState] failed to remove ${stateFile}: ${err.message}`); + console.error(` [clearState] failed to remove state files in ${stateDir}: ${err.message}`); } } @@ -363,18 +368,45 @@ function runTests() { } })) passed++; else failed++; - // --- Test 12: reads refresh active session state --- + // --- Test 12: hot-path reads do not rewrite state within heartbeat --- clearState(); - if (test('touches last_active on read so active sessions do not age out', () => { - const staleButActive = Date.now() - (29 * 60 * 1000); + if (test('does not rewrite state on hot-path reads within heartbeat window', () => { + const recentlyActive = Date.now() - (READ_HEARTBEAT_MS - 10 * 1000); + writeState({ + checked: ['/src/keep-alive.js'], + last_active: recentlyActive + }); + + const beforeStat = fs.statSync(stateFile); + const before = JSON.parse(fs.readFileSync(stateFile, 'utf8')); + assert.strictEqual(before.last_active, recentlyActive, 'seed state should use the expected timestamp'); + + const result = runHook({ + tool_name: 'Edit', + tool_input: { file_path: '/src/keep-alive.js', old_string: 'a', new_string: 'b' } + }); + const output = parseOutput(result.stdout); + assert.ok(output, 'should produce valid JSON output'); + if (output.hookSpecificOutput) { + assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny', + 'already-checked file should still be allowed'); + } + + const afterStat = fs.statSync(stateFile); + const after = JSON.parse(fs.readFileSync(stateFile, 'utf8')); + assert.strictEqual(after.last_active, recentlyActive, 'read should not touch last_active within heartbeat'); + assert.strictEqual(afterStat.mtimeMs, beforeStat.mtimeMs, 'read should not rewrite the state file within heartbeat'); + })) passed++; else failed++; + + // --- Test 13: reads refresh stale active state after heartbeat --- + clearState(); + if (test('refreshes last_active after heartbeat elapses', () => { + const staleButActive = Date.now() - (READ_HEARTBEAT_MS + 5 * 1000); writeState({ checked: ['/src/keep-alive.js'], last_active: staleButActive }); - const before = JSON.parse(fs.readFileSync(stateFile, 'utf8')); - assert.strictEqual(before.last_active, staleButActive, 'seed state should use the expected timestamp'); - const result = runHook({ tool_name: 'Edit', tool_input: { file_path: '/src/keep-alive.js', old_string: 'a', new_string: 'b' } @@ -387,10 +419,10 @@ function runTests() { } const after = JSON.parse(fs.readFileSync(stateFile, 'utf8')); - assert.ok(after.last_active > staleButActive, 'successful reads should refresh last_active'); + assert.ok(after.last_active > staleButActive, 'read should refresh last_active after heartbeat'); })) passed++; else failed++; - // --- Test 13: pruning preserves routine bash gate marker --- + // --- Test 14: pruning preserves routine bash gate marker --- clearState(); if (test('preserves __bash_session__ when pruning oversized state', () => { const checked = ['__bash_session__']; @@ -419,6 +451,71 @@ function runTests() { assert.ok(persisted.checked.length <= 500, 'pruned state should still honor the checked-entry cap'); })) passed++; else failed++; + // --- Test 15: raw input session IDs provide stable retry state without env vars --- + clearState(); + if (test('uses raw input session_id when hook env vars are missing', () => { + const input = { + session_id: 'raw-session-1234', + tool_name: 'Bash', + tool_input: { command: 'ls -la' } + }; + + const first = runBashHook(input, { + CLAUDE_SESSION_ID: '', + ECC_SESSION_ID: '', + }); + const firstOutput = parseOutput(first.stdout); + assert.strictEqual(firstOutput.hookSpecificOutput.permissionDecision, 'deny'); + + const second = runBashHook(input, { + CLAUDE_SESSION_ID: '', + ECC_SESSION_ID: '', + }); + const secondOutput = parseOutput(second.stdout); + if (secondOutput.hookSpecificOutput) { + assert.notStrictEqual(secondOutput.hookSpecificOutput.permissionDecision, 'deny', + 'retry should be allowed when raw session_id is stable'); + } else { + assert.strictEqual(secondOutput.tool_name, 'Bash'); + } + })) passed++; else failed++; + + // --- Test 16: allows Claude settings edits so the hook can be disabled safely --- + clearState(); + if (test('allows edits to .claude/settings.json without gating', () => { + const input = { + tool_name: 'Edit', + tool_input: { file_path: '/workspace/app/.claude/settings.json', old_string: '{}', new_string: '{"hooks":[]}' } + }; + const result = runHook(input); + const output = parseOutput(result.stdout); + assert.ok(output, 'should produce valid JSON output'); + if (output.hookSpecificOutput) { + assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny', + 'settings edits must not be blocked by gateguard'); + } else { + assert.strictEqual(output.tool_name, 'Edit'); + } + })) passed++; else failed++; + + // --- Test 17: allows read-only git introspection without first-bash gating --- + clearState(); + if (test('allows read-only git status without first-bash gating', () => { + const input = { + tool_name: 'Bash', + tool_input: { command: 'git status --short' } + }; + const result = runBashHook(input); + const output = parseOutput(result.stdout); + assert.ok(output, 'should produce valid JSON output'); + if (output.hookSpecificOutput) { + assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny', + 'read-only git introspection should not be blocked'); + } else { + assert.strictEqual(output.tool_name, 'Bash'); + } + })) passed++; else failed++; + // Cleanup only the temp directory created by this test file. if (!externalStateDir) { try { diff --git a/tests/lib/install-manifests.test.js b/tests/lib/install-manifests.test.js index ed0d7c73..652567ef 100644 --- a/tests/lib/install-manifests.test.js +++ b/tests/lib/install-manifests.test.js @@ -124,11 +124,11 @@ function runTests() { ); assert.ok( plan.operations.some(operation => ( - operation.sourceRelativePath === 'rules/common/agents.md' + operation.sourceRelativePath === '.cursor/rules/common-agents.md' && operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc') && operation.strategy === 'flatten-copy' )), - 'Should produce Cursor .mdc rules while preferring rules-core over duplicate platform copies' + 'Should produce Cursor .mdc rules while preferring native Cursor platform copies over duplicate rules-core files' ); })) passed++; else failed++; diff --git a/tests/lib/install-targets.test.js b/tests/lib/install-targets.test.js index d34fbcfa..f26ce3f5 100644 --- a/tests/lib/install-targets.test.js +++ b/tests/lib/install-targets.test.js @@ -94,14 +94,14 @@ function runTests() { normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json' )); const preserved = plan.operations.find(operation => ( - normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md' + normalizedRelativePath(operation.sourceRelativePath) === '.cursor/rules/common-coding-style.md' )); assert.ok(hooksJson, 'Should preserve non-rule Cursor platform config files'); assert.strictEqual(hooksJson.strategy, 'preserve-relative-path'); assert.strictEqual(hooksJson.destinationPath, path.join(projectRoot, '.cursor', 'hooks.json')); - assert.ok(preserved, 'Should include flattened rules scaffold operations'); + assert.ok(preserved, 'Should include flattened Cursor rule scaffold operations'); assert.strictEqual(preserved.strategy, 'flatten-copy'); assert.strictEqual( preserved.destinationPath, @@ -236,8 +236,8 @@ function runTests() { assert.strictEqual(commonAgentsDestinations.length, 1, 'Should keep only one common-agents.mdc operation'); assert.strictEqual( normalizedRelativePath(commonAgentsDestinations[0].sourceRelativePath), - 'rules/common/agents.md', - 'Should prefer rules-core when cursor platform rules would collide' + '.cursor/rules/common-agents.md', + 'Should prefer native .cursor/rules content when cursor platform rules would collide' ); })) passed++; else failed++; diff --git a/tests/plugin-manifest.test.js b/tests/plugin-manifest.test.js index 98674968..68bb4118 100644 --- a/tests/plugin-manifest.test.js +++ b/tests/plugin-manifest.test.js @@ -79,6 +79,28 @@ function assertSafeRepoRelativePath(relativePath, label) { ); } +function collectMarkdownFiles(rootPath) { + if (!fs.existsSync(rootPath)) { + return []; + } + + const stat = fs.statSync(rootPath); + if (stat.isFile()) { + return rootPath.endsWith('.md') ? [rootPath] : []; + } + + const files = []; + for (const entry of fs.readdirSync(rootPath, { withFileTypes: true })) { + const nextPath = path.join(rootPath, entry.name); + if (entry.isDirectory()) { + files.push(...collectMarkdownFiles(nextPath)); + } else if (entry.isFile() && nextPath.endsWith('.md')) { + files.push(nextPath); + } + } + return files; +} + const rootPackage = loadJsonObject(packageJsonPath, 'package.json'); const packageLock = loadJsonObject(packageLockPath, 'package-lock.json'); const opencodePackageLock = loadJsonObject(opencodePackageLockPath, '.opencode/package-lock.json'); @@ -454,6 +476,29 @@ test('README version row matches package.json', () => { assert.strictEqual(match[1], expectedVersion); }); +test('user-facing docs do not reference deprecated ecc@ecc plugin identifier', () => { + const markdownFiles = [ + path.join(repoRoot, 'README.md'), + path.join(repoRoot, 'README.zh-CN.md'), + path.join(repoRoot, 'skills', 'configure-ecc', 'SKILL.md'), + ...collectMarkdownFiles(path.join(repoRoot, 'docs')), + ]; + + const offenders = []; + for (const filePath of markdownFiles) { + const source = fs.readFileSync(filePath, 'utf8'); + if (source.includes('ecc@ecc')) { + offenders.push(path.relative(repoRoot, filePath)); + } + } + + assert.deepStrictEqual( + offenders, + [], + `Deprecated ecc@ecc identifier must not appear in user-facing docs: ${offenders.join(', ')}`, + ); +}); + test('docs/zh-CN/README.md version row matches package.json', () => { const readme = fs.readFileSync(zhCnReadmePath, 'utf8'); const match = readme.match(/^\| \*\*版本\*\* \| 插件 \| 插件 \| 参考配置 \| ([0-9][0-9.]*) \|$/m); diff --git a/tests/scripts/harness-audit.test.js b/tests/scripts/harness-audit.test.js index 7019fd8b..25a8890b 100644 --- a/tests/scripts/harness-audit.test.js +++ b/tests/scripts/harness-audit.test.js @@ -132,6 +132,39 @@ function runTests() { } })) passed++; else failed++; + if (test('detects marketplace-installed Claude plugins under marketplaces/', () => { + const homeDir = createTempDir('harness-audit-marketplace-home-'); + const projectRoot = createTempDir('harness-audit-marketplace-project-'); + + try { + fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin'), { recursive: true }); + fs.writeFileSync( + path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin', 'plugin.json'), + JSON.stringify({ name: 'everything-claude-code' }, null, 2) + ); + + fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'tests'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true }); + fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), '# Project instructions\n'); + fs.writeFileSync(path.join(projectRoot, '.mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2)); + fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env\n'); + fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n'); + fs.writeFileSync(path.join(projectRoot, 'tests', 'app.test.js'), 'test placeholder\n'); + fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.json'), JSON.stringify({ hooks: ['PreToolUse'] }, null, 2)); + fs.writeFileSync( + path.join(projectRoot, 'package.json'), + JSON.stringify({ name: 'consumer-project', scripts: { test: 'node tests/app.test.js' } }, null, 2) + ); + + const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir })); + assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && check.pass)); + } finally { + cleanup(homeDir); + cleanup(projectRoot); + } + })) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); } From 3be24a570497c957286c7b2a0f786b992f699690 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 14 Apr 2026 19:26:24 -0700 Subject: [PATCH 2/5] fix: restore urgent PR CI health --- agents/a11y-architect.md | 1 + scripts/hooks/gateguard-fact-force.js | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/agents/a11y-architect.md b/agents/a11y-architect.md index 7ef2e517..531d43ff 100644 --- a/agents/a11y-architect.md +++ b/agents/a11y-architect.md @@ -1,6 +1,7 @@ --- name: a11y-architect description: Accessibility Architect specializing in WCAG 2.2 compliance for Web and Native platforms. Use PROACTIVELY when designing UI components, establishing design systems, or auditing code for inclusive user experiences. +model: sonnet tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] --- diff --git a/scripts/hooks/gateguard-fact-force.js b/scripts/hooks/gateguard-fact-force.js index 754fff80..1123f97c 100644 --- a/scripts/hooks/gateguard-fact-force.js +++ b/scripts/hooks/gateguard-fact-force.js @@ -189,7 +189,14 @@ function isChecked(key) { function sanitizePath(filePath) { // Strip control chars (including null), bidi overrides, and newlines - return filePath.replace(/[\x00-\x1f\x7f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, ' ').trim().slice(0, 500); + let sanitized = ''; + for (const char of String(filePath || '')) { + const code = char.codePointAt(0); + const isAsciiControl = code <= 0x1f || code === 0x7f; + const isBidiOverride = (code >= 0x200e && code <= 0x200f) || (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069); + sanitized += (isAsciiControl || isBidiOverride) ? ' ' : char; + } + return sanitized.trim().slice(0, 500); } function normalizeForMatch(value) { From e5225db006d9319b98c593572cf462ac3981ec5d Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 14 Apr 2026 19:31:23 -0700 Subject: [PATCH 3/5] docs: sync catalog counts on urgent fix branch --- AGENTS.md | 6 +++--- README.md | 10 +++++----- README.zh-CN.md | 2 +- docs/zh-CN/AGENTS.md | 6 +++--- docs/zh-CN/README.md | 10 +++++----- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fd7b24cf..e25ea6e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — Agent Instructions -This is a **production-ready AI coding plugin** providing 47 specialized agents, 181 skills, 79 commands, and automated hook workflows for software development. +This is a **production-ready AI coding plugin** providing 48 specialized agents, 183 skills, 79 commands, and automated hook workflows for software development. **Version:** 1.10.0 @@ -145,8 +145,8 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat ## Project Structure ``` -agents/ — 47 specialized subagents -skills/ — 181 workflow skills and domain knowledge +agents/ — 48 specialized subagents +skills/ — 183 workflow skills and domain knowledge commands/ — 79 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) diff --git a/README.md b/README.md index 945e4388..fe21d176 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ For manual install instructions see the README in the `rules/` folder. When copy /plugin list everything-claude-code@everything-claude-code ``` -**That's it!** You now have access to 47 agents, 181 skills, and 79 legacy command shims. +**That's it!** You now have access to 48 agents, 183 skills, and 79 legacy command shims. ### Dashboard GUI @@ -1197,9 +1197,9 @@ The configuration is automatically detected from `.opencode/opencode.json`. | Feature | Claude Code | OpenCode | Status | |---------|-------------|----------|--------| -| Agents | PASS: 47 agents | PASS: 12 agents | **Claude Code leads** | +| Agents | PASS: 48 agents | PASS: 12 agents | **Claude Code leads** | | Commands | PASS: 79 commands | PASS: 31 commands | **Claude Code leads** | -| Skills | PASS: 181 skills | PASS: 37 skills | **Claude Code leads** | +| Skills | PASS: 183 skills | PASS: 37 skills | **Claude Code leads** | | Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** | | Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** | | MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** | @@ -1306,9 +1306,9 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e | Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|------------|------------|-----------|----------| -| **Agents** | 47 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | +| **Agents** | 48 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | | **Commands** | 79 | Shared | Instruction-based | 31 | -| **Skills** | 181 | Shared | 10 (native format) | 37 | +| **Skills** | 183 | Shared | 10 (native format) | 37 | | **Hook Events** | 8 types | 15 types | None yet | 11 types | | **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | | **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | diff --git a/README.zh-CN.md b/README.zh-CN.md index 75e10a50..ae0b6780 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -162,7 +162,7 @@ npx ecc-install typescript /plugin list everything-claude-code@everything-claude-code ``` -**完成!** 你现在可以使用 47 个代理、181 个技能和 79 个命令。 +**完成!** 你现在可以使用 48 个代理、183 个技能和 79 个命令。 ### multi-* 命令需要额外配置 diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index 0bad9c1c..e94456c2 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 47 个专业代理、181 项技能、79 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 48 个专业代理、183 项技能、79 条命令以及自动化钩子工作流,用于软件开发。 **版本:** 1.10.0 @@ -146,8 +146,8 @@ ## 项目结构 ``` -agents/ — 47 个专业子代理 -skills/ — 181 个工作流技能和领域知识 +agents/ — 48 个专业子代理 +skills/ — 183 个工作流技能和领域知识 commands/ — 79 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 99ccdeef..183d773c 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -209,7 +209,7 @@ npx ecc-install typescript /plugin list everything-claude-code@everything-claude-code ``` -**搞定!** 你现在可以使用 47 个智能体、181 项技能和 79 个命令了。 +**搞定!** 你现在可以使用 48 个智能体、183 项技能和 79 个命令了。 *** @@ -1094,9 +1094,9 @@ opencode | 功能特性 | Claude Code | OpenCode | 状态 | |---------|-------------|----------|--------| -| 智能体 | PASS: 47 个 | PASS: 12 个 | **Claude Code 领先** | +| 智能体 | PASS: 48 个 | PASS: 12 个 | **Claude Code 领先** | | 命令 | PASS: 79 个 | PASS: 31 个 | **Claude Code 领先** | -| 技能 | PASS: 181 项 | PASS: 37 项 | **Claude Code 领先** | +| 技能 | PASS: 183 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1206,9 +1206,9 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 | 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|------------|------------|-----------|----------| -| **智能体** | 47 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | +| **智能体** | 48 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | | **命令** | 79 | 共享 | 基于指令 | 31 | -| **技能** | 181 | 共享 | 10 (原生格式) | 37 | +| **技能** | 183 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | From 8776c4f8f3a89aacdefaa49e46a1caebb0480442 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 14 Apr 2026 19:44:08 -0700 Subject: [PATCH 4/5] fix: harden urgent install and gateguard patch --- README.md | 2 + README.zh-CN.md | 6 +- scripts/harness-audit.js | 3 +- scripts/hooks/gateguard-fact-force.js | 48 +++++++++++++-- tests/hooks/gateguard-fact-force.test.js | 72 ++++++++++++++++++----- tests/plugin-manifest.test.js | 28 ++++++++- tests/scripts/harness-audit.test.js | 74 +++++++++++++++++++++++- 7 files changed, 206 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index fe21d176..23be9dc6 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,8 @@ Get up and running in under 2 minutes: /plugin install everything-claude-code ``` +> Install-name clarification: older posts may still show `ecc@ecc`. That shorthand is deprecated. Anthropic marketplace/plugin installs are keyed by a canonical plugin identifier, so ECC standardized on `everything-claude-code@everything-claude-code` to keep the listing name, install path, `/plugin list`, and repo docs aligned instead of maintaining two different public names for the same plugin. + ### Step 2: Install Rules (Required) > WARNING: **Important:** Claude Code plugins cannot distribute `rules` automatically. Install them manually: diff --git a/README.zh-CN.md b/README.zh-CN.md index ae0b6780..b6557064 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -99,12 +99,14 @@ ```bash # 添加市场 -/plugin marketplace add affaan-m/everything-claude-code +/plugin marketplace add https://github.com/affaan-m/everything-claude-code # 安装插件 /plugin install everything-claude-code ``` +> 安装名称说明:较早的帖子里可能还会出现 `ecc@ecc`。那个旧缩写现在已经废弃。Anthropic 的 marketplace/plugin 安装是按规范化插件标识符寻址的,因此 ECC 统一为 `everything-claude-code@everything-claude-code`,这样市场条目、安装命令、`/plugin list` 输出和仓库文档都使用同一个公开名称,不再出现两个名字指向同一插件的混乱。 + ### 第二步:安装规则(必需) > WARNING: **重要提示:** Claude Code 插件无法自动分发 `rules`,需要手动安装: @@ -543,7 +545,7 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho ```bash # 将此仓库添加为市场 -/plugin marketplace add affaan-m/everything-claude-code +/plugin marketplace add https://github.com/affaan-m/everything-claude-code # 安装插件 /plugin install everything-claude-code diff --git a/scripts/harness-audit.js b/scripts/harness-audit.js index 4eb6b7b2..648e3802 100644 --- a/scripts/harness-audit.js +++ b/scripts/harness-audit.js @@ -1,6 +1,7 @@ #!/usr/bin/env node const fs = require('fs'); +const os = require('os'); const path = require('path'); const CATEGORIES = [ @@ -187,7 +188,7 @@ function detectTargetMode(rootDir) { } function findPluginInstall(rootDir) { - const homeDir = process.env.HOME || ''; + const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir() || ''; const pluginDirs = [ 'ecc', 'ecc@ecc', diff --git a/scripts/hooks/gateguard-fact-force.js b/scripts/hooks/gateguard-fact-force.js index 1123f97c..9886efc5 100644 --- a/scripts/hooks/gateguard-fact-force.js +++ b/scripts/hooks/gateguard-fact-force.js @@ -54,7 +54,7 @@ function sanitizeSessionKey(value) { return sanitized; } - return ''; + return hashSessionKey('sid', raw); } function hashSessionKey(prefix, value) { @@ -177,9 +177,13 @@ function isChecked(key) { for (const f of files) { if (!f.startsWith('state-') || !f.endsWith('.json')) continue; const fp = path.join(STATE_DIR, f); - const stat = fs.statSync(fp); - if (now - stat.mtimeMs > SESSION_TIMEOUT_MS * 2) { - fs.unlinkSync(fp); + try { + const stat = fs.statSync(fp); + if (now - stat.mtimeMs > SESSION_TIMEOUT_MS * 2) { + fs.unlinkSync(fp); + } + } catch (_) { + // Ignore files that disappear between readdir/stat/unlink. } } } catch (_) { /* ignore */ } @@ -210,11 +214,43 @@ function isClaudeSettingsPath(filePath) { function isReadOnlyGitIntrospection(command) { const trimmed = String(command || '').trim(); - if (!trimmed || /[;&|><`$()]/.test(trimmed)) { + if (!trimmed || /[\r\n;&|><`$()]/.test(trimmed)) { return false; } - return /^(git\s+(status|diff|log|show|branch(?:\s+--show-current)?|rev-parse(?:\s+--abbrev-ref\s+head)?)(\s|$))/i.test(trimmed); + const tokens = trimmed.split(/\s+/); + if (tokens[0] !== 'git' || tokens.length < 2) { + return false; + } + + const subcommand = tokens[1].toLowerCase(); + const args = tokens.slice(2); + + if (subcommand === 'status') { + return args.every(arg => ['--porcelain', '--short', '--branch'].includes(arg)); + } + + if (subcommand === 'diff') { + return args.length <= 1 && args.every(arg => ['--name-only', '--name-status'].includes(arg)); + } + + if (subcommand === 'log') { + return args.every(arg => arg === '--oneline' || /^--max-count=\d+$/.test(arg)); + } + + if (subcommand === 'show') { + return args.length === 1 && !args[0].startsWith('--') && /^[a-zA-Z0-9._:/-]+$/.test(args[0]); + } + + if (subcommand === 'branch') { + return args.length === 1 && args[0] === '--show-current'; + } + + if (subcommand === 'rev-parse') { + return args.length === 2 && args[0] === '--abbrev-ref' && /^head$/i.test(args[1]); + } + + return false; } // --- Gate messages --- diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index a7af8cda..b1925fe2 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -10,7 +10,8 @@ const { spawnSync } = require('child_process'); const runner = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'run-with-flags.js'); const externalStateDir = process.env.GATEGUARD_STATE_DIR; const tmpRoot = process.env.TMPDIR || process.env.TEMP || process.env.TMP || '/tmp'; -const stateDir = externalStateDir || fs.mkdtempSync(path.join(tmpRoot, 'gateguard-test-')); +const baseStateDir = externalStateDir || tmpRoot; +const stateDir = fs.mkdtempSync(path.join(baseStateDir, 'gateguard-test-')); // Use a fixed session ID so test process and spawned hook process share the same state file const TEST_SESSION_ID = 'gateguard-test-session'; const stateFile = path.join(stateDir, `state-${TEST_SESSION_ID}.json`); @@ -31,12 +32,9 @@ function test(name, fn) { function clearState() { try { if (fs.existsSync(stateDir)) { - for (const entry of fs.readdirSync(stateDir)) { - if (entry.startsWith('state-') && entry.endsWith('.json')) { - fs.unlinkSync(path.join(stateDir, entry)); - } - } + fs.rmSync(stateDir, { recursive: true, force: true }); } + fs.mkdirSync(stateDir, { recursive: true }); } catch (err) { console.error(` [clearState] failed to remove state files in ${stateDir}: ${err.message}`); } @@ -516,15 +514,61 @@ function runTests() { } })) passed++; else failed++; - // Cleanup only the temp directory created by this test file. - if (!externalStateDir) { - try { - if (fs.existsSync(stateDir)) { - fs.rmSync(stateDir, { recursive: true, force: true }); - } - } catch (err) { - console.error(` [cleanup] failed to remove ${stateDir}: ${err.message}`); + // --- Test 18: rejects mutating git commands that only share a prefix --- + clearState(); + if (test('does not treat mutating git commands as read-only introspection', () => { + const input = { + tool_name: 'Bash', + tool_input: { command: 'git status && rm -rf /tmp/demo' } + }; + const result = runBashHook(input); + const output = parseOutput(result.stdout); + assert.ok(output, 'should produce valid JSON output'); + assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny'); + assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('current instruction')); + })) passed++; else failed++; + + // --- Test 19: long raw session IDs hash instead of collapsing to project fallback --- + clearState(); + if (test('uses a stable hash for long raw session ids', () => { + const longSessionId = `session-${'x'.repeat(120)}`; + const input = { + session_id: longSessionId, + tool_name: 'Bash', + tool_input: { command: 'ls -la' } + }; + + const first = runBashHook(input, { + CLAUDE_SESSION_ID: '', + ECC_SESSION_ID: '', + }); + const firstOutput = parseOutput(first.stdout); + assert.strictEqual(firstOutput.hookSpecificOutput.permissionDecision, 'deny'); + + const stateFiles = fs.readdirSync(stateDir).filter(entry => entry.startsWith('state-') && entry.endsWith('.json')); + assert.strictEqual(stateFiles.length, 1, 'long raw session id should still produce a dedicated state file'); + assert.ok(/state-sid-[a-f0-9]{24}\.json$/.test(stateFiles[0]), 'long raw session ids should hash to a bounded sid-* key'); + + const second = runBashHook(input, { + CLAUDE_SESSION_ID: '', + ECC_SESSION_ID: '', + }); + const secondOutput = parseOutput(second.stdout); + if (secondOutput.hookSpecificOutput) { + assert.notStrictEqual(secondOutput.hookSpecificOutput.permissionDecision, 'deny', + 'retry should be allowed when long raw session_id is stable'); + } else { + assert.strictEqual(secondOutput.tool_name, 'Bash'); } + })) passed++; else failed++; + + // Cleanup only the temp directory created by this test file. + try { + if (fs.existsSync(stateDir)) { + fs.rmSync(stateDir, { recursive: true, force: true }); + } + } catch (err) { + console.error(` [cleanup] failed to remove ${stateDir}: ${err.message}`); } console.log(`\n ${passed} passed, ${failed} failed\n`); diff --git a/tests/plugin-manifest.test.js b/tests/plugin-manifest.test.js index 68bb4118..069afb21 100644 --- a/tests/plugin-manifest.test.js +++ b/tests/plugin-manifest.test.js @@ -476,7 +476,7 @@ test('README version row matches package.json', () => { assert.strictEqual(match[1], expectedVersion); }); -test('user-facing docs do not reference deprecated ecc@ecc plugin identifier', () => { +test('user-facing docs do not use deprecated ecc@ecc install commands', () => { const markdownFiles = [ path.join(repoRoot, 'README.md'), path.join(repoRoot, 'README.zh-CN.md'), @@ -487,7 +487,7 @@ test('user-facing docs do not reference deprecated ecc@ecc plugin identifier', ( const offenders = []; for (const filePath of markdownFiles) { const source = fs.readFileSync(filePath, 'utf8'); - if (source.includes('ecc@ecc')) { + if (/\/plugin\s+(install|list)\s+ecc@ecc\b/.test(source)) { offenders.push(path.relative(repoRoot, filePath)); } } @@ -495,7 +495,29 @@ test('user-facing docs do not reference deprecated ecc@ecc plugin identifier', ( assert.deepStrictEqual( offenders, [], - `Deprecated ecc@ecc identifier must not appear in user-facing docs: ${offenders.join(', ')}`, + `Deprecated ecc@ecc install commands must not appear in user-facing docs: ${offenders.join(', ')}`, + ); +}); + +test('user-facing docs do not use the legacy non-URL marketplace add form', () => { + const markdownFiles = [ + path.join(repoRoot, 'README.md'), + path.join(repoRoot, 'README.zh-CN.md'), + ...collectMarkdownFiles(path.join(repoRoot, 'docs')), + ]; + + const offenders = []; + for (const filePath of markdownFiles) { + const source = fs.readFileSync(filePath, 'utf8'); + if (source.includes('/plugin marketplace add affaan-m/everything-claude-code')) { + offenders.push(path.relative(repoRoot, filePath)); + } + } + + assert.deepStrictEqual( + offenders, + [], + `Legacy non-URL marketplace add form must not appear in user-facing docs: ${offenders.join(', ')}`, ); }); diff --git a/tests/scripts/harness-audit.test.js b/tests/scripts/harness-audit.test.js index 25a8890b..a114aff3 100644 --- a/tests/scripts/harness-audit.test.js +++ b/tests/scripts/harness-audit.test.js @@ -19,11 +19,13 @@ function cleanup(dirPath) { } function run(args = [], options = {}) { + const userProfile = options.userProfile || options.homeDir || process.env.USERPROFILE; const stdout = execFileSync('node', [SCRIPT, ...args], { cwd: options.cwd || path.join(__dirname, '..', '..'), env: { ...process.env, HOME: options.homeDir || process.env.HOME, + USERPROFILE: userProfile, }, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], @@ -132,7 +134,7 @@ function runTests() { } })) passed++; else failed++; - if (test('detects marketplace-installed Claude plugins under marketplaces/', () => { + if (test('detects marketplace-installed Claude plugins under home marketplaces/', () => { const homeDir = createTempDir('harness-audit-marketplace-home-'); const projectRoot = createTempDir('harness-audit-marketplace-project-'); @@ -165,6 +167,76 @@ function runTests() { } })) passed++; else failed++; + if (test('detects marketplace-installed Claude plugins under project marketplaces/', () => { + const homeDir = createTempDir('harness-audit-marketplace-home-'); + const projectRoot = createTempDir('harness-audit-marketplace-project-'); + + try { + fs.mkdirSync(path.join(projectRoot, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin', 'plugin.json'), + JSON.stringify({ name: 'everything-claude-code' }, null, 2) + ); + + fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'tests'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true }); + fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), '# Project instructions\n'); + fs.writeFileSync(path.join(projectRoot, '.mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2)); + fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env\n'); + fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n'); + fs.writeFileSync(path.join(projectRoot, 'tests', 'app.test.js'), 'test placeholder\n'); + fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.json'), JSON.stringify({ hooks: ['PreToolUse'] }, null, 2)); + fs.writeFileSync( + path.join(projectRoot, 'package.json'), + JSON.stringify({ name: 'consumer-project', scripts: { test: 'node tests/app.test.js' } }, null, 2) + ); + + const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir })); + assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && check.pass)); + } finally { + cleanup(homeDir); + cleanup(projectRoot); + } + })) passed++; else failed++; + + if (test('detects marketplace-installed Claude plugins from USERPROFILE fallback on Windows-style setups', () => { + const homeDir = createTempDir('harness-audit-marketplace-home-'); + const projectRoot = createTempDir('harness-audit-marketplace-project-'); + + try { + fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin'), { recursive: true }); + fs.writeFileSync( + path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin', 'plugin.json'), + JSON.stringify({ name: 'everything-claude-code' }, null, 2) + ); + + fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'tests'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true }); + fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), '# Project instructions\n'); + fs.writeFileSync(path.join(projectRoot, '.mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2)); + fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env\n'); + fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n'); + fs.writeFileSync(path.join(projectRoot, 'tests', 'app.test.js'), 'test placeholder\n'); + fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.json'), JSON.stringify({ hooks: ['PreToolUse'] }, null, 2)); + fs.writeFileSync( + path.join(projectRoot, 'package.json'), + JSON.stringify({ name: 'consumer-project', scripts: { test: 'node tests/app.test.js' } }, null, 2) + ); + + const parsed = JSON.parse(run(['repo', '--format', 'json'], { + cwd: projectRoot, + homeDir: '', + userProfile: homeDir, + })); + assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && check.pass)); + } finally { + cleanup(homeDir); + cleanup(projectRoot); + } + })) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); } From c54b44edf3c7df94582be66bdd6ab3ea1ca27912 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 14 Apr 2026 20:03:57 -0700 Subject: [PATCH 5/5] test: fix harness audit env fallback --- tests/scripts/harness-audit.test.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/scripts/harness-audit.test.js b/tests/scripts/harness-audit.test.js index a114aff3..ae22e754 100644 --- a/tests/scripts/harness-audit.test.js +++ b/tests/scripts/harness-audit.test.js @@ -20,13 +20,20 @@ function cleanup(dirPath) { function run(args = [], options = {}) { const userProfile = options.userProfile || options.homeDir || process.env.USERPROFILE; + const env = { + ...process.env, + USERPROFILE: userProfile, + }; + + if (Object.prototype.hasOwnProperty.call(options, 'homeDir')) { + env.HOME = options.homeDir; + } else { + env.HOME = process.env.HOME; + } + const stdout = execFileSync('node', [SCRIPT, ...args], { cwd: options.cwd || path.join(__dirname, '..', '..'), - env: { - ...process.env, - HOME: options.homeDir || process.env.HOME, - USERPROFILE: userProfile, - }, + env, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000,