The Portrait Stayed in the Attic
A version is a name and a hash is an identity. A record that carries only the name records a portrait that never ages, while the thing it points at is free to change in the attic.

Oscar Wilde published The Picture of Dorian Gray twice under the same title. The first version ran in the July 1890 issue of Lippincott's Monthly Magazine at thirteen chapters. The second, in 1891, kept the editor's cuts and grew to twenty chapters. Same name on the spine, different book inside. I mention it because the novel is about exactly that trick.
Basil Hallward paints Dorian at the height of his beauty. Dorian wishes the picture could age in his place, and it does. The man stays as he was and the canvas takes every year and every cruelty. In Chapter VIII Wilde puts the arrangement in one sentence: "The portrait was to bear the burden of his shame: that was all."
In Chapter X he sends for the housekeeper, Mrs. Leaf, and asks for the key to the old schoolroom. She protests that it is full of dust and has not been opened in nearly five years. "I don't want it put straight, Leaf. I only want the key." He has the portrait carried up by a frame-maker who "must not be allowed to know where the picture was being taken to." Then: "Dorian locked the door and put the key in his pocket. He felt safe now. No one would ever look upon the horrible thing. No eye but his would ever see his shame."
For years the referent changes and the name does not. Everyone in London deals with Dorian Gray, the label, and the label is immaculate. The only true record of what he has become hangs in a room nobody is permitted to enter. One person insists. Chapter XII opens on the ninth of November, the eve of Dorian's thirty-eighth birthday, and Basil says he could not claim to know Dorian without seeing his soul, which only God can do. Dorian takes him upstairs: "You shall see the thing that you fancy only God can see." In Chapter XIII, in front of the canvas, Basil says, "This is the face of a satyr." Dorian answers, "It is the face of my soul," and then kills the painter for having looked.
The ending is the audit. In Chapter XX Dorian takes the knife that killed Basil to the canvas, and the servants find "a splendid portrait of their master as they had last seen him" on the wall and, on the floor, a withered dead man in evening dress. "It was not till they had examined the rings that they recognized who it was." The name had stayed fresh to the last. Only the rings, the part that could not be repainted, said who had been living in the house.
Every version string in your agent stack is Dorian Gray. The portrait is somewhere you are not looking.

Three portraits
Three things from the last two months share one shape. The operator's configuration said the same thing before and after. The thing that ran did not.
Hermes. On August 28, CVE-2026-82021 was published against Hermes Agent: CVSS 4.0 score 9.0, CWE-494, Download of Code Without Integrity Check. The bundled MCP catalog "allows a remote attacker to execute arbitrary code by compromising a third-party upstream repository referenced via a mutable branch rather than a pinned commit SHA." Fairness first. The fix shipped on July 20 in v0.19.0, thirty-nine days before the CVE was published; the pull request behind it says it "Enforced exact version pinning across the entire MCP catalog" down to commit SHAs. The maintainers moved before the record did. But a catalog entry pointing at a branch is a name, and the branch head is whatever the upstream says it is today. Every install that read that catalog was told the same thing and could have received a different thing, and nothing in the log would show it. The CVE does not name the upstream, so I will not either.
HookPry. On September 3 a paper introducing HookPry made the same move against harness lifecycle hooks, the shell commands a coding agent fires on events like tool use. The abstract's warning: these commands "run with host privileges yet ship as lifecycle-hook configuration and may fire at times the LLM never observes." The attack: "a benign versioned plugin can be trojanized by an update that silently binds attacker-chosen commands to benign events." Across 1,000 end-to-end runs it "compromises all seven evaluated harnesses, with per-harness success rates reaching 92.5%," and no run was explicitly blocked by the model. On the defense side: "Microsoft Defender has 0% recall, and the union of three static defenses misses 47.5% of malicious artifacts." The paper's Claude Code finding is that a trusted plugin's update loads its new hooks "without user notification, item-level confirmation, or re-authorization"; that is the researchers' claim, not Anthropic's documentation, and they say they have disclosed to the vendors. The plugin kept its name. Its version went up. What changed was in the one place the transcript does not show, and Anthropic's hooks reference confirms it: "For most events, Claude Code writes stdout to the debug log and doesn't show it in the transcript."
Claude Code itself. This one has no attacker in it, which is why it is the most useful of the three. Claude Code 2.1.259 reached npm on September 2 at 21:21 UTC. It tightened Bash Read() deny rules to cover files passed as option values, and it changed allowedMcpServers "to govern only servers users add," with a consequence the changelog states plainly: "a literal managed-mcp.json server your allowlist used to filter out now loads on upgrade." Twenty-five hours and ten minutes later, 2.1.260 reverted the deny-rule change, because it "denied npm run build under a Read(./**/build/**) rule in every mode." An operator's settings file that read identically on both days governed three different agents: one where the managed server was filtered out, one where it loaded and the build was denied, and one where it loaded and the build ran. Nothing in the settings file changed. And the npm registry, when I fetched it, carried three dist-tags on the package: latest at 2.1.260, next at 2.1.260, and stable at 2.1.236. By default, npm's docs say, npm install "installs the latest tag." A tag is a name that someone moves.
None of the three is a scandal. Hermes fixed its catalog before anyone wrote it up; Anthropic reverted a bad change the next day, which is what good release engineering looks like. The point is that in all three cases the record you would consult afterward, the config, the lockfile, the version string, would tell you the portrait was fine.

The supply chain learned this already
On March 14, 2025, StepSecurity's detection flagged that tj-actions/changed-files, a GitHub Action then "used in over 23,000 repositories," had been compromised. The attacker did not publish a new version. In Wiz's description, they "managed to modify existing version tags to make them all point to their malicious code," so that v1.0.0 and v44.5.1 alike resolved to one commit that dumped CI runner memory and printed the secrets into public logs. Everybody who had pinned to a version tag was pinned to a name. The name was moved. GitHub's hardening guide now says it in one line: "Pinning an action to a full-length commit SHA is currently the only way to use an action as an immutable release," because "a tag can be moved or deleted."
Docker says the same about image tags: "Image tags are mutable, meaning a publisher can update a tag to point to a new image," and the remedy is a digest, with the honest trade-off that pinning to one means "you're opting out of automated security fixes." npm's dist-tags, as above, are aliases by design. Each ecosystem arrived at the same distinction after getting burned. A nominal reference is a label maintained by convention; it can drift independently of what it names. A content-addressed reference is a digest of the state itself; it cannot drift without the state changing. That is the vocabulary of a paper I am about to lean on.
Zero of forty-seven, and the attic key
I wrote about the 0 of 47 finding once before, as a matter of forged records. Here it is a matter of identity. Rashid Azarang's paper From Traceability to Justifiability (August 21) surveyed 47 delivery platforms, 20 CI/CD and 27 model-serving and agent platforms, 188 double-graded cells, and found "no platform whose default record emits a content-addressed identity of the behavioral tuple (model version, instructions, tool definitions, retrieval and runtime configuration)." Zero of 47. Meanwhile "immutable nominal versioning of the tuple is arriving as the agent platforms' default answer, on 16 of 27 platforms: version integers behind mutable pointers, the layer the artifact supply chain community already found insufficient." The agent platforms are converging, by default, on what tj-actions taught everyone else not to trust. The paper's own motivating case: a manifest "whose every content-addressed field was correct while its nominal release identity had been wrong for twelve consecutive releases." Dorian, twelve times over.
So who gets to look at the portrait? Looking requires a key. Two pieces of work from the last fortnight are the closest thing I have found to one.
AgentProv (August 30, from CISPA) audits the component you most take on faith: the model. "Commercial LLM APIs advertise a specific foundation model, but the served backbone may be silently substituted, quantized, or wrapped." AgentProv fingerprints a deployed model by the distribution of its tool calls; it catches every substituted model, "100% on 630 evaluated checkpoint pairs," with a 7% false-positive rate under system-prompt injection. The introduction cites a survey of 17 third-party wrappers with a 45.8% endpoint-level identity failure, "including wholesale backbone swaps such as GPT-5 to GLM-4-9B." The model name in your config is a version pointer too. Somebody moves it.
The IETF draft on compliance receipts for agent actions (Marques, dated August 31, an individual submission, still a draft) is the other half: what a per-action receipt has to commit to, so that looking is possible later. Each receipt carries a payload_digest over the action, a policy_digest of the form sha256:<hex>, a previousReceiptHash chaining it to the one before, an executable_hash of the binary or container manifest, a tool_fingerprint over the tool's name and input schema, an sbom_digest, and a config_manifest_digest that must cover "model identifiers and versions, prompt template digests, retrieval index identifiers, and any other inputs whose change would constitute a substantial modification." That field list is the attic key. It is the difference between a record that says "Claude Code, 2.1.259" and one that says: this is the digest of every layer in the room when this action happened.

What the standards say, and what they leave out
The standards bodies have noticed that agents change under you, and they deserve their due before I say what is missing. The Agent Control Standard opens with "Enterprises cannot secure what they cannot inventory" and promises that "Unlike static SBOMs, AgBOM updates in real time as agents discover new tools, connect to new MCP servers, or modify their own capabilities." OWASP's Agent Observability Standard defines its own AgBOM across six entity categories and six update triggers, each of the form "discovered, removed or changed capabilities." CycloneDX 1.6 (April 2024) added attestations that "enable organizations to communicate standards, claims, and evidence," on top of the ML-BOM from 1.5. SLSA's levels grade how hard provenance is to forge, from Build L1, "trivial to bypass or forge," upward. Sigstore's model-signing project signs "a list of (file path, digest) pairs" and verifies by recomputing the hashes. Most of this is very new work, aimed at a real problem.
Now the gap, and I am judging these on their public specification pages, which is all either project currently offers. Neither requires a digest anywhere in the inventory it defines, and if either spec carries that requirement somewhere I could not reach, I will print the correction. Both describe an inventory that updates, which is to say a portrait repainted whenever the subject changes. It is still a description of what is believed to be running. It is nominal. A description of the subject, however current, is not the subject; the only record that cannot drift from what ran is a digest of what ran, taken at the moment it ran, by something the subject could not edit. SLSA has that idea for build artifacts, and the IETF draft has it for actions. None of these standards yet says that an agent BOM must be a hash of the behavioral tuple at execution time or it is a portrait. That sentence is the missing one, and it is short enough to add.
What a digest costs
Content-addressing has a price: every honest change looks like an identity change, because it is one. If your run identity is a digest over model, instructions, tools, hooks, config and retrieval index, then a one-line prompt fix is a new agent. So is a provider's silent point release. So is the 2.1.259 to 2.1.260 pair above: two identity changes in twenty-five hours, both legitimate. A fleet that alerts on every identity change alerts constantly, and an alert everyone ignores is a portrait nobody climbs the stairs to see. The only workable answer I know is a drift budget. Decide, per agent, which layers may change without a human looking (a prompt edit by a named owner, a patch-level harness bump) and which may not (the model backbone, the hook set, the tool schemas, anything with host privileges). Record the digest on every run regardless. Spend review attention only where the budget is exceeded. The record is complete; the alarm is selective. Write it down, because "we alert on everything" and "we alert on nothing" converge on the same outcome.
Our own tool sits on the right side of this line and still falls short of the argument. g0 inventory . --cyclonedx --sign-key k emits a signed CycloneDX 1.6 AI-BOM that is, per our docs, content-addressed: a g0:bomHash over the component set "so two BOMs of the same inventory hash identically and diff cleanly across releases," with an ed25519 signature over the hash. Run it today and next week and --diff will show added, removed and changed components. That is better than a name. But read the discovery table: it finds frameworks, models, agents, tools, MCP servers, vector databases and prompts. Hooks are not in it. The hooks in Claude Code's settings.json, a plugin's hooks/hooks.json, a skill's frontmatter, the exact layer HookPry trojanized, are content-scanned by g0 check and watched at runtime by g0 protect, which is on main and unreleased. The BOM you can diff this week does not diff the thing that fires "at times the LLM never observes." You should know that before you trust the diff.
Basil on the stairs
Basil Hallward made the original and knew what it was supposed to look like, and he did the thing nobody else in the book did: he refused to accept the name as the record and went up the stairs to look. "Know you? I wonder do I know you? Before I could answer that, I should have to see your soul." It cost him everything. Wilde was not subtle about what looking costs.
The version in your config is Dorian's face: fresh, stable, and what everyone downstream sees. What actually ran is upstairs, changing, in a room whose key is in someone else's pocket: a branch head, a plugin update, a dist-tag, a served backbone that is not the one on the invoice. You can build the room so the portrait is visible: a digest of the tuple on every run, a receipt that carries it, a fleet that diffs it against a written budget. Or you can trust the face, and learn who was living in the house when it is time to check the rings.
Basil went up the stairs. That is the whole job: go up, and take a hash.
Guard0's g0 inventory signs a content-addressed CycloneDX 1.6 AI-BOM you can diff week over week, which is the cheap half of this essay's argument and the half you can run today. Hooks are not in its discovery table, so the layer HookPry trojanizes is one you still have to read by hand, in settings.json, in each plugin's hooks.json and in each skill's frontmatter, and a diff that does not cover them is a portrait with a room left out.
References
- From Traceability to Justifiability: Accountability Structures in Agentic Software Engineering (arXiv 2608.23610)
- HookPry: When Attacker-Controlled Hook Updates Steer AI Agent Harnesses towards Malicious Behaviors (arXiv 2609.03884)
- AgentProv: Auditing Agentic LLM API Providers via Tool-use Policy Probes (arXiv 2609.00052)
- CVE-2026-82021, Hermes Agent
- Claude Code CHANGELOG
- Claude Code hooks reference
- StepSecurity: tj-actions/changed-files action is compromised
- GitHub: Security hardening for GitHub Actions
- IETF draft-marques-asqav-compliance-receipts-08
- Agent Control Standard
- OWASP Agent Observability Standard, AgBOM
- The Picture of Dorian Gray, Project Gutenberg #174
Get Started
Start free on Cloud
Dashboards, AI triage, compliance tracking. Free for up to 5 projects.
Start free →Accountability at scale
SSO, RBAC, CI/CD gates, self-hosted deployment, SOC2 compliance.