From 2d508d3cffcbe1cfa2eaae6d72e46b1972f7a06a Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 09:45:13 +0100 Subject: [PATCH 01/10] better ci validations, fix marketplace error --- .claude-plugin/marketplace.json | 9 + .github/scripts/validate_plugin_metadata.py | 271 ++++++++++++++++++++ .github/workflows/validate.yml | 46 +--- 3 files changed, 283 insertions(+), 43 deletions(-) create mode 100644 .github/scripts/validate_plugin_metadata.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index f495326..0074e5f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -47,6 +47,15 @@ }, "source": "./plugins/burpsuite-project-parser" }, + { + "name": "claude-in-chrome-troubleshooting", + "version": "1.0.0", + "description": "Diagnose and fix Claude in Chrome MCP extension connectivity issues", + "author": { + "name": "Dan Guido" + }, + "source": "./plugins/claude-in-chrome-troubleshooting" + }, { "name": "constant-time-analysis", "version": "0.1.0", diff --git a/.github/scripts/validate_plugin_metadata.py b/.github/scripts/validate_plugin_metadata.py new file mode 100644 index 0000000..faec3ef --- /dev/null +++ b/.github/scripts/validate_plugin_metadata.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// +"""Validate plugin metadata consistency across all configuration files. + +Checks that plugins have: +1. A valid .claude-plugin/plugin.json +2. An entry in .claude-plugin/marketplace.json +3. An entry in README.md +4. An entry in CODEOWNERS +""" + +from __future__ import annotations + +import json +import os +import re +import sys +from dataclasses import dataclass +from pathlib import Path + +PLUGIN_PATH_PATTERN = re.compile(r"^plugins/([^/]+)/") + + +@dataclass +class ValidationError: + """A single validation error.""" + + plugin: str + message: str + + def __str__(self) -> str: + return f"{self.plugin}: {self.message}" + + +def scan_plugins_directory(plugins_dir: Path) -> set[str]: + """Scan plugins/ directory and return all plugin directory names.""" + if not plugins_dir.is_dir(): + return set() + + return { + p.name + for p in plugins_dir.iterdir() + if p.is_dir() and not p.name.startswith(".") + } + + +def extract_plugins_from_changed_files( + changed_files: list[str], + repo_root: Path, +) -> set[str]: + """Extract plugin names from changed file paths. + + If marketplace.json changed, includes all plugins from marketplace AND + all directories in plugins/ (to catch unregistered plugins). + """ + plugins = set() + marketplace_changed = False + + for path in changed_files: + if match := PLUGIN_PATH_PATTERN.match(path): + plugins.add(match.group(1)) + elif path == ".claude-plugin/marketplace.json": + marketplace_changed = True + + if marketplace_changed: + marketplace_path = repo_root / ".claude-plugin" / "marketplace.json" + plugins.update(parse_marketplace(marketplace_path).keys()) + plugins.update(scan_plugins_directory(repo_root / "plugins")) + + return plugins + + +def parse_marketplace(marketplace_path: Path) -> dict[str, dict]: + """Parse marketplace.json and return plugin_name -> plugin_data mapping.""" + if not marketplace_path.exists(): + return {} + + data = json.loads(marketplace_path.read_text()) + return {p["name"]: p for p in data.get("plugins", []) if p.get("name")} + + +def parse_codeowners(codeowners_path: Path) -> set[str]: + """Parse CODEOWNERS and return set of plugin names with entries.""" + if not codeowners_path.exists(): + return set() + + plugins = set() + pattern = re.compile(r"^/plugins/([^/]+)/") + + for line in codeowners_path.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#"): + if match := pattern.match(line): + plugins.add(match.group(1)) + + return plugins + + +def parse_readme(readme_path: Path) -> set[str]: + """Parse README.md and return set of plugin names mentioned in tables.""" + if not readme_path.exists(): + return set() + + plugins = set() + # Matches [text](plugins/name) or [text](./plugins/name) + pattern = re.compile(r"\[[^\]]+\]\(\.?/?plugins/([^/)]+)") + + for line in readme_path.read_text().splitlines(): + for match in pattern.finditer(line): + plugins.add(match.group(1)) + + return plugins + + +def validate_plugin_json(plugin_path: Path, plugin_name: str) -> list[str]: + """Validate plugin.json exists and has required fields.""" + errors = [] + json_path = plugin_path / ".claude-plugin" / "plugin.json" + + if not json_path.exists(): + return ["missing .claude-plugin/plugin.json"] + + try: + data = json.loads(json_path.read_text()) + except json.JSONDecodeError as e: + return [f".claude-plugin/plugin.json is invalid JSON: {e}"] + + if "name" not in data: + errors.append(".claude-plugin/plugin.json missing 'name' field") + elif data["name"] != plugin_name: + errors.append( + f".claude-plugin/plugin.json name '{data['name']}' " + f"doesn't match directory name '{plugin_name}'" + ) + + if "description" not in data: + errors.append(".claude-plugin/plugin.json missing 'description' field") + + if "version" not in data: + errors.append(".claude-plugin/plugin.json missing 'version' field") + + return errors + + +def validate_marketplace_entry( + marketplace_plugins: dict[str, dict], + plugin_path: Path, + plugin_name: str, +) -> list[str]: + """Validate plugin has matching entry in marketplace.json.""" + if plugin_name not in marketplace_plugins: + return ["not found in .claude-plugin/marketplace.json"] + + errors = [] + marketplace_entry = marketplace_plugins[plugin_name] + json_path = plugin_path / ".claude-plugin" / "plugin.json" + + if not json_path.exists(): + return errors + + try: + plugin_data = json.loads(json_path.read_text()) + except json.JSONDecodeError: + return errors + + if plugin_data.get("name") != marketplace_entry.get("name"): + errors.append( + f"name mismatch: plugin.json has '{plugin_data.get('name')}', " + f"marketplace.json has '{marketplace_entry.get('name')}'" + ) + + expected_source = f"./plugins/{plugin_name}" + actual_source = marketplace_entry.get("source", "") + if actual_source != expected_source: + errors.append( + f"marketplace.json source '{actual_source}' should be '{expected_source}'" + ) + + return errors + + +def validate_plugins( + plugins_to_check: set[str], + repo_root: Path, +) -> list[ValidationError]: + """Validate all specified plugins and return errors.""" + errors = [] + + plugins_dir = repo_root / "plugins" + marketplace_plugins = parse_marketplace( + repo_root / ".claude-plugin" / "marketplace.json" + ) + codeowners_plugins = parse_codeowners(repo_root / "CODEOWNERS") + readme_plugins = parse_readme(repo_root / "README.md") + + for plugin_name in sorted(plugins_to_check): + plugin_path = plugins_dir / plugin_name + plugin_exists = plugin_path.is_dir() + + if plugin_exists: + for msg in validate_plugin_json(plugin_path, plugin_name): + errors.append(ValidationError(plugin_name, msg)) + + for msg in validate_marketplace_entry( + marketplace_plugins, plugin_path, plugin_name + ): + errors.append(ValidationError(plugin_name, msg)) + + if plugin_name not in codeowners_plugins: + errors.append(ValidationError(plugin_name, "not found in CODEOWNERS")) + + if plugin_name not in readme_plugins: + errors.append(ValidationError(plugin_name, "not found in README.md")) + else: + if plugin_name in marketplace_plugins: + errors.append( + ValidationError( + plugin_name, + "deleted but still in .claude-plugin/marketplace.json", + ) + ) + if plugin_name in codeowners_plugins: + errors.append( + ValidationError(plugin_name, "deleted but still in CODEOWNERS") + ) + if plugin_name in readme_plugins: + errors.append( + ValidationError(plugin_name, "deleted but still in README.md") + ) + + return errors + + +def main() -> int: + """Validate plugin metadata consistency.""" + repo_root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent.parent.parent + + changed_files = os.environ.get("CHANGED_FILES", "").splitlines() + if changed_files: + plugins_to_check = extract_plugins_from_changed_files(changed_files, repo_root) + if not plugins_to_check: + print("No plugins affected by changed files") + return 0 + else: + plugins_to_check = scan_plugins_directory(repo_root / "plugins") + if not plugins_to_check: + print(f"No plugins found in {repo_root / 'plugins'}") + return 0 + + print( + f"Checking {len(plugins_to_check)} plugin(s): {', '.join(sorted(plugins_to_check))}" + ) + + errors = validate_plugins(plugins_to_check, repo_root) + + if not errors: + print("✓ All plugin metadata is in sync") + return 0 + + print(f"\n✗ Found {len(errors)} error(s):\n") + for error in errors: + print(f" • {error}") + + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ee825b4..520f2ee 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -22,49 +22,6 @@ jobs: with: persist-credentials: false - - name: Validate JSON files - run: | - echo "Validating marketplace.json..." - python3 -m json.tool .claude-plugin/marketplace.json > /dev/null - - echo "Validating plugin.json files..." - for f in plugins/*/.claude-plugin/plugin.json; do - echo " Checking $f" - python3 -m json.tool "$f" > /dev/null - done - - - name: Check marketplace consistency - run: | - echo "Checking all marketplace plugins have directories..." - python3 -c " - import json - import os - import sys - - with open('.claude-plugin/marketplace.json') as f: - marketplace = json.load(f) - - errors = [] - for plugin in marketplace['plugins']: - name = plugin['name'] - source = plugin['source'] - # source is like ./plugins/name - path = source.lstrip('./') - if not os.path.isdir(path): - errors.append(f'Plugin {name}: directory {path} not found') - - plugin_json = os.path.join(path, '.claude-plugin', 'plugin.json') - if not os.path.isfile(plugin_json): - errors.append(f'Plugin {name}: missing {plugin_json}') - - if errors: - for e in errors: - print(f'ERROR: {e}', file=sys.stderr) - sys.exit(1) - - print(f'All {len(marketplace[\"plugins\"])} plugins validated') - " - - name: Validate SKILL.md frontmatter run: | echo "Checking SKILL.md frontmatter..." @@ -129,3 +86,6 @@ jobs: exit 1 fi echo "No personal emails found" + + - name: Validate plugin metadata + run: python3 .github/scripts/validate_plugin_metadata.py From 2689d47857819d0d6d05a7417e43361efc755852 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 10:01:56 +0100 Subject: [PATCH 02/10] fix lints --- .github/scripts/validate_plugin_metadata.py | 30 +++++---------------- .github/workflows/lint.yml | 10 +++++-- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/.github/scripts/validate_plugin_metadata.py b/.github/scripts/validate_plugin_metadata.py index faec3ef..71d235a 100644 --- a/.github/scripts/validate_plugin_metadata.py +++ b/.github/scripts/validate_plugin_metadata.py @@ -40,11 +40,7 @@ def scan_plugins_directory(plugins_dir: Path) -> set[str]: if not plugins_dir.is_dir(): return set() - return { - p.name - for p in plugins_dir.iterdir() - if p.is_dir() and not p.name.startswith(".") - } + return {p.name for p in plugins_dir.iterdir() if p.is_dir() and not p.name.startswith(".")} def extract_plugins_from_changed_files( @@ -175,9 +171,7 @@ def validate_marketplace_entry( expected_source = f"./plugins/{plugin_name}" actual_source = marketplace_entry.get("source", "") if actual_source != expected_source: - errors.append( - f"marketplace.json source '{actual_source}' should be '{expected_source}'" - ) + errors.append(f"marketplace.json source '{actual_source}' should be '{expected_source}'") return errors @@ -190,9 +184,7 @@ def validate_plugins( errors = [] plugins_dir = repo_root / "plugins" - marketplace_plugins = parse_marketplace( - repo_root / ".claude-plugin" / "marketplace.json" - ) + marketplace_plugins = parse_marketplace(repo_root / ".claude-plugin" / "marketplace.json") codeowners_plugins = parse_codeowners(repo_root / "CODEOWNERS") readme_plugins = parse_readme(repo_root / "README.md") @@ -204,9 +196,7 @@ def validate_plugins( for msg in validate_plugin_json(plugin_path, plugin_name): errors.append(ValidationError(plugin_name, msg)) - for msg in validate_marketplace_entry( - marketplace_plugins, plugin_path, plugin_name - ): + for msg in validate_marketplace_entry(marketplace_plugins, plugin_path, plugin_name): errors.append(ValidationError(plugin_name, msg)) if plugin_name not in codeowners_plugins: @@ -223,13 +213,9 @@ def validate_plugins( ) ) if plugin_name in codeowners_plugins: - errors.append( - ValidationError(plugin_name, "deleted but still in CODEOWNERS") - ) + errors.append(ValidationError(plugin_name, "deleted but still in CODEOWNERS")) if plugin_name in readme_plugins: - errors.append( - ValidationError(plugin_name, "deleted but still in README.md") - ) + errors.append(ValidationError(plugin_name, "deleted but still in README.md")) return errors @@ -250,9 +236,7 @@ def main() -> int: print(f"No plugins found in {repo_root / 'plugins'}") return 0 - print( - f"Checking {len(plugins_to_check)} plugin(s): {', '.join(sorted(plugins_to_check))}" - ) + print(f"Checking {len(plugins_to_check)} plugin(s): {', '.join(sorted(plugins_to_check))}") errors = validate_plugins(plugins_to_check, repo_root) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ac4a26a..fb34acc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -65,5 +65,11 @@ jobs: sudo apt-get install -y bats curl -LsSf https://astral.sh/uv/install.sh | sh echo "$HOME/.local/bin" >> "$GITHUB_PATH" - - name: Run hook tests - run: bats plugins/modern-python/hooks/*.bats + - name: Run bats tests + run: | + bats_files=$(find plugins -name "*.bats" -type f) + if [ -z "$bats_files" ]; then + echo "No .bats files found, skipping" + exit 0 + fi + echo "$bats_files" | xargs bats From 36aec2ca9e2a906caaaccea618045d28535459fb Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 10:10:55 +0100 Subject: [PATCH 03/10] fix lint script --- .github/scripts/validate_plugin_metadata.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/scripts/validate_plugin_metadata.py b/.github/scripts/validate_plugin_metadata.py index 71d235a..f2b3f9f 100644 --- a/.github/scripts/validate_plugin_metadata.py +++ b/.github/scripts/validate_plugin_metadata.py @@ -88,8 +88,7 @@ def parse_codeowners(codeowners_path: Path) -> set[str]: for line in codeowners_path.read_text().splitlines(): line = line.strip() - if line and not line.startswith("#"): - if match := pattern.match(line): + if line and not line.startswith("#") and (match := pattern.match(line)): plugins.add(match.group(1)) return plugins From 2e68d36097bb59c34f567eda2b5be3d08274f60f Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 10:17:47 +0100 Subject: [PATCH 04/10] fix lint script2 --- .github/scripts/validate_plugin_metadata.py | 36 +++++++++++++++------ 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/.github/scripts/validate_plugin_metadata.py b/.github/scripts/validate_plugin_metadata.py index f2b3f9f..ef4bead 100644 --- a/.github/scripts/validate_plugin_metadata.py +++ b/.github/scripts/validate_plugin_metadata.py @@ -40,7 +40,11 @@ def scan_plugins_directory(plugins_dir: Path) -> set[str]: if not plugins_dir.is_dir(): return set() - return {p.name for p in plugins_dir.iterdir() if p.is_dir() and not p.name.startswith(".")} + return { + p.name + for p in plugins_dir.iterdir() + if p.is_dir() and not p.name.startswith(".") + } def extract_plugins_from_changed_files( @@ -89,7 +93,7 @@ def parse_codeowners(codeowners_path: Path) -> set[str]: for line in codeowners_path.read_text().splitlines(): line = line.strip() if line and not line.startswith("#") and (match := pattern.match(line)): - plugins.add(match.group(1)) + plugins.add(match.group(1)) return plugins @@ -170,7 +174,9 @@ def validate_marketplace_entry( expected_source = f"./plugins/{plugin_name}" actual_source = marketplace_entry.get("source", "") if actual_source != expected_source: - errors.append(f"marketplace.json source '{actual_source}' should be '{expected_source}'") + errors.append( + f"marketplace.json source '{actual_source}' should be '{expected_source}'" + ) return errors @@ -183,7 +189,9 @@ def validate_plugins( errors = [] plugins_dir = repo_root / "plugins" - marketplace_plugins = parse_marketplace(repo_root / ".claude-plugin" / "marketplace.json") + marketplace_plugins = parse_marketplace( + repo_root / ".claude-plugin" / "marketplace.json" + ) codeowners_plugins = parse_codeowners(repo_root / "CODEOWNERS") readme_plugins = parse_readme(repo_root / "README.md") @@ -195,7 +203,9 @@ def validate_plugins( for msg in validate_plugin_json(plugin_path, plugin_name): errors.append(ValidationError(plugin_name, msg)) - for msg in validate_marketplace_entry(marketplace_plugins, plugin_path, plugin_name): + for msg in validate_marketplace_entry( + marketplace_plugins, plugin_path, plugin_name + ): errors.append(ValidationError(plugin_name, msg)) if plugin_name not in codeowners_plugins: @@ -212,16 +222,22 @@ def validate_plugins( ) ) if plugin_name in codeowners_plugins: - errors.append(ValidationError(plugin_name, "deleted but still in CODEOWNERS")) + errors.append( + ValidationError(plugin_name, "deleted but still in CODEOWNERS") + ) if plugin_name in readme_plugins: - errors.append(ValidationError(plugin_name, "deleted but still in README.md")) + errors.append( + ValidationError(plugin_name, "deleted but still in README.md") + ) return errors def main() -> int: """Validate plugin metadata consistency.""" - repo_root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent.parent.parent + repo_root = ( + Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent.parent.parent + ) changed_files = os.environ.get("CHANGED_FILES", "").splitlines() if changed_files: @@ -235,7 +251,9 @@ def main() -> int: print(f"No plugins found in {repo_root / 'plugins'}") return 0 - print(f"Checking {len(plugins_to_check)} plugin(s): {', '.join(sorted(plugins_to_check))}") + print( + f"Checking {len(plugins_to_check)} plugin(s): {', '.join(sorted(plugins_to_check))}" + ) errors = validate_plugins(plugins_to_check, repo_root) From 6cee51fabbffcbef3e4555117d2c20726d0ac1a3 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 10:23:44 +0100 Subject: [PATCH 05/10] fix ruff errors --- .github/scripts/validate_plugin_metadata.py | 34 +++++---------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/.github/scripts/validate_plugin_metadata.py b/.github/scripts/validate_plugin_metadata.py index ef4bead..67d9d8e 100644 --- a/.github/scripts/validate_plugin_metadata.py +++ b/.github/scripts/validate_plugin_metadata.py @@ -40,11 +40,7 @@ def scan_plugins_directory(plugins_dir: Path) -> set[str]: if not plugins_dir.is_dir(): return set() - return { - p.name - for p in plugins_dir.iterdir() - if p.is_dir() and not p.name.startswith(".") - } + return {p.name for p in plugins_dir.iterdir() if p.is_dir() and not p.name.startswith(".")} def extract_plugins_from_changed_files( @@ -174,9 +170,7 @@ def validate_marketplace_entry( expected_source = f"./plugins/{plugin_name}" actual_source = marketplace_entry.get("source", "") if actual_source != expected_source: - errors.append( - f"marketplace.json source '{actual_source}' should be '{expected_source}'" - ) + errors.append(f"marketplace.json source '{actual_source}' should be '{expected_source}'") return errors @@ -189,9 +183,7 @@ def validate_plugins( errors = [] plugins_dir = repo_root / "plugins" - marketplace_plugins = parse_marketplace( - repo_root / ".claude-plugin" / "marketplace.json" - ) + marketplace_plugins = parse_marketplace(repo_root / ".claude-plugin" / "marketplace.json") codeowners_plugins = parse_codeowners(repo_root / "CODEOWNERS") readme_plugins = parse_readme(repo_root / "README.md") @@ -203,9 +195,7 @@ def validate_plugins( for msg in validate_plugin_json(plugin_path, plugin_name): errors.append(ValidationError(plugin_name, msg)) - for msg in validate_marketplace_entry( - marketplace_plugins, plugin_path, plugin_name - ): + for msg in validate_marketplace_entry(marketplace_plugins, plugin_path, plugin_name): errors.append(ValidationError(plugin_name, msg)) if plugin_name not in codeowners_plugins: @@ -222,22 +212,16 @@ def validate_plugins( ) ) if plugin_name in codeowners_plugins: - errors.append( - ValidationError(plugin_name, "deleted but still in CODEOWNERS") - ) + errors.append(ValidationError(plugin_name, "deleted but still in CODEOWNERS")) if plugin_name in readme_plugins: - errors.append( - ValidationError(plugin_name, "deleted but still in README.md") - ) + errors.append(ValidationError(plugin_name, "deleted but still in README.md")) return errors def main() -> int: """Validate plugin metadata consistency.""" - repo_root = ( - Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent.parent.parent - ) + repo_root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent.parent.parent changed_files = os.environ.get("CHANGED_FILES", "").splitlines() if changed_files: @@ -251,9 +235,7 @@ def main() -> int: print(f"No plugins found in {repo_root / 'plugins'}") return 0 - print( - f"Checking {len(plugins_to_check)} plugin(s): {', '.join(sorted(plugins_to_check))}" - ) + print(f"Checking {len(plugins_to_check)} plugin(s): {', '.join(sorted(plugins_to_check))}") errors = validate_plugins(plugins_to_check, repo_root) From e6ecbdf3ea452336a1aa7fbc2a2caf6d3183d673 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 10:31:45 +0100 Subject: [PATCH 06/10] fix abs path regex --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 520f2ee..83bbb83 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -72,7 +72,7 @@ jobs: run: | echo "Checking for hardcoded user paths..." # Exclude /path/to (example paths) and /home/vscode (standard devcontainer user) - if grep -rE '/home/[a-z]|/Users/[A-Z]' plugins/ --include='*.md' --include='*.py' --include='*.json' | grep -v '/path/to' | grep -v '/home/vscode'; then + if grep -rPn '(? Date: Thu, 12 Feb 2026 10:32:50 +0100 Subject: [PATCH 07/10] fix shellcheck lint --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fb34acc..2eeff90 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,7 +36,7 @@ jobs: with: persist-credentials: false - name: Run shellcheck - run: find plugins -name "*.sh" -type f -exec shellcheck -x {} + + run: find plugins -name "*.sh" -type f -exec shellcheck --severity=warning -x {} + shfmt: name: Shell (shfmt) From 56c4a7beabc74c2759267f41421552ca311a3702 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 12 Feb 2026 10:05:27 -0500 Subject: [PATCH 08/10] fix: resolve code review findings for PR #85 - Remove dead CHANGED_FILES code path (unreachable in CI) - Add version and description consistency checks between plugin.json and marketplace.json - Eliminate duplicate plugin.json reads via parse_plugin_json() - Fix bats test discovery for filenames with spaces (print0/xargs -0) - Sync marketplace.json with plugin.json for 5 pre-existing mismatches (second-opinion version, ask-questions/burpsuite/fix-review/insecure-defaults descriptions) Co-Authored-By: Claude Opus 4.6 --- .claude-plugin/marketplace.json | 10 +- .github/scripts/validate_plugin_metadata.py | 117 +++++++++----------- .github/workflows/lint.yml | 8 +- 3 files changed, 57 insertions(+), 78 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0074e5f..d42495f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ { "name": "ask-questions-if-underspecified", "version": "1.0.1", - "description": "Clarify requirements before implementing. When doubting, ask questions.", + "description": "Clarify requirements before implementing. Do not use automatically, only when invoked explicitly.", "author": { "name": "Kevin Valerio", "email": "opensource@trailofbits.com", @@ -41,7 +41,7 @@ { "name": "burpsuite-project-parser", "version": "1.0.0", - "description": "Search and extract data from Burp Suite project files (.burp) for use in Claude", + "description": "Search and extract data from Burp Suite project files (.burp) directly from the command line for use in Claude", "author": { "name": "Will Vandevanter" }, @@ -126,7 +126,7 @@ }, { "name": "fix-review", - "description": "Verify fix commits address audit findings without introducing bugs", + "description": "Verifies that code changes address security audit findings without introducing bugs", "version": "0.1.0", "author": { "name": "Trail of Bits", @@ -245,7 +245,7 @@ { "name": "insecure-defaults", "version": "1.0.0", - "description": "Detects and verifies insecure default configurations including hardcoded credentials, fallback secrets, weak authentication defaults, and dangerous configuration values that remain active in production", + "description": "Detects and verifies insecure default configurations", "author": { "name": "Trail of Bits", "email": "opensource@trailofbits.com", @@ -255,7 +255,7 @@ }, { "name": "second-opinion", - "version": "1.0.0", + "version": "1.2.0", "description": "Runs code reviews using external LLM CLIs (OpenAI Codex, Google Gemini) on uncommitted changes, branch diffs, or specific commits", "author": { "name": "Dan Guido" diff --git a/.github/scripts/validate_plugin_metadata.py b/.github/scripts/validate_plugin_metadata.py index 67d9d8e..9de6fb9 100644 --- a/.github/scripts/validate_plugin_metadata.py +++ b/.github/scripts/validate_plugin_metadata.py @@ -15,14 +15,11 @@ from __future__ import annotations import json -import os import re import sys from dataclasses import dataclass from pathlib import Path -PLUGIN_PATH_PATTERN = re.compile(r"^plugins/([^/]+)/") - @dataclass class ValidationError: @@ -43,32 +40,6 @@ def scan_plugins_directory(plugins_dir: Path) -> set[str]: return {p.name for p in plugins_dir.iterdir() if p.is_dir() and not p.name.startswith(".")} -def extract_plugins_from_changed_files( - changed_files: list[str], - repo_root: Path, -) -> set[str]: - """Extract plugin names from changed file paths. - - If marketplace.json changed, includes all plugins from marketplace AND - all directories in plugins/ (to catch unregistered plugins). - """ - plugins = set() - marketplace_changed = False - - for path in changed_files: - if match := PLUGIN_PATH_PATTERN.match(path): - plugins.add(match.group(1)) - elif path == ".claude-plugin/marketplace.json": - marketplace_changed = True - - if marketplace_changed: - marketplace_path = repo_root / ".claude-plugin" / "marketplace.json" - plugins.update(parse_marketplace(marketplace_path).keys()) - plugins.update(scan_plugins_directory(repo_root / "plugins")) - - return plugins - - def parse_marketplace(marketplace_path: Path) -> dict[str, dict]: """Parse marketplace.json and return plugin_name -> plugin_data mapping.""" if not marketplace_path.exists(): @@ -84,6 +55,7 @@ def parse_codeowners(codeowners_path: Path) -> set[str]: return set() plugins = set() + # Leading / matches CODEOWNERS path format (e.g., /plugins/foo/) pattern = re.compile(r"^/plugins/([^/]+)/") for line in codeowners_path.read_text().splitlines(): @@ -110,31 +82,46 @@ def parse_readme(readme_path: Path) -> set[str]: return plugins -def validate_plugin_json(plugin_path: Path, plugin_name: str) -> list[str]: +def parse_plugin_json(plugin_path: Path) -> dict | None: + """Parse plugin.json and return data, or None if missing/invalid.""" + json_path = plugin_path / ".claude-plugin" / "plugin.json" + if not json_path.exists(): + return None + + try: + return json.loads(json_path.read_text()) + except json.JSONDecodeError: + return None + + +def validate_plugin_json( + plugin_data: dict | None, + plugin_path: Path, + plugin_name: str, +) -> list[str]: """Validate plugin.json exists and has required fields.""" - errors = [] json_path = plugin_path / ".claude-plugin" / "plugin.json" if not json_path.exists(): return ["missing .claude-plugin/plugin.json"] - try: - data = json.loads(json_path.read_text()) - except json.JSONDecodeError as e: - return [f".claude-plugin/plugin.json is invalid JSON: {e}"] + if plugin_data is None: + return [".claude-plugin/plugin.json is invalid JSON"] + + errors = [] - if "name" not in data: + if "name" not in plugin_data: errors.append(".claude-plugin/plugin.json missing 'name' field") - elif data["name"] != plugin_name: + elif plugin_data["name"] != plugin_name: errors.append( - f".claude-plugin/plugin.json name '{data['name']}' " + f".claude-plugin/plugin.json name '{plugin_data['name']}' " f"doesn't match directory name '{plugin_name}'" ) - if "description" not in data: + if "description" not in plugin_data: errors.append(".claude-plugin/plugin.json missing 'description' field") - if "version" not in data: + if "version" not in plugin_data: errors.append(".claude-plugin/plugin.json missing 'version' field") return errors @@ -142,24 +129,18 @@ def validate_plugin_json(plugin_path: Path, plugin_name: str) -> list[str]: def validate_marketplace_entry( marketplace_plugins: dict[str, dict], - plugin_path: Path, + plugin_data: dict | None, plugin_name: str, ) -> list[str]: """Validate plugin has matching entry in marketplace.json.""" if plugin_name not in marketplace_plugins: return ["not found in .claude-plugin/marketplace.json"] + if plugin_data is None: + return [] + errors = [] marketplace_entry = marketplace_plugins[plugin_name] - json_path = plugin_path / ".claude-plugin" / "plugin.json" - - if not json_path.exists(): - return errors - - try: - plugin_data = json.loads(json_path.read_text()) - except json.JSONDecodeError: - return errors if plugin_data.get("name") != marketplace_entry.get("name"): errors.append( @@ -167,6 +148,15 @@ def validate_marketplace_entry( f"marketplace.json has '{marketplace_entry.get('name')}'" ) + if plugin_data.get("version") != marketplace_entry.get("version"): + errors.append( + f"version mismatch: plugin.json has '{plugin_data.get('version')}', " + f"marketplace.json has '{marketplace_entry.get('version')}'" + ) + + if plugin_data.get("description") != marketplace_entry.get("description"): + errors.append("description mismatch between plugin.json and marketplace.json") + expected_source = f"./plugins/{plugin_name}" actual_source = marketplace_entry.get("source", "") if actual_source != expected_source: @@ -192,10 +182,12 @@ def validate_plugins( plugin_exists = plugin_path.is_dir() if plugin_exists: - for msg in validate_plugin_json(plugin_path, plugin_name): + plugin_data = parse_plugin_json(plugin_path) + + for msg in validate_plugin_json(plugin_data, plugin_path, plugin_name): errors.append(ValidationError(plugin_name, msg)) - for msg in validate_marketplace_entry(marketplace_plugins, plugin_path, plugin_name): + for msg in validate_marketplace_entry(marketplace_plugins, plugin_data, plugin_name): errors.append(ValidationError(plugin_name, msg)) if plugin_name not in codeowners_plugins: @@ -223,29 +215,22 @@ def main() -> int: """Validate plugin metadata consistency.""" repo_root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent.parent.parent - changed_files = os.environ.get("CHANGED_FILES", "").splitlines() - if changed_files: - plugins_to_check = extract_plugins_from_changed_files(changed_files, repo_root) - if not plugins_to_check: - print("No plugins affected by changed files") - return 0 - else: - plugins_to_check = scan_plugins_directory(repo_root / "plugins") - if not plugins_to_check: - print(f"No plugins found in {repo_root / 'plugins'}") - return 0 + plugins_to_check = scan_plugins_directory(repo_root / "plugins") + if not plugins_to_check: + print(f"No plugins found in {repo_root / 'plugins'}") + return 0 print(f"Checking {len(plugins_to_check)} plugin(s): {', '.join(sorted(plugins_to_check))}") errors = validate_plugins(plugins_to_check, repo_root) if not errors: - print("✓ All plugin metadata is in sync") + print("All plugin metadata is in sync") return 0 - print(f"\n✗ Found {len(errors)} error(s):\n") + print(f"\nFound {len(errors)} error(s):\n") for error in errors: - print(f" • {error}") + print(f" - {error}") return 1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2eeff90..10ee00c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -66,10 +66,4 @@ jobs: curl -LsSf https://astral.sh/uv/install.sh | sh echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Run bats tests - run: | - bats_files=$(find plugins -name "*.bats" -type f) - if [ -z "$bats_files" ]; then - echo "No .bats files found, skipping" - exit 0 - fi - echo "$bats_files" | xargs bats + run: find plugins -name "*.bats" -type f -print0 | xargs -0 --no-run-if-empty bats From 54cd3e2b5787df5c0358b0d0e7a8a98d15a2c957 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 12 Feb 2026 10:20:50 -0500 Subject: [PATCH 09/10] fix: gh-cli bats tests fail when gh is in /usr/bin The _no_gh test helpers used PATH=/usr/bin:/bin to exclude gh, but on Ubuntu CI runners gh is installed at /usr/bin/gh. Fix by creating a temp directory with symlinks to only jq and bash. Co-Authored-By: Claude Opus 4.6 --- plugins/gh-cli/hooks/test_helper.bash | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/plugins/gh-cli/hooks/test_helper.bash b/plugins/gh-cli/hooks/test_helper.bash index 61403fe..0c22ac6 100644 --- a/plugins/gh-cli/hooks/test_helper.bash +++ b/plugins/gh-cli/hooks/test_helper.bash @@ -22,14 +22,31 @@ run_curl_hook() { } # Run hook without gh available (for testing early exit) +# Create a minimal PATH containing jq but not gh. +# On Ubuntu CI runners, both live in /usr/bin, so we can't just restrict PATH. +# Instead, create a temp dir with only a jq symlink. +_make_path_without_gh() { + local tmpdir + tmpdir="$(mktemp -d)" + ln -s "$(command -v jq)" "${tmpdir}/jq" + ln -s "$(command -v bash)" "${tmpdir}/bash" + echo "$tmpdir" +} + run_fetch_hook_no_gh() { local url="$1" - run env PATH=/usr/bin:/bin bash -c 'jq -n --arg url "$1" '"'"'{"tool_input":{"url":$url,"prompt":"test"}}'"'"' 2>/dev/null | "$2"' _ "$url" "$FETCH_HOOK" + local safe_path + safe_path="$(_make_path_without_gh)" + run env PATH="$safe_path" bash -c 'jq -n --arg url "$1" '"'"'{"tool_input":{"url":$url,"prompt":"test"}}'"'"' 2>/dev/null | "$2"' _ "$url" "$FETCH_HOOK" + rm -rf "$safe_path" } run_curl_hook_no_gh() { local cmd="$1" - run env PATH=/usr/bin:/bin bash -c 'jq -n --arg cmd "$1" '"'"'{"tool_input":{"command":$cmd}}'"'"' 2>/dev/null | "$2"' _ "$cmd" "$CURL_HOOK" + local safe_path + safe_path="$(_make_path_without_gh)" + run env PATH="$safe_path" bash -c 'jq -n --arg cmd "$1" '"'"'{"tool_input":{"command":$cmd}}'"'"' 2>/dev/null | "$2"' _ "$cmd" "$CURL_HOOK" + rm -rf "$safe_path" } # Assert the hook allowed the action (exit 0, no output) From 2d654c74333422bd86bb0cdf3976bde363796e11 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 12 Feb 2026 10:27:20 -0500 Subject: [PATCH 10/10] fix: improve plugin descriptions for better skill triggering - ask-questions-if-underspecified: restore trigger context ("asking questions") while keeping invocation constraint - burpsuite-project-parser: drop filler "directly from the command line", use outcome-oriented "for security analysis" - insecure-defaults: restore specific scenarios (hardcoded credentials, fallback secrets, weak auth defaults) for better matching Co-Authored-By: Claude Opus 4.6 --- .claude-plugin/marketplace.json | 6 +++--- .../.claude-plugin/plugin.json | 2 +- plugins/burpsuite-project-parser/.claude-plugin/plugin.json | 4 ++-- plugins/insecure-defaults/.claude-plugin/plugin.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 731b9e5..7c0e538 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ { "name": "ask-questions-if-underspecified", "version": "1.0.1", - "description": "Clarify requirements before implementing. Do not use automatically, only when invoked explicitly.", + "description": "Clarify ambiguous requirements by asking questions before implementing. Only when invoked explicitly.", "author": { "name": "Kevin Valerio", "email": "opensource@trailofbits.com", @@ -41,7 +41,7 @@ { "name": "burpsuite-project-parser", "version": "1.0.0", - "description": "Search and extract data from Burp Suite project files (.burp) directly from the command line for use in Claude", + "description": "Search and extract data from Burp Suite project files (.burp) for security analysis", "author": { "name": "Will Vandevanter" }, @@ -235,7 +235,7 @@ { "name": "insecure-defaults", "version": "1.0.0", - "description": "Detects and verifies insecure default configurations", + "description": "Detects insecure default configurations including hardcoded credentials, fallback secrets, weak authentication defaults, and dangerous values in production", "author": { "name": "Trail of Bits", "email": "opensource@trailofbits.com", diff --git a/plugins/ask-questions-if-underspecified/.claude-plugin/plugin.json b/plugins/ask-questions-if-underspecified/.claude-plugin/plugin.json index e005dfd..d36f4ae 100644 --- a/plugins/ask-questions-if-underspecified/.claude-plugin/plugin.json +++ b/plugins/ask-questions-if-underspecified/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ask-questions-if-underspecified", "version": "1.0.1", - "description": "Clarify requirements before implementing. Do not use automatically, only when invoked explicitly.", + "description": "Clarify ambiguous requirements by asking questions before implementing. Only when invoked explicitly.", "author": { "name": "Kevin Valerio", "email": "opensource@trailofbits.com", diff --git a/plugins/burpsuite-project-parser/.claude-plugin/plugin.json b/plugins/burpsuite-project-parser/.claude-plugin/plugin.json index d32aed7..55f67eb 100644 --- a/plugins/burpsuite-project-parser/.claude-plugin/plugin.json +++ b/plugins/burpsuite-project-parser/.claude-plugin/plugin.json @@ -1,10 +1,10 @@ { "name": "burpsuite-project-parser", "version": "1.0.0", - "description": "Search and extract data from Burp Suite project files (.burp) directly from the command line for use in Claude", + "description": "Search and extract data from Burp Suite project files (.burp) for security analysis", "author": { "name": "Will Vandevanter", "email": "opensource@trailofbits.com", "url": "https://github.com/trailofbits" } -} \ No newline at end of file +} diff --git a/plugins/insecure-defaults/.claude-plugin/plugin.json b/plugins/insecure-defaults/.claude-plugin/plugin.json index de0d535..8004fe0 100644 --- a/plugins/insecure-defaults/.claude-plugin/plugin.json +++ b/plugins/insecure-defaults/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "insecure-defaults", "version": "1.0.0", - "description": "Detects and verifies insecure default configurations", + "description": "Detects insecure default configurations including hardcoded credentials, fallback secrets, weak authentication defaults, and dangerous values in production", "author": { "name": "Trail of Bits", "email": "opensource@trailofbits.com",