How the detect-changes Script Works in the Claude Plugins Validation Pipeline

The detect-changes script (00-detect-changes.sh) serves as the entry point for the validate-plugins GitHub Action, computing which Claude plugins, external sources, and in-repo folders have been modified compared to a base reference and exporting structured JSON for downstream validation.

The detect-changes script is the intelligence layer of the validation pipeline in the anthropics/claude-plugins-community repository. Located at .github/actions/validate-plugins/scripts/00-detect-changes.sh, this Bash utility determines exactly what changed in a pull request to enable targeted validation of plugin metadata and source code. By comparing the current branch against BASE_REF, it minimizes CI overhead by validating only affected plugins rather than the entire marketplace.

Required Environment Variables and Setup

The script begins by sourcing shared utilities and validating mandatory environment variables:

source "$ACTION_PATH/lib/common.sh"
: "${BASE_REF:?BASE_REF is required}"
: "${MARKETPLACE_PATH:?MARKETPLACE_PATH is required}"
: "${VALIDATE_TMP:?VALIDATE_TMP is required}"
  • BASE_REF – The git reference (typically the target branch) against which the diff is computed.
  • MARKETPLACE_PATH – Path to either a single marketplace.json file or a directory containing per-plugin JSON entry files.
  • VALIDATE_TMP – A temporary directory where the script writes intermediate files including changes.json and marketplace.json.

Resolving the Base Reference with Fallback Handling

The script attempts to resolve BASE_REF locally. If the reference is not available, it performs a shallow fetch, and if that fails, it triggers a full validation fallback:

if ! git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then
  warn "BASE_REF '$BASE_REF' not resolvable; fetching"
  if ! git fetch --depth=1 origin "$BASE_REF" 2>/dev/null; then
    warn "fetch of $BASE_REF failed; treating ALL entries and folders as changed"
    ALL_CHANGED=1
  fi
fi

When ALL_CHANGED is set to 1, the script bypasses incremental diff logic and treats every plugin as modified. This defensive pattern ensures that validation errors cannot slip through due to missing git history.

Determining Changed Files and Marketplace Assembly

The script computes the list of changed files using git diff, or leaves the list empty if operating in ALL_CHANGED mode:

if (( ALL_CHANGED )); then
  DIFF_FILES=""
elif ! DIFF_FILES="$(git diff --name-only "$BASE_REF"...HEAD 2>&1)"; then
  warn "git diff failed ($DIFF_FILES); treating ALL entries and folders as changed"
  ALL_CHANGED=1
  DIFF_FILES=""
fi

Next, it assembles the marketplace JSON, supporting two distinct modes:

Per-file mode (when ENTRIES_DIR is defined) merges all *.json files in the directory into a single marketplace object with an optional manifest header:

jq -s --argjson hdr "$manifest_header" \
  '$hdr + {plugins: (. | sort_by(.name))}' \
  "$ENTRIES_DIR"/*.json > "$VALIDATE_TMP/marketplace.json"

Single-file mode simply copies the existing marketplace file to the temporary directory.

Computing Modified Plugin Entries

The script identifies changed plugin names using different strategies depending on the marketplace structure.

For per-file marketplaces, it extracts the basename of any JSON file under ENTRIES_DIR that appears in the diff:

src_files="$(printf '%s\n' "$DIFF_FILES" | grep -E "^${ENTRIES_DIR%/}/[^/]+\.json$" || true)"
changed_entries_json="$(printf '%s\n' "$src_files" \
    | sed -E 's|.*/([^/]+)\.json$|\1|' \
    | jq -R -s -c 'split("\n") | map(select(length > 0))')"

For single-file marketplaces, it performs a deep JSON comparison using jq to find plugins that differ between the base and current versions:

changed_entries_json="$(jq -c -s \
    '(.[0].plugins | map({(.name): .}) | add // {}) as $bmap
     | [.[1].plugins[] | select(($bmap[.name] // null) != .)]
     | map(.name)' \
    "$VALIDATE_TMP/marketplace.base.json" "$VALIDATE_TMP/marketplace.json")"

Identifying External Sources and In-Repo Plugin Folders

The script filters for external sources by selecting only entries where the source field is an object (indicating a URL or git subdirectory reference):

changed_external_json="$(
  jq -c \
    --argjson names "$changed_entries_json" \
    '[.plugins[]
      | select(.name as $n | $names | index($n))
      | select(.source | type == "object")
      | {name, source, strict}]' \
    "$VALIDATE_TMP/marketplace.json"
)"

To detect in-repo plugin folders, the script walks up the directory tree from each changed file until it locates a .claude-plugin/plugin.json marker. This supports nested plugin structures like partner-built/slack/:

while IFS= read -r f; do
  [[ -z "$f" ]] && continue
  d="$(dirname "$f")"
  while [[ "$d" != "." && "$d" != "/" ]]; do
    if [[ -f "$d/.claude-plugin/plugin.json" ]]; then
      folders+=("$d"); break
    fi
    d="$(dirname "$d")"
  done
done <<<"$DIFF_FILES"

In ALL_CHANGED mode, it instead discovers every plugin folder in the repository:

while IFS= read -r pj; do
  folders+=("$(dirname "$(dirname "$pj")")")
done < <(find . -mindepth 2 -path '*/.claude-plugin/plugin.json' -not -path './.git/*' | sed 's|^\./||')

Output Generation and GitHub Actions Integration

The script writes three intermediate JSON files (_changed-entries.json, _changed-external.json, _changed-folders.json) and merges them into a consolidated changes.json:

jq -n \
  --slurpfile entries  "$VALIDATE_TMP/_changed-entries.json" \
  --slurpfile external "$VALIDATE_TMP/_changed-external.json" \
  --slurpfile folders  "$VALIDATE_TMP/_changed-folders.json" \
  '{entries:$entries[0], external:$external[0], folders:$folders[0]}' \
  > "$VALIDATE_TMP/changes.json"

Finally, it exports the results through the GitHub Actions output mechanism so subsequent workflow steps can access them:

{
  echo "changed-entries=$changed_entries_json"
  echo "changed-external=$changed_external_json"
  echo "changed-folders=$changed_folders_json"
} >> "${GITHUB_OUTPUT:-/dev/stdout}"

Integration with Downstream Validation

According to the anthropics/claude-plugins-community source code, the changes.json output is consumed by:

Summary

  • The detect-changes script requires BASE_REF, MARKETPLACE_PATH, and VALIDATE_TMP environment variables to define the comparison baseline and output locations.
  • It supports both per-file marketplace directories (via ENTRIES_DIR) and single consolidated marketplace.json files.
  • When git references are unresolvable, it gracefully falls back to validating all plugins via the ALL_CHANGED flag to prevent validation bypasses.
  • The script identifies modified plugins through both file-path matching (for per-file mode) and deep JSON comparison using jq (for single-file mode).
  • It discovers affected in-repo folders by traversing parent directories to locate .claude-plugin/plugin.json markers, accommodating nested plugin structures.
  • Results are exported as structured JSON to both temporary files and GitHub Actions outputs for consumption by downstream validation scripts.

Frequently Asked Questions

What happens if the base reference cannot be resolved?

If git rev-parse fails to verify BASE_REF, the script attempts a shallow fetch with git fetch --depth=1. If that fetch fails, it sets ALL_CHANGED=1 and treats every plugin as modified, ensuring comprehensive validation coverage even when git history is incomplete.

How does the script handle nested plugin directories?

For each changed file, the script walks up the directory tree until it finds a .claude-plugin/plugin.json file. This traversal correctly identifies the root directory for nested plugins such as partner-built/slack/, ensuring that changes to any file within the plugin trigger validation of the entire plugin package.

What is the difference between changed-entries and changed-external outputs?

changed-entries contains a flat array of plugin names (strings) that were modified, suitable for quick lookups. changed-external contains full JSON objects including the name, source configuration, and strict flag for plugins that reference external repositories or URLs, enabling specialized validation for remote dependencies.

Can I run the detect-changes script locally for debugging?

Yes. Export the required environment variables (BASE_REF, MARKETPLACE_PATH, VALIDATE_TMP) from your shell and execute the script directly from the repository root. Inspect the generated $VALIDATE_TMP/changes.json file to verify which plugins the script identified as modified before committing your changes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →