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

> Learn how Claude plugins static pin check ensures security by blocking floating package specs and only allowing exact versions or vendored code for auto-execution.

- Repository: [Anthropic/claude-plugins-community](https://github.com/anthropics/claude-plugins-community)
- Tags: how-to-guide
- Published: 2026-08-31

---

**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`](https://github.com/anthropics/claude-plugins-community/blob/main/.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`](https://github.com/anthropics/claude-plugins-community/blob/main/.mcp.json) or [`plugin.json`](https://github.com/anthropics/claude-plugins-community/blob/main/plugin.json) and extracts each MCP server's `command` and arguments.

```bash

# 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:

```bash

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

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

```

Triggers the classification:

```bash

# 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:

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

```

With [`node_modules/my-local-tool/package.json`](https://github.com/anthropics/claude-plugins-community/blob/main/node_modules/my-local-tool/package.json) present (and not a symlink), refinement produces:

```bash

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

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

```

This harness loads [`pin-check.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/.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`](https://github.com/anthropics/claude-plugins-community/blob/main/.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`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/scan-plugins/policy/schema.json) | JSON Schema defining `unpinned_autoexec_runtime` and `unpinned_autoexec_specs` fields |
| [`.github/workflows/validate-plugins.yml`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/workflows/validate-plugins.yml) | CI workflow invoking the static pin check |
| [`.github/actions/scan-plugins/test-pin-check.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/.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`](https://github.com/anthropics/claude-plugins-community/blob/main/.mcp.json) or [`plugin.json`](https://github.com/anthropics/claude-plugins-community/blob/main/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.