Why skill signing matters

Skills are markdown injected into LLM context. A malicious skill — one crafted to redirect an investigation actor, suppress findings, or exfiltrate information through tool calls — looks identical to a legitimate one until the model acts on it.

Two incidents illustrate the risk:

  • ToxicSkills: A research audit of publicly available AI agent skill marketplaces found that 36.8% of sampled skills contained security flaws — instructions that could redirect agent behavior, bypass stated constraints, or cause information disclosure through crafted tool invocations.
  • ClawHavoc: A coordinated campaign distributing 335 malicious skills through the OpenClaw marketplace. Skills appeared functional (executing their stated purpose) but contained delayed trigger conditions that activated under common scenarios to maximize activation rate. Skills used namespace conflicts and author identity spoofing to appear legitimate. Payload was typically introduced in v1.2 or later, after the skill had accumulated ratings and installs on a clean v1.0.

mallcop's response: any skill loaded into actor context must be traceable to a key you've designated as trusted. If no trust path exists, the skill is rejected.

Trust anchors

A trust anchor is a public key you've declared as the root of trust. mallcop ships with one anchor pre-loaded: the mallcop root key, which covers all built-in skills. Built-in skills just work — no configuration needed.

To add your own anchor (for org-signed skills or third-party skill authors):

mallcop trust add-anchor security-team@example.com ./security-team.pub

Anchors are stored in .mallcop/trust/anchors in your deployment repo. Each line is: identity keytype base64. Anchors are the only keys that don't need to be endorsed by something else — you add them directly, which means you are making an explicit trust decision.

To add a key to the keyring without making it an anchor (i.e., it needs to be endorsed before its skills are trusted):

mallcop trust add-key alice@example.com ./alice.pub

Endorsing authors

An endorsement says: "I (the endorser) vouch for this identity, for skills matching this scope, at this trust level, until this date."

# Endorse alice for all AWS skills until end of year
mallcop trust endorse alice@example.com \
  --scope "aws-*" \
  --level author \
  --expires 2026-12-31 \
  --reason "Alice owns AWS security runbooks" \
  --key ~/.ssh/id_ed25519

# Endorse the security-team identity to re-delegate trust (full level)
mallcop trust endorse security-team@example.com \
  --scope "*" \
  --level full \
  --expires 2026-12-31 \
  --key ~/.ssh/id_ed25519

Trust levels

LevelCan sign skillsCan endorse others
authorYesNo — terminal node in the chain
fullYesYes — can act as intermediary, endorsing other identities

Scope patterns

Scope is a glob pattern matched against skill names. It constrains what a given endorsement covers, regardless of what the endorsed identity signs.

PatternMatches
*All skills
aws-*aws-iam, aws-networking, etc.
privilege-analysisExactly that skill name

Scope narrows at each hop in the chain — an intermediary with aws-* scope cannot endorse someone for skills outside that scope.

Expiry

Every endorsement has a required expiry date (YYYY-MM-DD). Expired endorsements are silently excluded from chain traversal. A chain that was valid yesterday may not be valid today if an intermediate endorsement expired. Re-endorse before expiry to maintain the chain.

Trust web

When mallcop needs to load a skill authored by a given identity, it performs a BFS from all anchors through the endorsement graph to find a valid path to that identity.

Chain rules:

  • Only anchor nodes are trusted without being endorsed
  • Intermediary nodes (not the target) must have trust_level=full
  • The terminal node (the skill author) can be full or author
  • Every endorsement in the path must be unexpired and scope-matching
  • If no path exists, the skill is rejected

Example chain: mallcop-root → security-team@example.com → alice@example.com

  • mallcop-root is an anchor (trusted by default)
  • mallcop-root endorses security-team at full level with scope=*
  • security-team endorses alice at author level with scope=aws-*
  • Alice signs her aws-iam skill — the chain validates
  • Alice attempts to sign a skill named gcp-iam — rejected: her endorsement scope is aws-*

To inspect the chain for a given identity and skill:

mallcop trust chain alice@example.com --skill aws-iam

skills.lock

skills.lock is a YAML file recording the SHA-256 hash of every installed skill's content. At startup, mallcop verifies each skill against its recorded hash. If the hash doesn't match — skill was modified, file was added or removed — mallcop refuses to load it.

This is the fail-closed property: a skill not in the lockfile is not loaded. A skill whose content changed since the lockfile was generated is not loaded. There is no "trust but verify" mode — verification failure means rejection.

# After adding or updating skills, regenerate the lockfile
mallcop skill lock

# Commit it to your deployment repo
git add skills.lock && git commit -m "update skills.lock"

The lockfile format:

version: 1
skills:
  aws-iam:
    author: mallcop@mallcop.app
    expires: null
    sha256: a3f9c2...
    source: builtin
    trust_chain: null
    verified_at: "2026-03-14T10:00:00+00:00"
  privilege-analysis:
    author: mallcop@mallcop.app
    ...

Org trust bootstrap

How to set up trust for a team writing custom skills. Takes about ten minutes.

1. Generate a signing key

ssh-keygen -t ed25519 -C "security-team@example.com" -f ./security-team-key
# Produces: security-team-key (private) + security-team-key.pub (public)

2. Add the key as a trust anchor

mallcop trust add-anchor security-team@example.com ./security-team-key.pub

3. Endorse individual contributors (optional)

If contributors will sign their own skills, add their public keys and endorse them from the anchor:

mallcop trust add-key alice@example.com ./alice.pub

mallcop trust endorse alice@example.com \
  --scope "aws-*" \
  --level author \
  --expires 2026-12-31 \
  --key ./security-team-key

Skip this step if contributors submit skills for the security-team key to sign.

4. Sign skills

# Sign with the anchor key (or the contributor's key if they're endorsed)
mallcop skill sign skills/aws-networking --key ./security-team-key

5. Regenerate and commit the lockfile

mallcop skill lock
git add .mallcop/trust/ skills/ skills.lock
git commit -m "add trust setup and sign custom skills"

Zero-config default

If you don't configure any trust anchors, mallcop loads only built-in skills (signed with the mallcop root key, which is pre-loaded). No custom skills, no third-party skills. This is the safe default — the system works, and you haven't opened any trust surface you haven't explicitly approved.

Trust setup is only needed when you want to load skills beyond the built-in set.

CLI reference

mallcop trust add-anchor

mallcop trust add-anchor IDENTITY PUBKEY_FILE

Adds IDENTITY as a trust anchor. PUBKEY_FILE is an SSH public key file. Anchors are written to .mallcop/trust/anchors. No-ops if IDENTITY is already present.

mallcop trust add-key

mallcop trust add-key IDENTITY PUBKEY_FILE

Adds IDENTITY to the keyring without making them an anchor. They still need to be endorsed before their skills are trusted.

mallcop trust endorse

mallcop trust endorse IDENTITY \
  --scope GLOB \
  --level [full|author] \
  --expires YYYY-MM-DD \
  --key PRIVATE_KEY_FILE \
  [--reason TEXT] \
  [--identity ENDORSER_IDENTITY]

Creates a signed .endorse + .endorse.sig pair in .mallcop/trust/endorsements/. Endorser identity defaults to the comment field of the corresponding .pub file.

mallcop trust chain

mallcop trust chain IDENTITY [--skill SKILL_NAME]

Shows the trust path from any anchor to IDENTITY for a given skill name (default: *). Exits non-zero if no path exists.

{"status": "ok", "path": ["mallcop@mallcop.app", "security-team@example.com", "alice@example.com"], "skill": "aws-iam"}

mallcop trust list

mallcop trust list

Prints the full trust state: all anchors, keyring entries, and endorsements with their scope, level, and expiry. Useful for auditing the current trust configuration.

Global flag

mallcop trust --trust-dir /path/to/trust SUBCOMMAND

All mallcop trust subcommands accept --trust-dir to override the default trust directory (.mallcop/trust in the current working directory).