56 results available
Sign in
Skills directory

agent-supply-chain

github

·

Last changed
Hardened

This skill has been hardened by Chainguard

Last changed

This version passed the hardening gate. Hardening updated the skill content and independently checked the resulting changes. Classified findings before hardening: 3. Classified findings closed: 0. Classified findings remaining: 3. Unclassified scanner findings remaining: 0. Review findings closed: 4. Review findings remaining: 13.


Open before hardening

3

Medium
Open after hardening

3

Medium

Expand
File
Group
Subtype
Severity
Status
SKILL.md

Sensitive Data & Secret Exposure

Agent snooping

Medium
Open
SKILL.md

Supply Chain & Install-Time Trust

Dependency or plugin compromise

Medium
Open
SKILL.md

Prompt Injection

—

Medium
Open

3 findings


SKILL.md

Agent Supply Chain Integrity

Generate and verify integrity manifests for AI agent plugins and tools. Detect file drift, review version pinning, and support supply chain provenance work.

Overview

Agent plugins and MCP servers have the same supply chain risks as npm packages or container images. However, the ecosystem has no equivalent of npm provenance, Sigstore, or SLSA. The manifests in this skill help detect file drift. Real provenance still requires Sigstore signing and SLSA attestations.

Plugin Directory → Hash All Files (SHA-256) → Generate INTEGRITY.json
                                                    ↓
Later: Plugin Directory → Re-Hash Files → Compare Against INTEGRITY.json
                                                    ↓
                                          Match? VERIFIED : TAMPERED

Trust boundary: an unsigned INTEGRITY.json stored inside the plugin only detects accidental drift. Anyone who can change the plugin files can also regenerate the manifest. For real tamper evidence, take two steps:

  • Sign the manifest with Sigstore, or record manifest_hash in a protected place outside the plugin directory.
  • Treat verification and promotion as failed unless the signature or the recorded value matches.

Treat every result from these patterns as advisory. A person should review the result before relying on it.

When to Use

  • Before promoting a plugin from development to production
  • During code review of plugin PRs
  • As a CI step to check recorded files against a trusted manifest after review. See the CI caveats below.
  • When auditing third-party agent tools or MCP servers
  • Building a plugin marketplace with integrity requirements

Pattern 1: Generate Integrity Manifest

Create an INTEGRITY.json with SHA-256 hashes of all plugin files. The file map and manifest_hash are deterministic for the same files. The generated_at value changes on each run.

import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path

EXCLUDE_DIRS = {".git", "__pycache__", "node_modules", ".venv", ".pytest_cache"}
EXCLUDE_FILES = {".DS_Store", "Thumbs.db", "INTEGRITY.json"}

def hash_file(path: Path) -> str:
    """Compute SHA-256 hex digest of a file."""
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

def generate_manifest(plugin_dir: str) -> dict:
    """Generate an integrity manifest for a plugin directory."""
    root = Path(plugin_dir)
    files = {}

    for path in sorted(root.rglob("*")):
        if not path.is_file():
            continue
        if path.name in EXCLUDE_FILES:
            continue
        if any(part in EXCLUDE_DIRS for part in path.relative_to(root).parts):
            continue
        rel = path.relative_to(root).as_posix()
        files[rel] = hash_file(path)

    # Chain hash: SHA-256 of all file hashes concatenated in sorted order
    chain = hashlib.sha256()
    for key in sorted(files.keys()):
        chain.update(files[key].encode("ascii"))

    manifest = {
        "plugin_name": root.name,
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "algorithm": "sha256",
        "file_count": len(files),
        "files": files,
        "manifest_hash": chain.hexdigest(),
    }
    return manifest

# Generate and save
manifest = generate_manifest("my-plugin/")
Path("my-plugin/INTEGRITY.json").write_text(
    json.dumps(manifest, indent=2) + "\n"
)
print(f"Generated manifest: {manifest['file_count']} files, "
      f"hash: {manifest['manifest_hash'][:16]}...")

Output (INTEGRITY.json):

{
  "plugin_name": "my-plugin",
  "generated_at": "2026-04-01T03:00:00+00:00",
  "algorithm": "sha256",
  "file_count": 12,
  "files": {
    ".claude-plugin/plugin.json": "a1b2c3d4...",
    "README.md": "e5f6a7b8...",
    "skills/search/SKILL.md": "c9d0e1f2...",
    "agency.json": "3a4b5c6d..."
  },
  "manifest_hash": "7e8f9a0b1c2d3e4f..."
}

The paths in this output are example values only.

Limits of this pattern:

  • Excluded names apply at any depth. Excluded directory names and excluded file names are matched at every level of the tree. Code placed in them is neither hashed nor reported as untracked. Treat any excluded directory found inside a plugin, such as node_modules, as a promotion failure until a person has reviewed it. Verify dependencies separately through lockfile integrity hashes.
  • Symbolic links are followed. A link can bring files from outside the plugin into the manifest. Treat any symbolic link, or any link that resolves outside the plugin directory, as a verification failure that needs review. This applies to Patterns 1 and 2 and to CI.
  • Third-party plugins. When auditing a third-party plugin, write the manifest to a location outside the plugin directory. Get the user's confirmation before replacing an existing manifest.

Pattern 2: Verify Integrity

Check that current files match the manifest.

# Requires: hash_file() and generate_manifest() from Pattern 1 above
import json
from pathlib import Path

def verify_manifest(plugin_dir: str) -> tuple[bool, list[str]]:
    """Verify plugin files against INTEGRITY.json."""
    root = Path(plugin_dir)
    manifest_path = root / "INTEGRITY.json"

    if not manifest_path.exists():
        return False, ["INTEGRITY.json not found"]

    manifest = json.loads(manifest_path.read_text())
    recorded = manifest.get("files", {})
    errors = []

    # Check recorded files
    for rel_path, expected_hash in recorded.items():
        full = root / rel_path
        if not full.exists():
            errors.append(f"MISSING: {rel_path}")
            continue
        actual = hash_file(full)
        if actual != expected_hash:
            errors.append(f"MODIFIED: {rel_path}")

    # Check for new untracked files
    current = generate_manifest(plugin_dir)
    for rel_path in current["files"]:
        if rel_path not in recorded:
            errors.append(f"UNTRACKED: {rel_path}")

    return len(errors) == 0, errors

# Verify
passed, errors = verify_manifest("my-plugin/")
if passed:
    print("VERIFIED: All files match manifest")
else:
    print(f"FAILED: {len(errors)} issue(s)")
    for e in errors:
        print(f"  {e}")

Output on tampered plugin:

FAILED: 3 issue(s)
  MODIFIED: skills/search/SKILL.md
  MISSING: agency.json
  UNTRACKED: backdoor.py

Treat manifest content as untrusted input. Apply these checks before acting on a manifest:

  • Read only files that stay inside the plugin directory. Reject any manifest entry that is an absolute path or points outside the plugin directory, and do this before reading any file.
  • Limit the file size before parsing.
  • Confirm that algorithm is sha256 and that each files entry maps a path to a sha256 hex digest.
  • Treat any parse error or type error as not verified.

Pattern 3: Dependency Version Audit

Check that agent dependencies use pinned versions.

import re
import json
from pathlib import Path

def audit_versions(config_path: str) -> list[dict]:
    """Audit dependency version pinning in a config file."""
    findings = []
    path = Path(config_path)
    content = path.read_text()

    if path.name == "package.json":
        data = json.loads(content)
        for section in ("dependencies", "devDependencies"):
            for pkg, ver in data.get(section, {}).items():
                if ver.startswith("^") or ver.startswith("~") or ver == "*" or ver == "latest":
                    findings.append({
                        "package": pkg,
                        "version": ver,
                        "severity": "HIGH" if ver in ("*", "latest") else "MEDIUM",
                        "fix": f'Pin to exact: "{pkg}": "{ver.lstrip("^~")}"'
                    })

    elif path.name in ("requirements.txt", "pyproject.toml"):
        for line in content.splitlines():
            line = line.strip()
            if ">=" in line and "<" not in line:
                findings.append({
                    "package": line.split(">=")[0].strip(),
                    "version": line,
                    "severity": "MEDIUM",
                    "fix": f"Add upper bound: {line},<next_major"
                })

    return findings

These checks are heuristics. They catch only obvious cases. Bare package names, many version ranges, and tags still pass. Keep these points in mind:

  • Only an exact version pin counts as pinned.
  • Rely on lockfiles with integrity hashes for real pinning.
  • For a star or latest finding, resolve a concrete version and pin it. The suggested fix text is a starting point, not a final pin.

Pattern 4: Promotion Gate

Use integrity verification as a gate before promoting plugins. This pattern requires verify_manifest from Pattern 2, plus the functions from Pattern 1.

import json
from pathlib import Path

def promotion_check(plugin_dir: str) -> dict:
    """Check if a plugin is ready for production promotion."""
    checks = {}

    # 1. Integrity manifest exists and verifies
    passed, errors = verify_manifest(plugin_dir)
    checks["integrity"] = {
        "passed": passed,
        "errors": errors
    }

    # 2. Required files exist
    root = Path(plugin_dir)
    required = ["README.md"]
    missing = [f for f in required if not (root / f).exists()]

    # Require at least one plugin manifest (supports both layouts)
    manifest_paths = [
        root / ".github/plugin/plugin.json",
        root / ".claude-plugin/plugin.json",
    ]
    if not any(p.exists() for p in manifest_paths):
        missing.append(".github/plugin/plugin.json (or .claude-plugin/plugin.json)")

    checks["required_files"] = {
        "passed": len(missing) == 0,
        "missing": missing
    }

    # 3. No unpinned dependencies
    mcp_path = root / ".mcp.json"
    if mcp_path.exists():
        config = json.loads(mcp_path.read_text())
        unpinned = []
        for server in config.get("mcpServers", {}).values():
            if isinstance(server, dict):
                for arg in server.get("args", []):
                    if isinstance(arg, str) and "@latest" in arg:
                        unpinned.append(arg)
        checks["pinned_deps"] = {
            "passed": len(unpinned) == 0,
            "unpinned": unpinned
        }

    # Overall
    all_passed = all(c["passed"] for c in checks.values())
    return {"ready": all_passed, "checks": checks}

result = promotion_check("my-plugin/")
if result["ready"]:
    print("Plugin is ready for production promotion")
else:
    print("Plugin NOT ready:")
    for name, check in result["checks"].items():
        if not check["passed"]:
            print(f"  FAILED: {name}")

Limits of this gate:

  • The pinning check here flags only the latest tag.
  • It runs only when the MCP config file is present.
  • Package runner arguments with no version at all are not flagged.
  • The integrity check trusts the manifest inside the plugin; see the trust boundary in the Overview.

A passing result therefore does not prove that dependencies are pinned or that files are authentic. A person should review the result before promotion.


CI Integration

Read these caveats before using the example step:

  • Recorded files only. The inline step detects missing or modified files that the manifest records. It does not detect new untracked files. For complete coverage, run the full verify_manifest logic from Pattern 2, ideally from a checked-in, reviewed verifier script invoked by path.
  • Trusted manifest source. Compare against a manifest from a trusted, reviewed source. A manifest supplied by the change under test can simply be regenerated by that change.
  • Static matrix values. Matrix values must be static directory names defined in the workflow, never taken from pull request or user data. Pass the value through the step environment and check it against an allowed list of plugin paths before changing directory. Use read-only job permissions. Run the step for untrusted pull requests only with read-only access.
  • Trusted working directory. Run the verifier from a trusted working directory outside the plugin under check, and pass the plugin directory in. A plugin can ship modules that Python would load from the current directory.
  • Untrusted file names. Manifest keys are untrusted. Reject or escape keys that contain control characters, line breaks, or double colons before printing them as workflow messages.

Add to your GitHub Actions workflow:

- name: Verify plugin integrity
  run: |
    PLUGIN_DIR="${{ matrix.plugin || '.' }}"
    cd "$PLUGIN_DIR"
    python -c "
    from pathlib import Path
    import json, hashlib, sys

    def hash_file(p):
        h = hashlib.sha256()
        with open(p, 'rb') as f:
            for c in iter(lambda: f.read(8192), b''):
                h.update(c)
        return h.hexdigest()

    manifest = json.loads(Path('INTEGRITY.json').read_text())
    errors = []
    for rel, expected in manifest['files'].items():
        p = Path(rel)
        if not p.exists():
            errors.append(f'MISSING: {rel}')
        elif hash_file(p) != expected:
            errors.append(f'MODIFIED: {rel}')
    if errors:
        for e in errors:
            print(f'::error::{e}')
        sys.exit(1)
    print(f'Verified {len(manifest[\"files\"])} files')
    "

Best Practices

PracticeRationale

Generate manifest after code review

Ensures reviewed code matches production code

Include manifest in the PR

Reviewers can verify what was hashed

Verify in CI before deploy

Catches post-review changes to recorded files; add the untracked check from Pattern 2 to catch added files

Anchor the manifest outside the plugin

Sign it with Sigstore or record manifest_hash in a protected place, so a regenerated manifest is detected

Treat the chain hash as a summary only

It covers file digests only, not file paths or metadata, and Pattern 2 and CI do not check it again

Review excluded directories

Excluded names such as node_modules are skipped at any depth; verify dependencies separately through lockfile integrity hashes

Deploy the verified artifact

After verification, package the plugin into an immutable archive or image, record its digest, deploy by that digest, and verify again at install time

Pin all dependency versions

Unpinned deps = different code on every install


  • OpenSSF SLSA — Supply-chain Levels for Software Artifacts
  • npm Provenance — Sigstore-based package provenance
  • Agent Governance Toolkit — Includes integrity verification and plugin signing. This is a third-party toolkit: review it and pin it to a specific release before use.
  • OWASP ASI-09: Supply Chain Integrity

Install

Install with chainctl
chainctl skill install public/github/agent-supply-chain:latest
Get started with chainctl
View source

Harden your own skills

Bring Chainguard's hardening pipeline to your first-party skills — custom rulesets, automated remediation, full audit trail.

Provenance

Upstream sourcehttps://github.com/github/awesome-copilot
Upstream commit2201a49dd6f972ac4f685d03361d58c9c9206690
OCI reference

skills.cgr.dev/public/github.com/github/skills/agent-supply-chain:latest

License

MIT

Files

HARDENING.md

LICENSE

SKILL.md


The trusted source for open source

Talk to an expert
PrivacyTerms

Product

Chainguard ContainersChainguard LibrariesChainguard VMsChainguard OS PackagesChainguard ActionsChainguard Agent SkillsIntegrationsPricing
© 2026 Chainguard, Inc. All Rights Reserved.
Chainguard® and the Chainguard logo are registered trademarks of Chainguard, Inc. in the United States and/or other countries.
The other respective trademarks mentioned on this page are owned by the respective companies and use of them does not imply any affiliation or endorsement.