mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 14:33:33 +08:00
fix: harden urgent install and gateguard patch
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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(', ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user