diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..ff95d14 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Skill(update-docs)" + ] + } +} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30e3aa6..f79d514 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,14 @@ jobs: - name: Test run: pytest plugins/slack-publish/tests/ -q --tb=short + sast: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: SAST + run: make sast + validate: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index 277005c..99ffa73 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ -.PHONY: test validate +.PHONY: test validate sast test: @bash tests/run-tests.sh validate: @claude plugin validate .claude-plugin/marketplace.json + +sast: + @bash scripts/sast.sh diff --git a/README.md b/README.md index 5787509..f35241c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ - [fitness-coach](https://github.com/1shooperman/shooperman-claude-plugins/blob/main/plugins/fitness-coach) — AI-augmented fitness planning via a panel of real-world expert coaches - [custom-plugin-tools](https://github.com/1shooperman/shooperman-claude-plugins/blob/main/plugins/custom-plugin-tools) — Multi-agent PR description writer: summarizes changes, audits security, and generates a test plan - [slack-publish](https://github.com/1shooperman/shooperman-claude-plugins/blob/main/plugins/slack-publish) — Publish local Markdown files to Slack as formatted messages via chat.postMessage +- [update-marketplace](https://github.com/1shooperman/shooperman-claude-plugins/blob/main/plugins/update-marketplace) — Update the marketplace and all installed plugins to the latest versions ## Add the marketplace diff --git a/plugins/custom-plugin-tools/agents/agent-change-summarizer.md b/plugins/custom-plugin-tools/agents/agent-change-summarizer.md index 4ef01ca..54f8407 100644 --- a/plugins/custom-plugin-tools/agents/agent-change-summarizer.md +++ b/plugins/custom-plugin-tools/agents/agent-change-summarizer.md @@ -8,7 +8,7 @@ description: > Context: update-pr skill is building a PR body assistant: "Running change-summarizer agent to derive summary from branch diff" -allowed-tools: [Bash, Read] +allowed-tools: [Bash(git log*), Bash(git diff*), Read] model: sonnet color: blue --- diff --git a/plugins/custom-plugin-tools/agents/agent-sdet.md b/plugins/custom-plugin-tools/agents/agent-sdet.md index 9d57d77..41342d7 100644 --- a/plugins/custom-plugin-tools/agents/agent-sdet.md +++ b/plugins/custom-plugin-tools/agents/agent-sdet.md @@ -14,7 +14,7 @@ description: > Context: a new script was added to a plugin assistant: "Running sdet agent to write tests/**/test-.sh" -allowed-tools: [Bash, Read, Write] +allowed-tools: [Bash(git diff*), Bash(find tests/*), Read, Write] model: sonnet color: yellow --- diff --git a/plugins/custom-plugin-tools/agents/agent-security-auditor.md b/plugins/custom-plugin-tools/agents/agent-security-auditor.md index 8c9d932..81fa8a6 100644 --- a/plugins/custom-plugin-tools/agents/agent-security-auditor.md +++ b/plugins/custom-plugin-tools/agents/agent-security-auditor.md @@ -9,7 +9,7 @@ description: > Context: update-pr skill is building a PR body assistant: "Running security-auditor agent to review changed files" -allowed-tools: [Read, Glob, Grep, Bash] +allowed-tools: [Read, Glob, Grep, Bash(git diff*)] model: sonnet color: red --- diff --git a/plugins/custom-plugin-tools/agents/agent-test-planner.md b/plugins/custom-plugin-tools/agents/agent-test-planner.md index f75ab2e..3b44f1e 100644 --- a/plugins/custom-plugin-tools/agents/agent-test-planner.md +++ b/plugins/custom-plugin-tools/agents/agent-test-planner.md @@ -9,7 +9,7 @@ description: > Context: update-pr skill is building a PR body assistant: "Running test-planner agent to generate test checklist from changed files" -allowed-tools: [Bash, Read] +allowed-tools: [Bash(git diff*), Read] model: sonnet color: green --- diff --git a/plugins/custom-plugin-tools/skills/update/SKILL.md b/plugins/custom-plugin-tools/skills/update/SKILL.md index 37f219c..b6885d8 100644 --- a/plugins/custom-plugin-tools/skills/update/SKILL.md +++ b/plugins/custom-plugin-tools/skills/update/SKILL.md @@ -3,7 +3,7 @@ name: update description: Use this skill when the user asks to update a PR description, refresh PR body, or sync PR details based on branch changes. Triggered by phrases like "update PR#N desc", "refresh the PR description", or "update PR based on branch changes". user-invocable: true argument-hint: "" -allowed-tools: [Bash] +allowed-tools: [Bash(gh pr*)] --- ## Arguments diff --git a/plugins/fitness-coach/skills/onboard-staff/SKILL.md b/plugins/fitness-coach/skills/onboard-staff/SKILL.md index 808915e..1cb8348 100644 --- a/plugins/fitness-coach/skills/onboard-staff/SKILL.md +++ b/plugins/fitness-coach/skills/onboard-staff/SKILL.md @@ -3,7 +3,7 @@ name: onboard-staff description: Use this skill when the user wants to create their coaching staff by naming real health and fitness experts (e.g. "onboard staff with Peter Attia, Andy Galpin, and Rhonda Patrick" or "/onboard-staff Peter Attia Andy Galpin") user-invocable: true argument-hint: " ... " -allowed-tools: [WebFetch, Write] +allowed-tools: [WebFetch(https://*), Write] --- ## Arguments diff --git a/plugins/slack-publish/agents/agent-token-checker.md b/plugins/slack-publish/agents/agent-token-checker.md index b730cbf..a6548bd 100644 --- a/plugins/slack-publish/agents/agent-token-checker.md +++ b/plugins/slack-publish/agents/agent-token-checker.md @@ -1,7 +1,7 @@ --- name: agent-token-checker description: Checks whether SLACK_BOT_TOKEN is available (env var or .env file). Returns exit code 0 if found, 1 if missing. Used by the publish skill before attempting to post to Slack. -allowed-tools: [Bash] +allowed-tools: [Bash(python3 *)] model: haiku --- diff --git a/plugins/slack-publish/skills/compose/SKILL.md b/plugins/slack-publish/skills/compose/SKILL.md index 6971536..45f1a46 100644 --- a/plugins/slack-publish/skills/compose/SKILL.md +++ b/plugins/slack-publish/skills/compose/SKILL.md @@ -3,7 +3,7 @@ name: compose description: Draft a new Markdown file for Slack publishing and save it to the XDG cache directory (~/.cache/slack-publish/). Use when the user wants to write or compose a message to send to Slack, e.g. "compose a Slack message", "draft a release announcement for Slack", "write a new post for #general", or runs /slack-publish:compose. user-invocable: true argument-hint: " [channel]" -allowed-tools: [Bash, Write, Read] +allowed-tools: [Bash(mkdir*), Bash(ls*), Write, Read] --- ## Arguments diff --git a/plugins/slack-publish/skills/publish/SKILL.md b/plugins/slack-publish/skills/publish/SKILL.md index 8a3ac6b..b3d7337 100644 --- a/plugins/slack-publish/skills/publish/SKILL.md +++ b/plugins/slack-publish/skills/publish/SKILL.md @@ -3,7 +3,7 @@ name: publish description: Publish a local Markdown file to a Slack channel as a formatted message (not a file upload). Use when the user asks to send or publish a .md file to Slack, e.g. "publish foo.md to #general", "send this markdown to my-channel", or runs /slack-publish:publish. user-invocable: true argument-hint: " " -allowed-tools: [Bash] +allowed-tools: [Bash(python3 *)] --- ## Arguments diff --git a/plugins/update-marketplace/.claude-plugin/plugin.json b/plugins/update-marketplace/.claude-plugin/plugin.json index e473516..f827f59 100644 --- a/plugins/update-marketplace/.claude-plugin/plugin.json +++ b/plugins/update-marketplace/.claude-plugin/plugin.json @@ -5,5 +5,5 @@ "name": "1shooperman", "email": "contact@aglflorida.com" }, - "version": "0.1.0" + "version": "0.1.1" } diff --git a/plugins/update-marketplace/README.md b/plugins/update-marketplace/README.md new file mode 100644 index 0000000..8ca1b95 --- /dev/null +++ b/plugins/update-marketplace/README.md @@ -0,0 +1,21 @@ +# update-marketplace + +Updates the marketplace and all installed plugins to the latest versions. + +## Skills + +| Skill | Invocation | Description | +|-------|-----------|-------------| +| `update` | `/update-marketplace:update` | Update the marketplace and each installed plugin to the latest version | + +## Usage + +``` +/update-marketplace:update +``` + +## Install + +```bash +claude plugin install update-marketplace +``` diff --git a/plugins/update-marketplace/agents/agent-updater.md b/plugins/update-marketplace/agents/agent-updater.md index 9abf49a..b3e0e14 100644 --- a/plugins/update-marketplace/agents/agent-updater.md +++ b/plugins/update-marketplace/agents/agent-updater.md @@ -1,14 +1,15 @@ --- name: agent-updater description: Updates each plugin -allowed-tools: [Bash(claude plugin *), Bash(echo)] +allowed-tools: [Bash(claude plugin *), Bash(grep), Bash(echo), Bash(sed)] model: haiku color: cyan --- ## Instructions -Run `$CLAUDE_PLUGIN_ROOT/scripts/update-marketplace.sh` +1. Get the `update-marketplace` plugin `installPath` via `claude plugin list --json` +2. Run `bash /scripts/update-marketplace.sh` ## Outputs diff --git a/plugins/update-marketplace/scripts/update-marketplace.sh b/plugins/update-marketplace/scripts/update-marketplace.sh index 1a1ad33..0a910fe 100755 --- a/plugins/update-marketplace/scripts/update-marketplace.sh +++ b/plugins/update-marketplace/scripts/update-marketplace.sh @@ -13,12 +13,15 @@ set -euo pipefail MARKETPLACE_NAME="shooperman-claude-plugins" # setup 2: get plugin names -PLUGINS=($(claude plugin list | grep "$MARKETPLACE_NAME" | sed 's/^[^a-zA-Z]*//')) +PLUGINS=() +while IFS= read -r line; do + [[ -n "$line" ]] && PLUGINS+=("$line") +done < <(claude plugin list | grep "$MARKETPLACE_NAME" | sed 's/^[^a-zA-Z]*//' | sed "s/@$MARKETPLACE_NAME//" || true) echo "Updating $MARKETPLACE_NAME" claude plugin marketplace update "$MARKETPLACE_NAME" -for plugin in "${PLUGINS[@]}"; do +for plugin in "${PLUGINS[@]+"${PLUGINS[@]}"}"; do echo "Updating $plugin" claude plugin update "$plugin@$MARKETPLACE_NAME" done diff --git a/plugins/update-marketplace/commands/update.md b/plugins/update-marketplace/skills/run-update/SKILL.md similarity index 87% rename from plugins/update-marketplace/commands/update.md rename to plugins/update-marketplace/skills/run-update/SKILL.md index fcee433..6fde1d2 100644 --- a/plugins/update-marketplace/commands/update.md +++ b/plugins/update-marketplace/skills/run-update/SKILL.md @@ -1,6 +1,7 @@ --- +name: run-update description: Update the marketplace to the latest version, then update each plugin to the version specific. -allowed-tools: [Bash] +allowed-tools: [] --- ## Instructions diff --git a/scripts/sast.sh b/scripts/sast.sh new file mode 100644 index 0000000..b2ebc25 --- /dev/null +++ b/scripts/sast.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PLUGINS_DIR="$REPO_ROOT/plugins" + +FINDINGS=0 +EXIT_CODE=0 + +flag() { + local severity="$1"; shift + echo "[$severity] $*" + FINDINGS=$((FINDINGS + 1)) + if [[ "$severity" == "ERROR" ]]; then + EXIT_CODE=1 + fi +} + +# Scan all markdown files under plugins/ for allowed-tools declarations +while IFS= read -r file; do + # Extract allowed-tools line from YAML frontmatter only (between first --- pair) + tools_line=$(awk '/^---/{f=!f; next} f && /^allowed-tools:/' "$file" | head -1) + [[ -z "$tools_line" ]] && continue + + rel="${file#"$REPO_ROOT"/}" + + # Bare Bash (no constraint) — allows any shell command + if echo "$tools_line" | grep -qE '\bBash\b[^(]|Bash\s*\]|Bash\s*,'; then + flag ERROR "$rel: bare 'Bash' grants unrestricted shell access" + fi + + # Bare WebFetch (no domain) — allows fetching any URL + if echo "$tools_line" | grep -qE '\bWebFetch\b[^(]|WebFetch\s*\]|WebFetch\s*,'; then + flag WARN "$rel: bare 'WebFetch' allows fetching any domain" + fi + + # Bash(*) — constraint is just a wildcard, matches any command + if echo "$tools_line" | grep -qE 'Bash\(\s*\*\s*\)'; then + flag ERROR "$rel: Bash(*) is effectively unrestricted" + fi + + # Wildcard-only Agent/Skill — e.g. Agent(*) or allowed-tools: [*] + if echo "$tools_line" | grep -qE '\[\s*\*\s*\]|Agent\(\s*\*\s*\)|Skill\(\s*\*\s*\)'; then + flag ERROR "$rel: wildcard '*' grants access to all tools/agents" + fi + +done < <(find "$PLUGINS_DIR" -name "*.md" -type f) + +echo "" +echo "SAST complete. Findings: $FINDINGS" +exit "$EXIT_CODE" diff --git a/tests/update-marketplace/test-update-marketplace.sh b/tests/update-marketplace/test-update-marketplace.sh new file mode 100644 index 0000000..a22c433 --- /dev/null +++ b/tests/update-marketplace/test-update-marketplace.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Tests for plugins/update-marketplace/scripts/update-marketplace.sh +set -euo pipefail + +SCRIPT="$(cd "$(dirname "$0")/../.." && pwd)/plugins/update-marketplace/scripts/update-marketplace.sh" + +pass=0 +fail=0 + +# Runs the script with a mock claude that captures calls. +# Args: test description, mock plugin list output, expected_calls (newline-separated) +run_test() { + local desc="$1" + local list_output="$2" + local expected_calls="$3" + + local tmpdir + tmpdir=$(mktemp -d) + local calls_file="$tmpdir/calls" + touch "$calls_file" + + # Mock claude binary + cat > "$tmpdir/claude" <> "$calls_file" +if [[ "\$*" == "plugin list" ]]; then + cat <<'LIST' +$list_output +LIST +fi +EOF + chmod +x "$tmpdir/claude" + + PATH="$tmpdir:$PATH" bash "$SCRIPT" > /dev/null 2>&1 + local actual + actual=$(cat "$calls_file") + + if [ "$actual" = "$expected_calls" ]; then + echo " PASS: $desc" + pass=$((pass + 1)) + else + echo " FAIL: $desc" + echo " expected calls:" + echo "$expected_calls" | sed 's/^/ /' + echo " actual calls:" + echo "$actual" | sed 's/^/ /' + fail=$((fail + 1)) + fi + + rm -rf "$tmpdir" +} + +echo " update-marketplace.sh" + +LIST_TWO_PLUGINS=" ❯ pokemon-gbl@shooperman-claude-plugins + Version: 1.1.5 + ❯ update-marketplace@shooperman-claude-plugins + Version: 0.1.1 + ❯ other-plugin@other-marketplace + Version: 2.0.0" + +LIST_ONE_PLUGIN=" ❯ pokemon-gbl@shooperman-claude-plugins + Version: 1.1.5 + ❯ other-plugin@other-marketplace + Version: 2.0.0" + +LIST_NO_PLUGINS=" ❯ other-plugin@other-marketplace + Version: 2.0.0" + +run_test \ + "updates marketplace then each plugin" \ + "$LIST_TWO_PLUGINS" \ + "plugin list +plugin marketplace update shooperman-claude-plugins +plugin update pokemon-gbl@shooperman-claude-plugins +plugin update update-marketplace@shooperman-claude-plugins" + +run_test \ + "updates single plugin" \ + "$LIST_ONE_PLUGIN" \ + "plugin list +plugin marketplace update shooperman-claude-plugins +plugin update pokemon-gbl@shooperman-claude-plugins" + +run_test \ + "no plugins — still updates marketplace" \ + "$LIST_NO_PLUGINS" \ + "plugin list +plugin marketplace update shooperman-claude-plugins" + +echo "" +echo " $pass passed, $fail failed" + +[ "$fail" -eq 0 ] || exit 1