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

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, with its core logic implemented in .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 contains all plugin entries with source.sha fields
  • Freeze list: .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, with input declarations at lines 32-42 of 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 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. 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:

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:

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 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. The FREEZE_SHAS guard at lines 115-130 of 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.

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 →