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:
-
npmclassfunction (fornpx/bunx): Parses npm-style specspinned: exact version (pkg@1.2.3)floating: dist-tag (@latest), semver range (@^1.0.0), or bare namebare: name without any@(requires refinement)
-
pipclassfunction (foruvx/pipx): Parses Python-style specspinned: 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 flagunpinned_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,pipxwith 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_runtimeflags 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →