Skip to content
T threat.cafe

· Nadia Ferreira-Strand (editorial persona) · Agent artifact supply chain · on threat.cafe

Approved last month, malicious today: a re-verification cadence for MCP servers and skills

An approval is only valid for the exact thing you reviewed. Pin every approved MCP server and skill to an immutable version or content hash. Re-verify whenever that version or hash changes. Then run a scheduled sweep, because new threat reports arrive even when your artifacts do not change. The first malicious MCP server in the wild, postmark-mcp, turned bad in version 1.0.16. OWASP's Agentic Skills Top 10 lists "Update Drift" as AST07. Install-time vetting alone does not survive either.

Outcome, time and roles

Outcome. Every approved MCP server and skill has a recorded version or hash, a last-verified date, and a trigger that forces a re-check when it changes.

Time. About half a day to build the inventory for a team with a few dozen artifacts. After that, minutes per change.

Roles. One platform owner with write access to managed settings. One reviewer who is not the author of the artifact. Anthropic's enterprise skills guidance states it directly: "Skill authors should not be their own reviewers."

Use this runbook if, do not use if

Use it if you already approve MCP servers or skills before use and want approvals to stay true.

Do not use it yet if you have no inventory. Build that first. A cadence for unknown artifacts is a cadence for nothing.

Why approvals decay

Three documented mechanisms change an approved artifact after review.

A new version from the same name. postmark-mcp was a copy of Postmark's official library. In version 1.0.16, released 17 September 2025, one added line blind-copied every email the server sent to an outside address. It had 1,643 downloads in total, The Hacker News reported. Anyone who approved an earlier version and let it float to the latest would have received the change silently.

A dependency changes underneath. On 31 March 2026, [email protected] was published from a hijacked maintainer account. It added a dependency, [email protected], with a postinstall hook. The bad axios version stayed on npm for about 2 hours 53 minutes, StepSecurity found. An approved MCP server that depends on axios with a loose version range could pull it in without its own version number changing.

The tools change mid-session. The MCP specification lets a server tell the client its tool list changed, through notifications/tools/list_changed. Backslash Security's checklist puts the risk plainly: "A server can redefine its tool descriptions on a later run."

OWASP's AST07 page sums it up: "Skills are installed and forgotten." The project is an incubator at version 0.0.0, so treat it as a checklist, not a standard.

Pre-flight: build the inventory

OWASP's AST07 mitigations ask for "an inventory of installed skills with version, hash, and last-verified timestamp." Extend that to MCP servers. One row per artifact:

artifact | type | source | pinned_to | hash | approved_by | last_verified | trigger

Fill pinned_to with an exact version, a commit SHA, or none (remote). A row that says latest is a finding.

Verification. Pick three rows at random. Confirm the pinned version is what developers actually run. If it is not, stop and fix distribution before going further.

The cadence by artifact type

The intervals below are threat.cafe editorial recommendations, not a vendor standard. The logic behind them: a pinned artifact cannot change, so its scheduled check is against new threat reports. An unpinned or remote one can change at any time, so it needs a check on every observable change plus a short sweep interval.

ARTIFACT TYPE               PIN TO                 RE-CHECK WHEN              SWEEP
--------------------------  ---------------------  -------------------------  ---------
Local MCP server            exact version in the   version or lockfile        monthly,
  (npm / PyPI package)        launch command;        changes; any dependency    against
                              lockfile integrity     in the tree changes        new reports
Skill in your Git repo      commit SHA + hash of   any commit touching the    monthly
                              the skill folder       skill folder
Skill via Claude API        explicit version on    a new version is           monthly
  (/v1/skills)                every request          uploaded
Skill synced from           not pinnable by user;  your claude.ai skills      weekly
  claude.ai to Claude Code    review at source       change
Remote MCP server           not pinnable; snapshot tools/list differs from    weekly,
  (HTTP)                      tools/list at          snapshot; list_changed     plus at
                              approval               received                   session start
--------------------------  ---------------------  -------------------------  ---------

Step-by-step execution

Step 1: Pin local MCP servers

Write the exact version into the launch command. A command like npx -y PACKAGE fetches the newest version on each run. A command with [email protected] does not.

Record the registry's integrity hash for that version:

npm view [email protected] dist.integrity

Verify. Run it again next week. The value must be identical. If it changes for the same version string, stop and treat it as an incident.

Enforce. In Claude Code, an allowlist serverCommand entry must match "every argument, in order," per the managed MCP docs. Put the pinned version in the entry. A new version then fails the allowlist until someone approves it.

Step 2: Hash skills you store yourself

Skills are folders of files, so hash the folder:

find ./skills/SKILL_NAME -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum

Store the result in the inventory. Anthropic's guidance says to "compute checksums of reviewed Skills and verify them at deployment time" and to use signed commits.

Verify. Re-run the command at deployment. A mismatch blocks the deploy.

Step 3: Pin API skills by version

Anthropic's docs are explicit: "If you omit version, requests use the latest version, so a new version uploaded by anyone in the workspace immediately changes what production agents run." Set version on every request.

Verify. Search your code for skill references with no version. Each one is a floating approval.

Step 4: Watch synced skills at the source

Claude Code downloads skills enabled on a user's claude.ai account into ~/.claude/skills/synced/, according to the Claude Code skills docs. It checks for changes about every 10 minutes while a session is active. Users cannot pin these. Review them where they are managed, on claude.ai. If that review cannot keep pace, set syncClaudeAiSkills to false in managed settings.

Step 5: Snapshot remote server tools

You cannot pin a URL. You can record what it offers. The MCP Inspector's CLI lists a remote server's tools:

npx @modelcontextprotocol/inspector --cli https://SERVER_URL --transport http --method tools/list > tools-approved.json

Hash the file. On each sweep, list the tools again and compare.

Verify. Any difference in a tool's name, description or input schema is a re-review, even if the change looks small. Descriptions are text the model reads.

Common failure modes and fixes

Failure 1: the pin is in the docs, not in the config. The wiki says version 1.2.3, but developers copied an older command without a version. Fix: enforce the pin in the managed allowlist, not in documentation. A command without the pinned version should fail to load.

Failure 2: the lockfile pins the server but not its tree. Pinning the top-level package does not freeze its dependencies if it is launched in a way that resolves them fresh. Fix: install from a committed lockfile with npm ci, and re-check when the lockfile changes. The axios case shows why the whole tree matters.

Failure 3: the sweep checks nothing new. A monthly sweep that re-reads the same scan result adds nothing. Fix: the sweep compares your inventory against new reports published since last_verified. A known-bad list or advisory feed is the input. The date column makes this cheap.

Failure 4: remote changes arrive between sweeps. A weekly snapshot misses a change on day two. Fix: add a session-start comparison for remote servers on your highest-risk list, and treat notifications/tools/list_changed as a re-check trigger where your client exposes it.

Failure 5: the reviewer is the author. Fix: two names on every approval row.

Rollback

When a re-check fails:

  1. Block first. Add the exact version, command or URL to the managed denylist. In Claude Code, "nothing overrides a denylist match."
  2. Restore the last good version. Anthropic's guidance: "Maintain the previous version as a fallback." For a local server, roll the pinned version back. For an API skill, point requests at the previous version.
  3. Check exposure. Find which machines ran the changed version and when. For postmark-mcp, The Hacker News relayed advice to rotate credentials and review email logs for the outside BCC address.
  4. Record it. Update last_verified and note the failed version in the inventory, so nobody re-approves it.

Health check. After rollback, list loaded MCP servers on one machine (claude mcp list in Claude Code) and confirm the blocked version is gone.

Is this realistic for a small team?

Illustrative example: a team with 20 approved artifacts that each change about once a month has around 20 re-checks a month. That is one a working day. Most are a diff review of a version bump. The weekly remote sweep is a script. The cost is real but small, and it falls on a calendar instead of in an incident.

The threat.cafe board records the version at which each documented MCP server or package turned malicious, which is the data the monthly sweep compares against.

Sources

#mcp #skills #version-pinning #update-drift #runbook

All posts