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

> Understand how the detect-changes script in the Claude Plugins validation pipeline identifies modified plugins and exports JSON for further validation. Learn about this crucial GitHub Action entry point.

- Repository: [Anthropic/claude-plugins-community](https://github.com/anthropics/claude-plugins-community)
- Tags: internals
- Published: 2026-08-28

---

**The `detect-changes` script ([`00-detect-changes.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/.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:

```bash
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`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/changes.json) and [`marketplace.json`](https://github.com/anthropics/claude-plugins-community/blob/main/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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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):

```bash
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`](https://github.com/anthropics/claude-plugins-community/blob/main/.claude-plugin/plugin.json) marker. This supports nested plugin structures like `partner-built/slack/`:

```bash
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:

```bash
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`](https://github.com/anthropics/claude-plugins-community/blob/main/_changed-entries.json), [`_changed-external.json`](https://github.com/anthropics/claude-plugins-community/blob/main/_changed-external.json), [`_changed-folders.json`](https://github.com/anthropics/claude-plugins-community/blob/main/_changed-folders.json)) and merges them into a consolidated [`changes.json`](https://github.com/anthropics/claude-plugins-community/blob/main/changes.json):

```bash
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:

```bash
{
  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`](https://github.com/anthropics/claude-plugins-community/blob/main/changes.json) output is consumed by:

- **[`11-validate-invariants.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/11-validate-invariants.sh)** – Validates that structural invariants hold for the changed entries.
- **[`90-report.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/90-report.sh)** – Generates human-readable PR summaries of what was validated.

## 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`](https://github.com/anthropics/claude-plugins-community/blob/main/.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`](https://github.com/anthropics/claude-plugins-community/blob/main/.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.