# How the `bump-plugin-shas` Action Keeps Plugins Pinned in the Claude Plugins Community

> Discover how the bump-plugin-shas action pins plugins in the Claude Plugins Community by validating commits and opening signed pull requests. Learn about secure plugin management.

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

---

**The `bump-plugin-shas` GitHub Action continuously re-pins external plugins by validating new upstream commits against policy guards, then opening pull requests with cryptographically signed commits.**

The `bump-plugin-shas` action in the `anthropics/claude-plugins-community` repository automates one of the most security-critical maintenance tasks for a plugin marketplace: ensuring that every external plugin reference points to a verified, up-to-date commit without introducing breaking changes or supply-chain risks. This article explains how the action keeps plugins pinned through a nine-stage validation pipeline.

## Core Architecture of the Bump Workflow

The action operates as a composite GitHub Action defined in [`.github/actions/bump-plugin-shas/action.yml`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/bump-plugin-shas/action.yml), with its core logic implemented in [`.github/actions/bump-plugin-shas/scripts/bump.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/bump-plugin-shas/scripts/bump.sh). The script follows a strict sequence that guarantees pins move forward only when multiple safety conditions are met.

### Stage 1: Configuration Loading

The action first ingests three sources of policy data:

- **Marketplace manifest**: [`.claude-plugin/marketplace.json`](https://github.com/anthropics/claude-plugins-community/blob/main/.claude-plugin/marketplace.json) contains all plugin entries with `source.sha` fields
- **Freeze list**: [`.github/freeze-shas.txt`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/freeze-shas.txt) lists plugins whose pins must never advance
- **Tracking config**: Optional rules like `releases-only` that modify how "latest" is determined

This happens at lines 58-66 of [`bump.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/bump.sh), with input declarations at lines 32-42 of [`action.yml`](https://github.com/anthropics/claude-plugins-community/blob/main/action.yml).

### Stage 2: Staleness Detection

Beginning at line 67, the script iterates over every external plugin entry—those with a `source.sha` field—and queries the upstream repository to determine if the stored SHA matches the current HEAD or latest release.

### Stage 3: Policy Guard Evaluation

Before any bump proceeds, four policy guards enforce organizational constraints (lines 88-102, 115-130, 134-155, 162-190, 230-260):

| Guard | Purpose | Behavior |
|-------|---------|----------|
| **SHA-exempt** (`SHA_EXEMPT`) | Allows deliberately unpinned plugins | Skips validation and bumping entirely |
| **Freeze-shas** (`FREEZE_SHAS`) | Security holds on known-good or suspect commits | Prevents any SHA movement for listed plugins |
| **Releases-only** (`RELEASES_ONLY`) | Stability for high-risk dependencies | Bumps to release tag commits instead of HEAD |
| **Owner-baseline** | Supply-chain attack prevention | Verifies repository owner hasn't changed via account ID comparison |

### Stage 4: SHA Resolution

For normal entries, the action runs `git ls-remote … HEAD` (lines 29-38). For releases-only entries, it calls `gh api` to fetch the latest release tag, then resolves that tag to a commit SHA (lines 34-53). The `compare=ahead` parameter enforces forward-only movement—no rollbacks or lateral moves are permitted.

### Stage 5: Subtree Deduplication

When `source.path` indicates a monorepo subdirectory, lines 97-117 compare tree-object IDs between old and new SHAs. If the subtree is unchanged, the bump is suppressed to avoid unnecessary churn and CI load.

### Stage 6: Validation Pipeline

At lines 123-161, the action clones the external repository at the candidate SHA and runs `claude plugin validate`. For plugins with `strict: false`, it synthesizes a minimal [`plugin.json`](https://github.com/anthropics/claude-plugins-community/blob/main/plugin.json) to ensure validation can proceed. Only passing entries remain eligible for bumping.

### Stage 7: Signed Manifest Updates

For each validated bump, `jq` rewrites the corresponding `source.sha` in [`marketplace.json`](https://github.com/anthropics/claude-plugins-community/blob/main/marketplace.json). The commit is created via GitHub's GraphQL `createCommitOnBranch` mutation (lines 47-61, 83-90), which produces a GPG-signed commit using GitHub's internal key. This satisfies organization-level `required_signatures` branch protection rules without manual intervention.

### Stage 8: Pull Request Creation

The `pr-mode` input determines PR structure:

- **Batch mode** (lines 88-124): Single branch and PR containing all bumps
- **Per-entry mode** (lines 94-124): Individual branch and PR per plugin, enabling selective review and rollback

Each PR body contains a table of old versus new SHAs with links to the validating workflow run.

### Stage 9: Reporting and Audit

Lines 105-124 and 155-172 generate a GitHub step summary showing counts of checked, bumped, skipped, and failed entries, with detailed reasons for each skip category.

## Workflow Integration Example

The scheduled workflow that invokes this action lives at [`.github/workflows/bump-plugin-shas.yml`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/workflows/bump-plugin-shas.yml):

```yaml
name: Bump Plugin SHAs
on:
  schedule:
    - cron: '23 7 * * *'
  workflow_dispatch:
    inputs:
      max_bumps:
        description: 'Cap on plugins bumped this run'
        default: '30'
      plugin:
        description: 'Bump ONLY this plugin name (empty = all stale)'
        default: ''

jobs:
  bump:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Load freeze list
        id: freeze
        run: |
          if [[ -f .github/freeze-shas.txt ]]; then
            echo "list=$(cat .github/freeze-shas.txt | paste -sd ',' -)" >> $GITHUB_OUTPUT
          else
            echo "list=" >> $GITHUB_OUTPUT
          fi

      - uses: ./.github/actions/bump-plugin-shas
        id: bump
        with:
          marketplace-path: .claude-plugin/marketplace.json
          max-bumps: ${{ inputs.max_bumps }}
          freeze-shas: ${{ steps.freeze.outputs.list }}
          only: ${{ inputs.plugin }}
          pr-mode: per-entry
          claude-cli-version: latest

```

## Local Testing and Debugging

For developers working on the action itself, direct script invocation bypasses GitHub Actions overhead:

```bash
cd /path/to/claude-plugins-community

GITHUB_TOKEN=$(gh auth token) \
  MARKETPLACE_PATH=.claude-plugin/marketplace.json \
  MAX_BUMPS=10 \
  ALLOWED_HOSTS="github.com gitlab.com bitbucket.org" \
  SHA_EXEMPT="" \
  FREEZE_SHAS="example-plugin" \
  ONLY="" \
  PR_MODE=per-entry \
  PR_BRANCH=bump/plugin-shas \
  BASE_BRANCH=main \
  ./.github/actions/bump-plugin-shas/scripts/bump.sh

```

Set `ONLY` to a specific plugin name to test single-entry behavior without triggering bulk operations.

## Key Security Mechanisms

The action implements defense-in-depth through multiple overlapping controls:

- **Forward-only commits**: The `compare=ahead` check prevents rollback attacks
- **Cryptographic provenance**: GraphQL-signed commits satisfy `required_signatures` policies
- **Owner verification**: Account ID comparison in [`.github/owner-baseline.json`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/owner-baseline.json) detects repository transfers
- **Immutable freezes**: The `freeze-shas` mechanism provides emergency stop capability
- **Pre-merge validation**: No SHA reaches the manifest without passing `claude plugin validate`

## Summary

- The `bump-plugin-shas` action maintains plugin pins through automated detection, policy-guarded validation, and signed commit creation
- Four policy guards—SHA-exempt, freeze-shas, releases-only, and owner-baseline—prevent unwanted or unsafe updates
- All bumps are forward-only, validated against `claude plugin validate`, and committed via GraphQL for GPG signing
- The action supports both batch and per-entry PR modes to match organizational review workflows

## Frequently Asked Questions

### What prevents the action from rolling back to an older, potentially compromised commit?

The `compare=ahead` parameter in the SHA resolution logic enforces forward-only movement. When comparing the current `source.sha` to the candidate upstream commit, the action verifies the candidate is strictly ahead in the git history. Any rollback, lateral move, or divergent branch is rejected before validation begins.

### How do I temporarily stop a plugin from being bumped?

Add the plugin name to [`.github/freeze-shas.txt`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/freeze-shas.txt). The `FREEZE_SHAS` guard at lines 115-130 of [`bump.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/bump.sh) checks this list before any resolution occurs. Frozen plugins are reported in the step summary with reason `frozen`, and no upstream queries are performed for them.

### Why does the action use GraphQL instead of standard git commits for manifest updates?

The `createCommitOnBranch` GraphQL mutation produces commits signed by GitHub's internal GPG key. This satisfies branch protection rules requiring `required_signatures` without configuring repository-specific signing keys or exposing private key material to the action runtime. Standard git commits would fail these policies.

### Can the action handle plugins hosted outside GitHub?

Yes, through the `ALLOWED_HOSTS` environment variable. The default configuration includes `github.com`, `gitlab.com`, and `bitbucket.org`. The `git ls-remote` resolution at lines 29-38 works with any git host, though the releases-only mode requires GitHub's API and is therefore GitHub-specific.