Invariant Checks Enforced by the validate-plugins Action in the Claude Plugins Community

The validate-plugins GitHub Action enforces eleven hard-coded policy invariants (I1–I11) that govern alphabetical sorting, description length, HTTPS enforcement, SHA pinning, filename consistency, and shell-character safety across the Claude plugin marketplace.

The validate-plugins composite action serves as the policy gatekeeper for the anthropics/claude-plugins-community repository. After canonical schema validation completes via claude plugin validate, step 11 executes a Bash script that applies these marketplace invariants independently of the upstream Zod schema. This ensures safety, consistency, and quality standards before any external code enters the ecosystem.

The Eleven Policy Invariants

The core validation logic resides in .github/actions/validate-plugins/scripts/11-validate-invariants.sh. Each invariant detection emits a GitHub annotation via the flag function and can be downgraded to a warning via the warn-invariants input.

I1: Alphabetical Sorting

The marketplace entries must be sorted alphabetically by the name field using case-insensitive comparison. At lines 91–94 of the validation script, the implementation extracts names using jq, compares the original order against a sorted copy, and flags "I1" when discrepancies exist.

I2: Unique Plugin Names

Every plugin name must be unique across the entire marketplace. Lines 95–98 detect duplicates by grouping entries with jq and flag "I2" when any name appears more than once, preventing namespace collisions.

I3: Description Length and Whitespace

Descriptions must contain between 10 and 2000 characters and cannot have leading or trailing whitespace. The entry loop at lines 14–19 calculates string length (len=${#desc}) and checks whitespace boundaries, flagging "I3" for violations of either constraint.

I4: HTTPS-Only URLs

External sources must use HTTPS URLs matching the pattern ^https://[A-Za-z0-9./_-]+$ (or the owner/repo shorthand for GitHub repositories). Lines 30–35 apply this regex test to source.url and flag "I4" when non-HTTPS protocols or malformed paths are detected.

I5: SHA Pinning

Every external source must specify a 40-character lowercase hexadecimal SHA (matching ^[0-9a-f]{40}$). Lines 36–48 validate this format and flag "I5" unless the plugin appears in the sha-exempt input list, ensuring reproducible builds and supply-chain integrity.

I6: Filename Matches Plugin Name

In per-file repository configurations, each plugins/<name>.json file must declare a .name field identical to its filename (without extension). Lines 60–66 iterate over $ENTRIES_DIR/*.json and flag "I6" when the basename mismatches the declared name, preventing file-to-content desynchronization.

I7: No Direct Marketplace Edits

Pull requests must modify individual entry files rather than the assembled marketplace.json directly. Lines 67–69 use git diff combined with grep -qxF to detect changes to the marketplace file itself, flagging "I7" when direct edits are detected and enforcing proper contribution workflow.

I8: Vendored Source Existence

For vendored sources using local paths (e.g., "./path"), the directory must contain a .claude-plugin/plugin.json file. The existence check at lines 71–84 validates this structure and flags "I8" if the required plugin metadata file is missing.

I9: Shell-Character Safety

All string fields under source must exclude shell metacharacters including $, backticks (`), semicolons (;), ampersands (&), pipes (|), parentheses ((, )), redirection operators (<, >), spaces, tabs, quotes, and backslashes. Lines 50–56 invoke has_unsafe_chars from .github/actions/validate-plugins/lib/common.sh (lines 37–44) to flag "I9" when dangerous characters appear, preventing command injection vulnerabilities.

I10: Hidden Unicode Detection

Plugin names and descriptions must not contain zero-width or bidirectional control characters (U+200B, U+200C, U+200D, U+200E, U+200F, U+202A–202E, U+2066–2069, U+FEFF). Lines 99–102 check against the pre-computed HIDDEN_UNI character set and flag "I10" if any hidden Unicode appears, mitigating homograph attacks and visual spoofing.

I11: Name Format Specification

Plugin names must match the regex ^[a-z0-9][a-z0-9-]{1,63}$, enforcing lowercase alphanumerics with optional hyphens and a length of 2–64 characters. Lines 7–10 perform this validation inside the entry loop and flag "I11" when names contain uppercase letters, underscores, or invalid characters.

Configuration and Error Handling

The action supports granular control over invariant enforcement through two inputs:

  • warn-invariants: Accepts a space-separated list of invariant codes (default: I1 I3 I5 I8) that emit warnings rather than errors. This allows marketplace maintainers to enforce strictness gradually without blocking contributions.
  • scope-errors-to-changed: When set to true, violations on entries not modified by the current pull request downgrade from error to warning. This prevents stale base-branch defects from blocking unrelated updates.

Implementation Architecture

The invariant validation layer consists of two critical files:

Each violation generates machine-readable annotations in the format:

::error file=marketplace.json::invariant I2: duplicate plugin names: dup

Example Violations

Below are minimal JSON snippets that trigger specific invariants when processed by the action:

I1 (Unsorted Names):

{
  "plugins": [
    {"name":"zzz","description":"Valid description","source":"./z"},
    {"name":"aaa","description":"Valid description","source":"./a"}
  ]
}

I4 (Non-HTTPS URL):

{
  "plugins": [
    {"name":"badurl","description":"Valid description","source":{"source":"url","url":"http://example.com/x","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
  ]
}

I5 (Missing SHA):

{
  "plugins": [
    {"name":"nosha","description":"Valid description","source":{"source":"url","url":"https://github.com/x/y"}}
  ]
}

I9 (Unsafe Shell Characters):

{
  "plugins": [
    {"name":"shell","description":"Valid description","source":{"source":"url","url":"https://github.com/x/y;rm -rf /","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
  ]
}

I10 (Zero-Width Space in Description):

{
  "plugins": [
    {"name":"zwsp","description":"hello​world ten chars","source":"./x"}
  ]
}

I11 (Invalid Name Format):

{
  "plugins": [
    {"name":"Bad_Name","description":"Valid description","source":"./x"}
  ]
}

Summary

  • The validate-plugins action enforces eleven specific invariants (I1–I11) through .github/actions/validate-plugins/scripts/11-validate-invariants.sh, operating independently of JSON schema validation.
  • I1 and I2 ensure alphabetical ordering and name uniqueness across the marketplace catalog.
  • I3 validates description length constraints (10–2000 characters) and prohibits boundary whitespace.
  • I4 mandates HTTPS URLs while I5 requires 40-character hexadecimal SHA pinning for external sources to prevent supply-chain attacks.
  • I6, I7, and I8 govern repository structure, prohibiting direct marketplace.json edits and verifying vendored path existence.
  • I9 provides security hardening against shell injection via the has_unsafe_chars helper in lib/common.sh.
  • I10 blocks hidden Unicode characters (U+200B, bidi controls) that enable visual spoofing.
  • I11 enforces strict naming conventions: lowercase alphanumeric with hyphens, 2–64 characters, matching ^[a-z0-9][a-z0-9-]{1,63}$.
  • Violations can be downgraded to warnings via warn-invariants or scoped to changed files via scope-errors-to-changed.

Frequently Asked Questions

How can I configure certain invariants to show warnings instead of errors?

Pass a space-separated list of invariant codes to the warn-invariants input. The default configuration (I1 I3 I5 I8) treats these specific invariants as warnings, while all others fail the workflow immediately. This allows gradual adoption of stricter policies without blocking existing contributions.

Why does the action require SHA pinning for external plugins?

Invariant I5 mandates 40-character lowercase hexadecimal SHAs to ensure reproducible builds and prevent supply-chain attacks where upstream repositories might change code unexpectedly. Only plugins explicitly listed in the sha-exempt input may omit this requirement, providing a controlled exception mechanism for trusted sources.

What is the difference between I6 and I7?

I6 validates that individual JSON entry filenames match their internal name field in per-file repository setups, ensuring the file plugins/example.json contains "name": "example". I7 prevents contributors from manually editing the assembled marketplace.json file directly, enforcing that all changes flow through proper individual entry files to maintain automation integrity.

How does the action prevent shell injection attacks?

Invariant I9 utilizes the has_unsafe_chars function defined in .github/actions/validate-plugins/lib/common.sh (lines 37–44) to scan all strings under the source object for metacharacters including $, backticks, semicolons, pipes, and redirection operators. If detected, the action flags "I9" and blocks the submission before any shell execution occurs, neutralizing command injection vectors.

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 →