Static Pin Check for Claude Plugins: How the Security Validation Works

The static pin check is a deterministic, network-free CI validation that blocks Claude plugins from declaring MCP servers with floating package specs like @latest or semver ranges, ensuring only exact versions or vendored code can auto-execute at session start.

Claude plugins in the anthropics/claude-plugins-community repository can declare MCP servers—runtime processes that launch when a user session begins. These servers often use package managers like npx, bunx, uvx, or pipx to fetch and execute code. Because the plugin marketplace pins only the repository SHA, a floating package spec could silently pull newer, unvetted code. The static pin check prevents this by classifying every server declaration before it reaches users.

How the Static Pin Check Classifies Package Specs

The check runs in .github/actions/scan-plugins/lib/pin-check.sh as a pure Bash + jq pipeline. It never contacts the network, making it safe for any CI environment.

Step 1: Extract Server Entries from the Manifest

The pin_check_rows function reads .mcp.json or plugin.json and extracts each MCP server's command and arguments.


# From pin-check.sh: pin_check_rows processes manifest JSON

printf '%s' "$MANIFEST_JSON" | jq -r '
  .mcpServers // {} | to_entries[] |
  "\(.key)\t\(.value.command)\t\((.value.args // []) | join(" "))"'

This produces raw rows containing the server name, launcher, and package spec.

Step 2: Classify the Launcher and Parse the Spec

The check distinguishes four launchers: npx, bunx, uvx, and pipx. Each has its own parsing logic:

  • npmclass function (for npx/bunx): Parses npm-style specs

    • pinned: exact version (pkg@1.2.3)
    • floating: dist-tag (@latest), semver range (@^1.0.0), or bare name
    • bare: name without any @ (requires refinement)
  • pipclass function (for uvx/pipx): Parses Python-style specs

    • pinned: exact version (pkg==1.2.3)
    • floating: anything else (pkg, pkg>=1.0, etc.)

Step 3: Refine Bare Specs Against Vendored Packages

Bare names without version indicators receive special handling. The pin_check_refine_bare function inspects node_modules/ in the plugin tree:


# From pin-check.sh: refinement logic

pin_check_refine_bare() {
  local rows="$1" tree_root="$2"
  # For each bare spec, check if node_modules/<pkg>/package.json exists

  # and is not a symlink. If yes → reclassify as 'vendored'

  # Otherwise → reclassify as 'floating'

}

This catches legitimate cases where a plugin bundles its dependencies directly.

Step 4: Emit TSV and Enforce Policy

Each server becomes a tab-separated row: "<name>\t<launcher>\t<class>\t<spec>". The static-pin-check CI step aggregates these and adds two fields to the scan result JSON:

  • unpinned_autoexec_runtime: boolean flag
  • unpinned_autoexec_specs: array of floating server declarations

Downstream policy validators treat these as violations unless explicitly waived.

Static Pin Check Examples

Floating Spec Detection

A plugin with this .mcp.json:

{
  "mcpServers": {
    "run-helper": {
      "command": "npx",
      "args": ["my-tool@latest"]
    }
  }
}

Triggers the classification:


# pin_check_rows output

run-helper   npx   floating   my-tool@latest

The @latest dist-tag makes this an unpinned auto-exec runtime, flagged in CI.

Vendored Package Resolution

When a bare name corresponds to a real vendored dependency:

{
  "mcpServers": {
    "run-helper": {
      "command": "npx",
      "args": ["my-local-tool"]
    }
  }
}

With node_modules/my-local-tool/package.json present (and not a symlink), refinement produces:


# After pin_check_refine_bare

run-helper   npx   vendored   my-local-tool

This passes validation because the code is pinned by the repository SHA itself.

CI Integration

The validation workflow in .github/workflows/validate-plugins.yml executes:

- name: scan-plugins pin-check golden vectors
  run: bash .github/actions/scan-plugins/test-pin-check.sh

This harness loads pin-check.sh, runs classification across all plugins, and fails on unexpected floating results.

Key Source Files in the Claude Plugins Repository

File Purpose
.github/actions/scan-plugins/lib/pin-check.sh Core implementation with pin_check_rows, pin_check_refine_bare, pin_check_tree functions
.github/actions/scan-plugins/scripts/scan.sh Orchestrates full plugin scanning, merges pin-check output into final JSON
.github/actions/scan-plugins/policy/schema.json JSON Schema defining unpinned_autoexec_runtime and unpinned_autoexec_specs fields
.github/workflows/validate-plugins.yml CI workflow invoking the static pin check
.github/actions/scan-plugins/test-pin-check.sh Test harness with golden vectors for regression testing

Why the Static Pin Check Matters

The security model depends on deterministic execution: a given plugin SHA should always run identical code. Floating specs break this guarantee by resolving packages at session time from public registries. The static pin check closes this gap entirely in CI—before any plugin reaches the marketplace—without requiring network access or runtime overhead.

Summary

  • Static pin check validates MCP server declarations in plugin manifests using pure Bash + jq
  • Four launchers supported: npx, bunx, uvx, pipx with npm-style and pip-style version parsing
  • Three classifications: pinned (exact version), vendored (bundled dependency), floating (unresolved spec)
  • Bare specs refined against node_modules/ to distinguish legitimate vendored packages
  • CI enforcement adds unpinned_autoexec_runtime flags to scan results for policy validation
  • Network-free operation ensures reproducible, safe execution in any environment

Frequently Asked Questions

What triggers an unpinned auto-exec runtime violation?

A server declaration receives the floating classification when its package spec lacks an exact version—using @latest, semver ranges like ^1.0.0, or a bare name that doesn't resolve to a vendored node_modules/ entry. These cases populate unpinned_autoexec_specs in the scan result.

Does the static pin check examine skill markdown or other plugin code?

No. The check only inspects MCP server declarations in .mcp.json or plugin.json manifests. Skill markdown bodies and other code paths are validated separately through permission-gated runtime checks that execute with appropriate sandboxing.

Why use Bash and jq instead of a higher-level language?

The implementation prioritizes zero network dependencies and universal CI compatibility. Shell utilities are available in all GitHub Actions runners without package installation, eliminating supply chain risks from build-time dependencies.

Can plugin authors waive pin check failures?

Yes. The unpinned_autoexec_runtime field in scan results feeds into downstream policy validators that support explicit waivers for exceptional cases. However, waivers require human review and justification since they bypass a core security control.

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 →