Pinning and verification.
A pin you never check is not a lock, it is a preference — and most of the pins holding your agent stack together are exactly that. Pinning has two halves: naming the artefact you want, and proving that the bytes you received are that artefact. Nearly every agent toolchain ships the first half, calls it security, and inherits the second half from whichever registry, git host or model provider happens to be resolving the name. The distinction worth internalising is between an identifier that describes the content and one that merely points at it, because only the first one can be verified by the party that cares.
Two kinds of identifier, and only one of them is self-checking.
Everything your agent pulls at build or run time is fetched by an identifier, and every identifier falls into one of two categories.
- A name resolves through a namespace somebody else owns.
v2.1.0,latest, a git tag, a branch, an OCI image tag, a model alias, an MCP server's registry entry. The name is a lookup key; what it returns is whatever the namespace owner says it returns, today. Nothing about the bytes you receive lets you check the answer. - A digest is the content, compressed. A
sha256:image digest, a git commit object ID, an npmintegrityhash, ago.sumline. You can hash what arrived and compare. The check needs no network call, no trusted third party and no cooperation from whoever served the file.
The useful property is not immutability — plenty of names are immutable by policy, and policy is a promise. It is local checkability: can the consumer, alone, detect a substitution? A digest says yes. A name says "ask the registry again, and hope it answers the same way twice."
Sort your dependencies once with that single question and the result is usually uncomfortable. Model versions, MCP servers, plugin and skill bundles, and remote prompt templates are almost always fetched by name. Container images and language-package lockfiles are usually fetched by digest — which is why the mature half of your supply chain is the half that predates agents.
A pin is a request. Verification is the part that makes it binding.
Write the protocol out and the gap becomes obvious. Pinning is two operations: ask for artefact X, then confirm that what landed is X. Skip the second and you have not pinned anything; you have expressed a preference to a server that is free to ignore it, and you have removed the warning you would otherwise have got from a version number visibly changing.
The cleanest real example is the September 2026 Plugin4Shell disclosure, which found the same omission in four coding agents at once. Each one checked out a plugin at a pinned 40-character commit SHA and then never asked git which commit it had actually landed on. Because that string is also a legal branch name, a repository owner who created a branch with that exact name could serve entirely different code while the pin in the manifest stayed byte-identical. The fix is one command — resolve HEAD after checkout and compare — and its absence converted an approved, reviewed plugin into an attacker-controlled one with no user action at all.
# the pin without the check: whatever the host resolved, you keep git clone --depth 1 "$REPO" plugin git -C plugin checkout "$PINNED_SHA" # the check that makes it a pin (one line, no network) test "$(git -C plugin rev-parse HEAD)" = "$PINNED_SHA" || exit 1
Generalise past git and the same sentence keeps applying: a lockfile entry that records a version but no hash, a model alias recorded in a config, a tool schema fetched fresh on every connection. Each is a request nobody audits on arrival.
When the check is missing, the security property has silently moved to the host.
Here is the part teams miss, and it is the reason this is a concept rather than a bug report. When a client does not verify, the system is not necessarily exploitable — it is exploitable unless the namespace forbids the ambiguity. That condition holds for some hosts and not others, and it is nowhere in your configuration.
In the git case, GitHub rejects any push whose branch or tag name is 40 hex characters, and GitLab blocks the same shape in a pre-receive hook. So for plugins hosted on those platforms, the attack is stopped by the platform, not by the agent — which is precisely the argument one vendor made for not patching. The argument is correct and it is also the problem: the moment a plugin is mirrored, vendored, self-hosted on a smaller forge, or fetched from a corporate instance running an older policy, the property vanishes and nothing in the manifest changed.
- The guarantee is not in the artefact you can read. Your lockfile looks the same whether the host enforces the rule or not, so no review, diff or audit can see the difference.
- It travels badly. Mirrors, proxies, air-gapped rehosting and acquisitions all move artefacts between namespaces with different rules, and the migration checklist never mentions refname policy.
- It is one party's product decision. The rule exists because a platform chose it, is documented as a usability guard rather than a security control, and can change without a CVE.
Read that pattern once and you will find it everywhere in the agent stack: a supply-chain property that is real in production, absent from the artefact, and owned by a company that never promised it.
What to pin in an agent, in the order that pays.
Agents extended the dependency graph in a direction classical packaging never covered: things that are fetched per run, change behaviour rather than code, and have no lockfile at all.
- Model versions. An alias like a bare family name is a name in the strongest sense — the provider re-points it, and your evals move underneath you. Pin the dated snapshot, and treat the switch as a deploy with a regression run, which is what model deprecation and migration is for.
- Tool schemas. An MCP server sends its tool list on connect and may send a different one tomorrow; the description text is part of your prompt, so a silent change is a prompt change you did not review. Hash the served schema, store the hash, and alert on drift — the failure tool poisoning describes needs exactly this to be visible.
- Skills, plugins and prompt bundles. These are code by any honest definition, including the Markdown ones, because they enter the context and steer tool calls. They need a digest and a review, on the same footing as a dependency — see agent skills.
- The environment the agent executes in. Base images, interpreter versions, installed CLIs. This half already has good tooling; use the digest form rather than the tag.
- Nothing about sampling. Pinning does not buy determinism, and expecting it to is a common disappointment — that is reproducibility and determinism, a different problem with different answers.
Do the cheap audit first: list everything your agent fetches by a name rather than a digest, and for each one write down who owns the namespace and what stops them — or someone who compromises them — from returning different bytes tomorrow. Then add the verification step wherever a digest exists and is simply not being compared, because that is a few lines of code and it converts a promise into a check. For the entries where no digest exists at all, the honest move is not a better pin but a narrower grant: assume the artefact can change and make sure what it can reach is small enough that it changing does not matter much.