Skip to content

mcyork/skillseal

Repository files navigation

SkillSeal

Cryptographic signing and verification for LLM agent skills and plugins.

Problem

LLM agents install and execute skills and plugins — Markdown files, commands, hooks, and agents that function as installers with full system privileges. There is no standard mechanism to verify who authored them or whether they have been tampered with after publication.

Major agent platforms have converged on SKILL.md as the instruction manifest format — both Claude Code and OpenAI use it. These platforms provide execution and containment (sandboxing, network controls), but none provide a trust layer: no signing, no provenance, no integrity verification. SkillSeal is that missing layer.

What SkillSeal Does

SkillSeal provides a lightweight signing framework for skill packages and plugins:

  • Provenance — Cryptographic signatures (GPG and SSH) tie a skill to a verified author identity
  • Multi-key signing — Authors sign with multiple keys simultaneously (GPG + SSH), so verifiers can check with whichever key type they prefer
  • Integrity — A manifest of SHA-256 hashes ensures no file has been altered
  • Trust policy — A local trust store lets agents and users define who they trust and at what level
  • Multi-attestation — Reviewers and automated scanners can add their own multi-key signatures, building a web of trust
  • Key discovery — Author public keys are resolved via GitHub (github.com/{username}.gpg for GPG, api.github.com/users/{username}/ssh_signing_keys for SSH), requiring no custom infrastructure
  • Pluggable providers — A provider architecture allows new signing methods to be added without modifying core code

Components

Component Purpose
skillseal sign <dir> Sign a skill or plugin with all configured keys
skillseal verify <dir> Verify a skill or plugin (one valid signature suffices)
skillseal sign-all <dir> [--max-depth N] Sign all skills and plugins in a directory (default depth: 3)
skillseal attest <dir> Create a multi-key attestation bundle for a skill
skillseal init <dir> Scaffold a new skill package
skillseal trust <cmd> Manage trust store (add, remove, list, set-policy, override, bundle)
skillseal-sign/SKILL.md Skill that teaches LLMs how to sign
skillseal-verify/SKILL.md Skill that teaches LLMs how to verify
hooks/skillseal-hook.ts Compiled enforcement hook for Claude Code (see hooks/README.md)
skillseal cache-clear Clear cached credentials for all providers

Configuration

SkillSeal uses ~/.skillseal/config.json for signing identity and keys:

{
  "github": "mcyork",
  "author": "Ian McCutcheon",
  "keys": [
    {
      "type": "gpg",
      "fingerprint": "7097CE1EF54E0808FD3855427ED9682FF64286D0"
    },
    {
      "type": "ssh",
      "fingerprint": "SHA256:vZcivMOtxMdRjvcyGpNSjECXhb/wspMSsHO/bfPXBmQ",
      "key_path": "/path/to/.ssh/skillseal_ed25519"
    }
  ]
}

Each key in the keys array becomes a signing provider. When you run skillseal sign, the tool signs with all configured keys simultaneously, producing one signature per provider in the SIGNATURES/ directory.

Bootstrap Verification

Before trusting SkillSeal, verify it. This uses standard GPG — no SkillSeal CLI required:

git clone https://github.com/mcyork/skillseal.git
cd skillseal

# Import the author's public key from GitHub
curl -sL https://github.com/mcyork.gpg | gpg --import

# Verify the signing key fingerprint matches
# Expected: 7097 CE1E F54E 0808 FD38 5542 7ED9 682F F642 86D0
gpg --fingerprint ian@esoup.net

# Verify the verification skill is authentic (GPG signature)
gpg --verify skillseal-verify/SIGNATURES/gpg.sig skillseal-verify/SKILL.md

# Verify the signing skill is authentic
gpg --verify skillseal-sign/SIGNATURES/gpg.sig skillseal-sign/SKILL.md

If GPG reports Good signature from "Ian McCutcheon (SkillSeal) <ian@esoup.net>", the package is authentic and unmodified. You can now trust the tool to verify everything else.

Both demo skills are also signed with SSH (Ed25519). The SSH signatures are in SIGNATURES/ssh.sig and can be verified using ssh-keygen -Y verify.

All commits in this repository are GPG-signed with the same key. On GitHub, every commit should display a Verified badge. If you see unverified commits, investigate before proceeding.

Author keys:

  • GPG: 7097CE1EF54E0808FD3855427ED9682FF64286D0
  • SSH: SHA256:vZcivMOtxMdRjvcyGpNSjECXhb/wspMSsHO/bfPXBmQ (Ed25519)

Install

Option 1: Compiled Binary (recommended)

Standalone binaries are available on GitHub Releases. No Bun or Node.js runtime required.

# Download the binary for your platform
# macOS Apple Silicon:
curl -fSL https://github.com/mcyork/skillseal/releases/latest/download/skillseal-darwin-arm64 -o skillseal
# macOS Intel:
curl -fSL https://github.com/mcyork/skillseal/releases/latest/download/skillseal-darwin-x64 -o skillseal
# Linux x64:
curl -fSL https://github.com/mcyork/skillseal/releases/latest/download/skillseal-linux-x64 -o skillseal
# Linux ARM64 (Raspberry Pi 4+):
curl -fSL https://github.com/mcyork/skillseal/releases/latest/download/skillseal-linux-arm64 -o skillseal

chmod +x skillseal

# Verify the binary checksum
curl -fSL https://github.com/mcyork/skillseal/releases/latest/download/SHA256SUMS -o SHA256SUMS
shasum -a 256 -c SHA256SUMS --ignore-missing   # macOS
# sha256sum -c SHA256SUMS --ignore-missing      # Linux

# Move to your PATH
sudo mv skillseal /usr/local/bin/

Option 2: From Source

Requires Bun.

git clone https://github.com/mcyork/skillseal.git
cd skillseal && bun install
# Run via: bun run skillseal <command>

Quick Start

# Sign a skill (signs with all configured keys)
skillseal sign /path/to/skill-directory

# Sign a plugin (auto-detected via .claude-plugin/plugin.json)
skillseal sign /path/to/plugin-directory

# Verify a skill or plugin (one valid signature suffices)
skillseal verify /path/to/skill-or-plugin-directory

# Scaffold a new skill package
skillseal init /path/to/new-skill

How It Works

Multi-Key Signing

SkillSeal v0.2 introduces a pluggable provider architecture for signing. Authors configure multiple keys in ~/.skillseal/config.json, and every signing operation produces one signature per key type in a SIGNATURES/ directory:

SIGNATURES/
├── gpg.sig    # Detached GPG signature
└── ssh.sig    # SSH signature (ssh-keygen -Y sign)

Verification requires only one valid signature to pass. This means:

  • A verifier with GPG but not SSH can still verify
  • A verifier with SSH but not GPG can still verify
  • Future key types can be added without breaking existing verification

Skills

A signed skill package contains:

my-skill/
├── SKILL.md          # The skill instructions (signed artifact)
├── SIGNATURES/       # One signature per key type
│   ├── gpg.sig       # Detached GPG signature
│   └── ssh.sig       # SSH signature
├── MANIFEST.json     # SHA-256 hashes of all package files
├── TRUST.json        # Author identity with keys[] array
└── ATTESTATIONS/     # Reviewer and scanner signatures

Plugins

A signed plugin contains:

my-plugin/
├── .claude-plugin/
│   └── plugin.json   # Plugin metadata (signed artifact)
├── skills/           # Skill packages
├── commands/         # Slash commands
├── agents/           # Agent definitions (optional)
├── hooks/            # Hook scripts (optional)
├── .mcp.json         # MCP server config (optional)
├── SIGNATURES/       # One signature per key type
│   ├── gpg.sig       # Detached GPG signature of plugin.json
│   └── ssh.sig       # SSH signature of plugin.json
├── MANIFEST.json     # SHA-256 hashes of all plugin files
├── TRUST.json        # Author identity with keys[] array
└── README.md

TRUST.json

The trust metadata file records the author's identity and all signing keys:

{
  "schema_version": "0.3.0",
  "author": {
    "name": "Ian McCutcheon",
    "email": "",
    "github": "mcyork",
    "keys": [
      {
        "type": "gpg",
        "fingerprint": "7097CE1EF54E0808FD3855427ED9682FF64286D0",
        "key_url": "https://github.com/mcyork.gpg"
      },
      {
        "type": "ssh",
        "fingerprint": "SHA256:vZcivMOtxMdRjvcyGpNSjECXhb/wspMSsHO/bfPXBmQ",
        "key_url": "https://api.github.com/users/mcyork/ssh_signing_keys"
      }
    ]
  },
  "attestations": []
}

Plugins are the distribution unit — one set of signatures covers all skills, commands, hooks, agents, and MCP configs within the plugin. The sign and verify commands auto-detect whether a directory is a plugin (has .claude-plugin/plugin.json) or a skill (has SKILL.md).

Verification fetches the author's public key from GitHub, validates the signature (any one valid signature suffices), checks manifest integrity, and applies trust policy from the local store at ~/.skillseal/trust-store.json.

Security: Signing Key Cache and Passphrase

SkillSeal uses GPG and SSH for signing operations. Both can cache credentials:

GPG delegates passphrase handling to gpg-agent, which caches passphrases by default. If your passphrase is cached, an LLM agent can sign skills without prompting you for approval.

SSH keys may be loaded in ssh-agent. If the key is loaded, signing is silent.

Default GPG cache behavior

Setting Default Meaning
default-cache-ttl 600 Seconds the passphrase stays cached after last use (10 min)
max-cache-ttl 7200 Maximum cache lifetime regardless of use (2 hours)

Hardening options

To require a passphrase prompt on every GPG signing operation, add this to ~/.gnupg/gpg-agent.conf:

ignore-cache-for-signing

Then reload the agent:

gpgconf --kill gpg-agent

For SSH, remove the key from the agent:

ssh-add -d /path/to/.ssh/skillseal_ed25519

Recommendations

  • Personal use with trusted LLM agents: Default caching is reasonable. Your passphrase protects the key at rest; the cache window is limited.
  • Shared machines or untrusted agents: Enable ignore-cache-for-signing for GPG. Remove SSH keys from agent.
  • CI/CD or automated signing: Use dedicated signing keys with no passphrase, stored in a secured environment with restricted access.

Key Cache and Offline Verification

SkillSeal caches fetched public keys locally at ~/.skillseal/key-cache/:

~/.skillseal/key-cache/
├── gpg/
│   └── mcyork.asc       # GPG public key
└── ssh/
    └── mcyork.json       # SSH signing keys

Keys are cached on every successful GitHub fetch and served from cache when GitHub is unavailable. There is no TTL — cached keys persist until manually cleared with skillseal cache-clear.

For fully offline environments, set "offline": true in ~/.skillseal/config.json. This reduces the GitHub fetch timeout to 1 second, falling back to cache immediately. Verification works normally as long as cached keys exist for the author.

Hardening the Trust Store

The trust store (~/.skillseal/trust-store.json) is the root of all trust decisions. If a compromised LLM agent can modify it — via prompt injection, malicious skill instructions, or social engineering — the entire signing and verification system is bypassed. The attacker adds their key, signs a malicious skill, and it passes verification.

SkillSeal signs the trust store with all configured keys (stored in ~/.skillseal/trust-store.signatures/), but that only prevents direct file edits — if the signing key caches are warm, the LLM can run skillseal trust add and the signing happens automatically.

Threat model

A malicious skill's instructions tell the LLM agent to:

  1. Run skillseal trust add attacker-github ATTACKER_FINGERPRINT (adds attacker to trust store)
  2. The trust store is re-signed automatically (key caches are warm)
  3. Install and run a malicious skill signed by the attacker (now passes verification)

The attack works because the LLM agent runs as the user and has the same file/key permissions.

Layer 1: GPG passphrase gating (recommended minimum)

Add ignore-cache-for-signing to ~/.gnupg/gpg-agent.conf (see section above). This forces an interactive passphrase prompt on every trust store modification. LLM agents cannot enter passphrases — the operation fails with a TTY error.

Protects against: LLM-initiated skillseal trust add/remove/set-policy commands.

Limitation: The user must enter their passphrase when they legitimately want to modify the trust store. This is a feature, not a bug — it's the human-in-the-loop.

Layer 2: File immutability (recommended)

After configuring your trust store, lock the files:

macOS:

# Lock trust store and its signatures
chflags uchg ~/.skillseal/trust-store.json
chflags uchg ~/.skillseal/trust-store.signatures/*

# To unlock when you need to make changes:
chflags nouchg ~/.skillseal/trust-store.json
chflags nouchg ~/.skillseal/trust-store.signatures/*

Linux:

# Lock (requires sudo)
sudo chattr +i ~/.skillseal/trust-store.json
sudo chattr +i ~/.skillseal/trust-store.signatures/*

# To unlock:
sudo chattr -i ~/.skillseal/trust-store.json
sudo chattr -i ~/.skillseal/trust-store.signatures/*

Protects against: Any write to the trust store files, regardless of key cache state. The LLM agent would need to know to run chflags nouchg first, and a Bash PreToolUse hook can block that (see below).

Layer 3: Root ownership (maximum)

Transfer ownership of trust store files to root:

sudo chown root:wheel ~/.skillseal/trust-store.json ~/.skillseal/trust-store.signatures/*

# To modify, temporarily take ownership back:
sudo chown $(whoami) ~/.skillseal/trust-store.json ~/.skillseal/trust-store.signatures/*
# ... make changes ...
sudo chown root:wheel ~/.skillseal/trust-store.json ~/.skillseal/trust-store.signatures/*

Protects against: All user-level modification attempts. The LLM agent cannot sudo.

Layer 4: Hook-based command blocking

If you use Claude Code, your Bash PreToolUse hook can detect and block commands targeting the trust store:

// In your security-validator.ts hook:
// Block any command that references the trust store
const cmd = payload.tool_input?.command || "";
if (cmd.includes("trust-store.json") || cmd.includes("skillseal trust add") ||
    cmd.includes("skillseal trust remove") || cmd.includes("skillseal trust set-policy") ||
    cmd.includes("chflags nouchg") || cmd.includes("chattr -i")) {
  console.log("BLOCKED: Trust store modification attempt detected");
  process.exit(2);
}

This is defense-in-depth — even if the other layers are misconfigured, the hook stops the agent from running the commands.

Recommended configuration

Environment Layers
Personal, single user Layer 1 (GPG gating) + Layer 2 (immutability)
Shared machine or high-security All four layers
Experimenting / active development Layer 1 only (unlock as needed)

Verifying trust store integrity

To check that your trust store hasn't been tampered with:

# Verify the GPG signature on the trust store
gpg --verify ~/.skillseal/trust-store.signatures/gpg.sig ~/.skillseal/trust-store.json

# Check file flags (macOS)
ls -lO ~/.skillseal/trust-store.json

# Review contents
cat ~/.skillseal/trust-store.json

If GPG reports a bad signature, the trust store has been modified since it was last legitimately saved. Investigate before proceeding.

Trust Model

There are three paths to trust, and one path that blocks.

Path 1: Trusted Author

The skill is signed by an author in your trust store. You know who wrote it and the signature proves it hasn't been tampered with.

Author signs skill → You add author to trust store → Skill runs

Path 2: Trusted Attester, Signed Skill

The author signed the skill, but you don't know them. A reviewer you trust has independently attested the skill — they reviewed it, signed a statement, and published it. You trust the reviewer's judgement.

Author signs skill → Reviewer attests → You trust reviewer → Skill runs

You never need to trust the author. The reviewer's attestation is the trust anchor.

Path 3: Trusted Attester, Unsigned Skill

The author didn't sign the skill at all (no TRUST.json, no SIGNATURES/). A reviewer you trust attested it anyway — they reviewed the content, pinned it by SHA-256 digest and git commit, and signed a statement.

Author publishes skill (unsigned) → Reviewer attests → You trust reviewer → Skill runs

This is how third-party skills from authors who haven't adopted SkillSeal can still be verified. The attestation pins the exact content that was reviewed.

Path 4: Destatement (Negative Attestation)

A trusted reviewer has examined a skill and published a destatement — an attestation with verdict: "reject". This signals that the reviewer found a problem. Destatements block execution regardless of author trust.

Reviewer publishes destatement → You trust reviewer → Skill is BLOCKED

Destatements are checked before any positive trust evaluation. A skill with a valid author signature AND a destatement from a trusted reviewer is still blocked. This gives reviewers the ability to flag dangerous skills even after they have been signed and distributed.

Per-Skill Overrides

If you disagree with a specific destatement, you can override it for a specific skill:

skillseal trust override add my-skill --despite reviewer-github --reason "We reviewed and disagree"
skillseal trust override remove my-skill --despite reviewer-github
skillseal trust override list

Overrides are recorded in the trust store and apply only to the named skill and reviewer combination. The destatement still exists — it is simply bypassed for that skill.

Trust Bundles

Organizations and community curators can publish trust bundles — signed JSON files containing lists of trusted authors and reviewers. Users subscribe to bundles and merge them into their local trust store:

skillseal trust bundle add org/trust-bundle-repo
skillseal trust bundle update
skillseal trust bundle list

On update, SkillSeal fetches the bundle from GitHub, verifies the publisher's signature against the local trust store, and merges new authors and reviewers without overwriting existing entries. Revoked fingerprints in the bundle remove matching keys from the local store.

Policy Defaults

Scenario Default Action
unsigned (no signature, no attestation) refuse
signature_invalid refuse
unknown_author (signed, but author not in trust store, no trusted attestation) prompt
known_author_no_attestations allow
known_author_with_attestations (attestations exist but reviewer not trusted) allow
known_author_stale_attestations (trusted reviewer, but attestation is for an older version) prompt
trusted_reviewer_attested (trusted reviewer, current attestation) allow
trusted_reviewer_destatement (destatement from trusted reviewer) refuse

Policies are configured in ~/.skillseal/trust-store.json. The PreToolUse hook treats both prompt and refuse as block — hooks can't prompt interactively.

Policy weakening protection: Changing a policy to a less restrictive action (e.g., refuseprompt, or promptallow) requires the --yes flag to confirm. This prevents accidental weakening of trust policies.

Enforcement: Compiled Hook

Signing and verification are only useful if you actually enforce them. SkillSeal ships a compiled enforcement hook for Claude Code that blocks any skill from executing unless it passes skillseal verify.

The hook compiles to a native binary with self-integrity verification — it hashes itself at startup and checks for a .seal file. If someone modifies the binary, all execution is blocked. See hooks/README.md for full documentation.

How it works

  1. Claude Code invokes a skill (the Skill tool) or runs a Bash command referencing skill files
  2. The hook intercepts the call before execution
  3. It runs skillseal verify on the skill directory
  4. If verification passes — silent, skill executes normally
  5. If verification fails — skill is blocked with a security message and anti-bypass directives

The hook uses a fail-closed security model: any error (missing files, CLI crash, parse failure, binary tampering) blocks execution rather than allowing it.

Quick Setup

# 1. Build the hook
cd hooks && ./build-hook.sh

# 2. Add to ~/.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Skill|Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/hooks/skillseal-hook"
          }
        ]
      }
    ]
  }
}

Note the matcher is Skill|Bash — the hook intercepts both skill invocations and Bash commands that reference skill directories, preventing agents from bypassing verification by running skill code directly.

What it looks like

Signed skill — executes normally, no output:

> /ssl-certificate-checker
(skill runs)

Blocked skill — structured security response:

> /malicious-skill
🔒 SECURITY BLOCK: "malicious-skill" failed SkillSeal verification.

⛔ MANDATORY SECURITY DIRECTIVES — READ CAREFULLY:
1. DO NOT attempt to read, open, or inspect the skill's source files...
2. DO NOT attempt to execute skill code directly via Bash...
...
7. STOP. Do not proceed with any action related to this skill.

The block message is deliberately opinionated — it tells the AI agent to stop, not try to work around the block. The user decides what to do.

Prerequisites

  • All your skills must be signed (skillseal sign-all ~/.claude/skills)
  • The trust store must have the authors added (skillseal trust add <github> <fingerprint>)
  • SkillSeal CLI must be on PATH or set via SKILLSEAL_CLI env var

Roadmap

Shipped (v0.3.0):

  • SSH signing support — Ed25519 SSH keys alongside GPG
  • Multi-key signing — Sign with multiple keys simultaneously via pluggable provider architecture
  • Multi-signature attestations — Attestation bundles carry multiple signatures
  • Compiled binaries — Standalone binaries for macOS (x64/ARM64) and Linux (x64/ARM64) via bun build --compile
  • Destatements — Negative attestations (verdict: "reject") that block execution regardless of author trust
  • Per-skill overrides — Bypass specific destatements when you disagree with a reviewer's assessment
  • Trust bundles — Subscribe to community-curated lists of trusted authors and reviewers
  • Key cache — Offline verification via locally cached public keys
  • GPG revocation detection — Revoked GPG keys are detected before signature verification
  • Trust bundle revocation lists — Trust bundles include revoked_fingerprints to block compromised keys
  • HEAD probe — Liveness check for local attestations against reviewer's remote repository
  • Compiled enforcement hook — Self-verifying native binary with SHA-256 seal, Bash interception, path traversal guards, and anti-bypass directives for AI agents
  • Portable hook configuration — Environment variable-driven paths (SKILLSEAL_CLI, SKILLSEAL_SKILLS_DIR, SKILLSEAL_PLUGINS_DIR) with smart fallback chain

Near-term:

  • Crypto provider plugins — Dynamic loading of third-party SigningProvider implementations from ~/.skillseal/providers/. Plugin packages are signed skill packages — SkillSeal's own trust model protects its extensions. Enables: minisign, sigstore/cosign, post-quantum signatures, hardware tokens (YubiKey/PKCS#11).

Strategic:

  • Agent-agnostic verification — Configurable signed artifact (not hardcoded to SKILL.md/plugin.json), enabling any LLM agent framework to adopt SkillSeal for instruction-file integrity.
  • Windows compatibility — Resolve POSIX-specific code paths (/dev/stdin, GPG_TTY, chmod) to support GPG4Win and Windows OpenSSH.

Specifications

License

Apache License 2.0 — see LICENSE

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors