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.jsoncontains all plugin entries withsource.shafields - Freeze list:
.github/freeze-shas.txtlists plugins whose pins must never advance - Tracking config: Optional rules like
releases-onlythat 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=aheadcheck prevents rollback attacks - Cryptographic provenance: GraphQL-signed commits satisfy
required_signaturespolicies - Owner verification: Account ID comparison in
.github/owner-baseline.jsondetects repository transfers - Immutable freezes: The
freeze-shasmechanism provides emergency stop capability - Pre-merge validation: No SHA reaches the manifest without passing
claude plugin validate
Summary
- The
bump-plugin-shasaction 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →