AI Agent Skill Signing: Tool Schema Integrity for Internal Agents
A support agent ships with a marketplace "refund helper" skill. Security reviewed the SKILL.md, the install dialog looked clean, and the first week was uneventful. Then the publisher pushed a quiet update, and a nested dependency changed with it. The skill still has the same name and stars. The agent still has production CRM credentials. Call-time schema validation still passes, because the poisoned schema is still valid JSON. The failure was admission: unsigned, unpinned bytes entered the runtime after someone said yes once.
You need a signed skill and tool-schema integrity contract: every skill tree and tool schema is cryptographically signed, publisher keys are TOFU-pinned, updates carry verifiable lineage, unsigned or revoked artifacts fail closed, and production loads only from an internal mirror of approved content hashes. It sits beside securing MCP servers for internal AI workflows (origin allowlists and metadata consent), validating AI agent tool calls before execution (schema and authz on the proposed call), propagating tool-output taint before privileged AI agent actions (runtime provenance after results return), instruction provenance contracts for AI agent delegation (harness message-role reconstruction), and AI agent audit logs (accountability outside model control). The job here is narrower: prove the package bytes the agent is about to load are the same bytes you approved.
Why the skill layer is a package problem without a lockfile
OWASP's Agentic Skills Top 10 treats skills as the execution layer between models and tools. MCP says how a model talks to tools. Skills decide what those tools actually do across multi-step workflows. That makes them first-class supply-chain artifacts, not passive configuration.
AST02 - Supply Chain Compromise describes the maturity gap. Skill registries often lack the provenance controls common in npm or PyPI. Attackers use coordinated uploads, dependency confusion, account takeover, repository poisoning, and config files that become execution paths when a repo opens. Mitigations OWASP names are familiar on purpose: code-signing identity over SKILL.md plus every declared resource, hash-pinned nested dependencies, internal mirrors and allowlists, and revocation of keys, versions, or publishers.
Mend's enterprise reading of AST10 makes the transfer explicit. Nine of the ten risks are package-ecosystem problems the industry already knows how to handle. Scanners are cheap to evade. Marketplace reputation is transferable between registries. The durable control point is the repository and the admit gate that pins content hashes.
The attack: approval, then substitution
Invariant Labs' MCP tool-poisoning research shows why "I approved it last week" is not a security boundary. A malicious server can change tool descriptions after the client approved them. That rug pull keeps the same server identity while rewriting what the model sees. Shadowing across multiple servers can redirect trusted tools without ever putting the attacker tool in the user-facing log.
The same pattern appears for skill directories and local tool schemas. A benign first version earns TOFU trust. A later version, still signed by a compromised or malicious publisher key, ships different instructions, hooks, or nested packages. Call-time validation still passes. Sandboxes still run. Human approval may even fire on the wrong trust basis if reviewers only see a friendly skill name.
AST02's SkillJacking and nested-dependency examples make the surface-scan failure concrete. A clean top-level SKILL.md can pull a typosquatted package. An unpinned remote document a skill fetches at runtime can flip after review. If your gate only hashes the marketing README, you are not verifying the skill.
Sign the tree, not the title
Treat every skill directory and every tool schema as a signed artifact.
SchemaPin defines ECDSA P-256 signatures over SHA-256 digests of canonicalized tool schemas and entire skill directories. Skill signing hashes every file except the signature manifest, builds a file map, signs the root hash, and writes .schemapin.sig. Offline verification uses a discovery document or trust bundle so CI and air-gapped hosts do not depend on a live marketplace fetch at load time.
Minimum admit rules:
- Canonicalize, then sign. Same bytes must hash the same way across languages. Do not sign pretty-printed JSON or an incomplete file list.
- Cover the whole skill tree.
SKILL.md, hooks, configs, scripts, and declared resources all belong in the manifest. A signature over the marketing description alone is theater. - Verify before load. The loader refuses
UNSIGNED,INVALID, andREVOKEDstatuses. Unsigned "temporary" skills in production are just unsigned skills. - Keep keys out of agent context. Signing keys live in CI or a KMS. Agents receive verified skill bytes, never publisher private keys.
This complements MCP origin allowlists from the MCP security guide. An approved origin still needs authenticated bytes. An approved origin with mutable tool text is how rug pulls win.
Pin keys and enforce lineage on every update
Require previous_hash lineage besides signatures; a re-signed rug pull still passes TOFU when the publisher key is compromised. That is the first operating rule.
SchemaPin's TOFU model pins a publisher public key on first trusted use and rejects silent key changes. That stops casual substitution by a different signer. It does not stop a compromised or malicious signer from re-signing a hostile update under the same key. Schema version binding adds schema_version and previous_hash so each release declares its predecessor. Verifiers that keep a per-tool latest_known_hash can refuse missing lineage or mismatched predecessors instead of silently rolling forward.
Operational pattern:
| Event | Required check |
|---|---|
| First install | Human-confirmed TOFU pin of publisher key + content hash |
| Same-key update | Cryptographic verify + verify_chain against latest_known_hash |
| Key rotation | Explicit operator acceptance of new pin + revocation of old key |
| Revocation notice | Fail closed on key, version digest, or publisher identity |
| Expired signature | Policy degrade or refuse for high-risk tools; never ignore silently |
The SchemaPin technical specification also documents revocation lists, optional expires_at, DNS TXT cross-checks, and offline trust bundles. Use them as policy inputs. Cryptographic validity alone is not admission.
Mirror inside the enterprise; do not trust the bazaar
Admit skills from an internal mirror and content-hash pin, not marketplace reputation or scanner green lights. That is the second operating rule.
AST02 and Mend both argue that automated scanning cannot replace supply-chain judgment. Trail of Bits-style bypasses and malicious scanners published as skills show why a green badge is not evidence. Enterprises already know the answer for packages: private mirrors, allowlists, pinned digests, and change management.
Build that for agents:
- Maintain an inventory of name, version, content hash, publisher pin, installer identity, and last verification time.
- Copy approved skill trees into an internal registry keyed by content hash, not by floating tags.
- Require change tickets for hash roll-forwards the same way you require them for production container digests.
- Reconcile runtime loads against the inventory so a laptop-installed community skill cannot silently appear in a managed agent pool.
Hash-pin nested dependencies and external skill references; surface SKILL.md can stay clean while a nested or remote payload flips. That is the third operating rule. Pin requirements.txt and lockfiles to sha256: digests. Snapshot external docs a skill instructs the agent to fetch, or record the resolved digest on every load and refuse drift. A skill that says "read the latest runbook from this URL" without a digest is an open supply-chain socket.
Fail closed at the loader
Put the decision outside the model:
function admitSkill(package, policy):
if package.signature is missing:
return DENY("unsigned")
result = verifyOffline(package, policy.trust_bundle, policy.pin_store)
if not result.valid:
return DENY(result.error)
if result.key_revoked or result.publisher_revoked:
return DENY("revoked")
if policy.requires_lineage and not chainOk(package, policy.latest_known_hash):
return DENY("lineage")
if package.content_hash not in policy.internal_mirror:
return DENY("not_mirrored")
if nestedDepsUnpinned(package):
return DENY("unpinned_deps")
audit.admit(package, result)
policy.latest_known_hash[package.name] = package.content_hash
return ALLOW(package.verified_bytes)
Emit the admit decision to your existing audit plane. Do not ask the model whether the signature "looks fine."
Worked example: refund helper skill
A platform team wants a refund helper skill for the internal support agent.
- Publisher signs the skill directory in CI and publishes
.schemapin.sigwithschema_version=1.0.0. - Security reviews the tree, verifies offline against the publisher discovery document, pins the key, and copies content hash
sha256:aaa…into the internal mirror. - Production loaders only accept that hash. Nested Python deps are pinned by hash. The skill's external "policy notes" URL is snapshotted into the reviewed artifact.
- Two weeks later the publisher ships
1.0.1signed by the same key but withoutprevious_hash. The loader denies lineage. No agent sees the new instructions. - After review,
1.0.1is resigned withprevious_hash=sha256:aaa…, mirrored assha256:bbb…, and rolled forward by ticket. Runtime taint and call-time validation still run, but they never have to be the first line of defense against a substituted package.
How this contract fits the rest of the stack
| Control | Owns | Does not replace |
|---|---|---|
| Skill/schema integrity (this article) | Admit-time authenticity of package bytes | Call-time authz, taint, egress |
| MCP server security | Origin, token audience, metadata consent | Content-hash lineage of skill files |
| Tool-call validation | Schema and business checks on one call | Whether the schema publisher is authentic |
| Tool-output taint | Provenance after results return | Preventing hostile bytes from loading |
| Instruction provenance | Non-escalating harness reconstruction | Signing the skill that seeded the prompt |
| Audit logs | Reconstructable admit and deny events | Cryptographic verification itself |
Promote when you can inventory every loadable skill and tool schema, verify signatures offline, enforce lineage on updates, refuse unsigned and revoked artifacts, and prove nested dependencies and external references are pinned. Hold if production agents can still pull floating marketplace tags, load unsigned local folders, or hot-reload changed tool descriptions without a hash roll-forward.
Decision record: promote or hold
Promote this contract when:
- every production skill and tool schema has a content hash and publisher pin;
- loaders fail closed on unsigned, invalid, revoked, or lineage-broken artifacts;
- an internal mirror is the only write path into production agents;
- nested dependencies and external references are digest-pinned;
- admit and deny events are auditable without reading model transcripts.
Hold when any production path can load a community skill by name alone, trust a scanner badge, or accept a same-key update without previous_hash checks.
What to do next
List every skill directory, MCP tool catalog, and local tool schema your production agents can load today. For each one, record publisher identity, content hash, signature status, nested dependency pins, and whether updates require lineage. Until that inventory is complete and the loader fails closed, treat marketplace installs as untrusted code drops, not configuration.
If you need the control plane implemented as part of an internal workflow program rather than a one-off signing script, start from AI workflow automation with the admit-time integrity gate as an explicit deliverable.