# How the `scan-plugins` GitHub Action Performs Security Scanning of Claude Plugins

> Discover how the scan-plugins GitHub Action secures Claude Marketplace plugins through static analysis and AI-driven policy review. Protect your integrations effectively.

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

---

**The `scan-plugins` GitHub Action combines a deterministic static analysis of unpinned package launchers with an AI-driven Claude policy review to secure external Claude Marketplace plugins.**

The `scan-plugins` action, located in the `anthropics/claude-plugins-community` repository, provides automated security scanning for Claude plugin submissions. It processes entries from the marketplace manifest ([`.claude-plugin/marketplace.json`](https://github.com/anthropics/claude-plugins-community/blob/main/.claude-plugin/marketplace.json)) and applies both auth-free static checks and authenticated AI analysis to detect security risks before plugins reach users.

## Two-Layer Security Architecture

The action implements a **defense-in-depth** approach with complementary scanning phases:

1. **Static pin check** – runs without authentication, detects floating launchers
2. **Claude policy scan** – requires Anthropic credentials, performs deep code analysis

This architecture ensures basic security validation even when API keys are unavailable or rate-limited.

## Static Pin Check: Auth-Free Launcher Validation

Before any AI review, the action executes [`scripts/static-pin-check.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/scripts/static-pin-check.sh) to inspect each plugin's configuration for **unpinned auto-execution launchers**.

### What It Detects

The scan examines [`.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) `mcpServers` entries) for these floating package-manager commands:

- `npx` – Node.js package executor
- `uvx` – uv package runner
- `bunx` – Bun package executor
- `pipx` – Python application installer

These launchers resolve code from package registries at runtime, bypassing the pinned source SHA that the marketplace manifest guarantees.

### Static Pin Check Behavior

- Emits `::warning` annotations for each unpinned launcher found
- Produces JSON outputs `pin-scanned` and `pin-failed` for downstream processing
- Hard-fails the job when `fail-on-unpinned-autoexec: "true"` is set

In [`.github/actions/scan-plugins/action.yml`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/scan-plugins/action.yml) (lines 45-53), this strict mode is configured:

```yaml
fail-on-unpinned-autoexec:
  description: 'Fail the job if unpinned auto-execution launchers are detected'
  required: false
  default: 'false'

```

## Authentication: API Key or Workload Identity

The Claude policy scan requires Anthropic credentials, but the action supports **two authentication paths**:

| Method | Inputs Required | Use Case |
|--------|---------------|----------|
| **Static API key** | `anthropic-api-key` | Simple setups, single-organization scanning |
| **Workload Identity Federation (WIF)** | `anthropic-federation-rule-id`, `anthropic-organization-id`, `anthropic-service-account-id` | Enterprise environments, no long-lived secrets |

If neither method is configured, the AI review skips gracefully while the static pin check still executes ([`.github/actions/scan-plugins/action.yml`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/scan-plugins/action.yml) lines 16-24).

## Claude Policy Scan: Deep Code Analysis

For each target that passes the pin check, the action performs an AI-powered security review through [`scripts/scan.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/scripts/scan.sh).

### Secure Repository Cloning

The action clones plugin repositories at their exact pinned SHA into temporary directories. To prevent SSRF attacks, it enforces an allowlist of hosts (lines 78-86):

```bash
ALLOWED_HOSTS=("github.com" "gitlab.com" "bitbucket.org")

```

### Headless Claude CLI Invocation

The scan invokes the Claude CLI (`claude -p`) with strict operational constraints defined in [`scripts/scan.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/scripts/scan.sh) (lines 17-22):

- **Read-only tools only**: `Read`, `Glob`, `Grep`
- **Policy prompt**: [`policy/prompt.md`](https://github.com/anthropics/claude-plugins-community/blob/main/policy/prompt.md) (configurable via `policy-prompt` input)
- **JSON schema validation**: [`policy/schema.json`](https://github.com/anthropics/claude-plugins-community/blob/main/policy/schema.json)
- **Timeout control**: `scan-timeout-secs` (default 300 seconds)

### Verdict Processing

The action parses Claude's JSON response to extract:

- `passes` – boolean approval status
- `summary` – human-readable assessment
- `violation`s – specific policy failures

Results merge with static pin-check data, then emit GitHub annotations:

- `::warning` for policy failures (default, non-blocking)
- `::error` when `fail-on-findings: "true"` (lines 54-62)

## Workflow Configuration Examples

### Standard Pull Request Scanning

```yaml

# .github/workflows/scan-plugins.yml

name: Scan Plugins
on:
  pull_request:
    paths:
      - '.claude-plugin/**'

jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: anthropics/claude-plugins-community/.github/actions/scan-plugins@<PINNED-SHA>
        with:
          anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
          fail-on-findings: "true"
          fail-on-unpinned-autoexec: "true"

```

### Static-Only Linting (No AI Review)

```yaml
- uses: anthropics/claude-plugins-community/.github/actions/scan-plugins@<PINNED-SHA>
  with:
    anthropic-api-key: ""
    fail-on-unpinned-autoexec: "true"

```

### Consuming Scan Outputs

```yaml
- id: scan
  uses: anthropics/claude-plugins-community/.github/actions/scan-plugins@<PINNED-SHA>
  with:
    anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}

- name: Process results
  run: |
    echo "Full verdicts: ${{ steps.scan.outputs.scanned }}"
    echo "Failed plugins: ${{ steps.scan.outputs.failed }}"
    echo "Overall: ${{ steps.scan.outputs.result }}"

```

## Result Reporting and Outputs

After processing all entries, the action generates:

1. **Step summary table** – pass/fail status, unpinned launcher flags, network/install indicators, and summaries per plugin
2. **Three structured outputs** ([`.github/actions/scan-plugins/action.yml`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/scan-plugins/action.yml) lines 84-115):

| Output | Description |
|--------|-------------|
| `scanned` | Full JSON array of all verdicts |
| `failed` | Array of plugin names that failed policy |
| `result` | Aggregate status: `pass`, `fail`, or `skipped` |

## Key Source Files

| Path | Purpose |
|------|---------|
| [`.github/actions/scan-plugins/action.yml`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/scan-plugins/action.yml) | Composite action definition, inputs/outputs |
| [`.github/actions/scan-plugins/scripts/scan.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/scan-plugins/scripts/scan.sh) | Core orchestration: target resolution, cloning, CLI invocation |
| [`.github/actions/scan-plugins/scripts/static-pin-check.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/scan-plugins/scripts/static-pin-check.sh) | Deterministic launcher pinning validation |
| [`.github/actions/scan-plugins/policy/prompt.md`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/scan-plugins/policy/prompt.md) | Default Claude policy prompt |
| [`.github/actions/scan-plugins/policy/schema.json`](https://github.com/anthropics/claude-plugins-community/blob/main/.github/actions/scan-plugins/policy/schema.json) | Verdict JSON schema |

## Summary

- The **`scan-plugins` GitHub Action** provides two-layer security scanning: static pin checks (auth-free) plus AI policy review (authenticated)
- **Floating launchers** (`npx`, `uvx`, `bunx`, `pipx`) are flagged as supply-chain risks before runtime
- **Flexible authentication** supports API keys or Workload Identity Federation for enterprise environments
- **Configurable strictness** allows annotation-only warnings or hard-fail enforcement via `fail-on-findings` and `fail-on-unpinned-autoexec`
- **SSRF protection** through host allowlisting and pinned-SHA cloning ensures safe repository access
- All scanning logic is implemented in shell scripts with explicit timeout and read-only tool constraints

## Frequently Asked Questions

### What happens if I don't provide an Anthropic API key?

The AI policy scan skips gracefully, but the **static pin check still executes**. This ensures basic security validation runs on every workflow invocation regardless of authentication state.

### Can the action block merges on security findings?

Yes. Set `fail-on-findings: "true"` to emit `::error` annotations that fail the job, or `fail-on-unpinned-autoexec: "true"` to hard-fail on floating launchers. Use both for maximum enforcement.

### How does the action prevent scanning malicious repositories?

The [`scripts/scan.sh`](https://github.com/anthropics/claude-plugins-community/blob/main/scripts/scan.sh) script validates repository hosts against an allowlist (`github.com`, `gitlab.com`, `bitbucket.org`) and clones only at the exact SHA pinned in the marketplace manifest, eliminating tag-rewriting and redirect attacks.

### What is the default timeout for Claude policy scans?

The default `scan-timeout-secs` is **300 seconds** (5 minutes). Configure this input for slower or faster analysis based on plugin complexity and your runner constraints.