Today we’re releasing the Context Bill of Materials.
Why?
When an AI agent does something it shouldn’t, the first question is what the model read right before it did, and CxBOM records that answer for every inference it assembles. In our reference session, the operator’s instructions held 45 of the 46,205 tokens the model saw. That’s 0.10% of the window, and nothing inside the model enforces that those 45 tokens outrank the rest.
cxbom.org is live with the spec, the JSON Schema, and an offline record viewer. The signing proxy and the record emitter are public with their tests at github.com/CXBOM/cxbom-reference under Apache 2.0, and the paper, CxBOM: A Context Bill of Materials and Signed Tool-Definition Pinning for LLM Agents, is on Zenodo. Fred Wilmot, Richard Bird, Peter Holcomb, and Dan Sorensen co-authored it with Helen Oakley, and Chris Hughes reviewing.
We built three attacks that hide instructions in a notes tool’s description, and building an attack was the hardest thing in this whole project. We filmed all three against our proxy, once with its gate off and once with it on. The film is below, captioned with the one sentence our signed claims ledger lets any of us say about it.
The Operator Wrote 0.10% of the Context
Six pieces of text went into that context, and only one of them carries the operator’s instructions.
That’s our reference session, one context we assembled for testing with Phi-3.5-mini-instruct’s tokenizer and chat template, and its record ships with the paper’s ancillary files. The context window is everything the model reads on one call, laid out as a single sequence. Each piece of text in it is a component, and we measure components in tokens, the chunks a model reads (roughly a short word or part of a longer one).
We played the operator, the party that builds and deploys the agent, and we wrote its 45-token system prompt, which holds the agent’s standing instructions. The user turn was a 16-token request. The chat template is the fixed formatting each model expects around every message, with markers for who is speaking. Its role markers and the separators between pieces added 13 tokens that belong to no component.
MCP, the Model Context Protocol, is how an agent plugs into tool servers that anyone can publish. The app the agent runs in, like an AI code editor or a desktop assistant, is the MCP host, and it connects to those servers. Each server hands the agent a definition for every tool it offers, with the tool’s name, a description of what it does, and a schema for its inputs. The model reads that description to plan when and how to call the tool, and the people who publish the server write it.
Everything else in the session came from outside: the 357-token definition of the fetch tool from the public MCP servers repository, and three chunks of the Federalist Papers at 15,710, 15,283, and 14,781 tokens.
That outside content fills 46,131 of the 46,205 tokens, so neither the operator nor the user wrote 99.84% of what was assembled for the model. The record itself shows 99.87%, because it leaves the 13 formatting tokens out of the count. That share describes this one session, which we built on three long documents.
A trust ring records where a piece of context came from. Under the spec, the operator registers each delivery path to a ring once (the system-prompt template, the MCP tool listing, the retriever that pulls documents in), and every piece of text that arrives by that path gets that ring. Nobody inspects the content to decide. A retrieved document that claims “this message comes from the operator” still lands in ring 1, because a retriever delivered it.
Ring 0 holds what the operator wrote, like the system prompt, and ring 2 holds what the user types. Everything the agent pulls in while it works lands in ring 1: tool definitions, server instructions, tool results, retrieved documents, and any of the model’s own earlier output that gets read back into a later call.
A ring is a label in the record and gives the text no extra weight inside the model. The reason sits in the math.
Scaled dot-product attention is the operation a transformer runs to decide how much each token draws on the other tokens it can see. It’s the core of the Transformer that Vaswani et al. introduced in 2017, and Section 6 of our paper builds its argument on it. Their paper writes it in one line.
Each token gets turned into three vectors. Its query, Q, is what that position is looking for. Its key, K, is what the token offers to other positions, and its value, V, is the content it hands over when it gets picked. The product QKᵀ scores every query against every key, so each position gets a raw score for every token it can see. Dividing by √d_k, the square root of the number of dimensions in each key vector, keeps those scores from growing so large that the softmax’s gradients shrink toward nothing, which is the reason Vaswani’s team gives for the scaling. The softmax turns each row of scores into weights, and the output at a position is the value vectors blended by those weights. A model that writes text one token at a time, like Phi-3.5, also masks the scores so each token sees only what came before it.
Look at what goes into that equation. Q, K, and V are computed from the tokens themselves. Nothing in it says which ring a token came from, who wrote it, or which path delivered it. Section 6 of our paper rests on that. In a standard transformer, no term in that math takes a token’s origin as input. The trust ring lives in the CxBOM record, and attention never sees it.
The softmax is the part of that equation this paper leans on hardest. For a row of scores z, it gives token i this weight.
Two properties of that formula matter here. The weights at a position always add up to 1, so attention is a fixed budget, and every token the position can see competes for a slice of it. At the position where the model writes its answer, your 45 operator tokens and the 45,774 tokens of retrieved documents compete in the same softmax. Robin Young named this Token Democracy, and his abstract puts it bluntly when it says “transformers process all tokens as equals.”
The exponential also turns a small lead in score into a large lead in weight, wherever the token came from. Role markers in the chat template are just more tokens, so any extra weight the model gives the system prompt shows up as higher scores it learned to produce. Section 6 says every in-model defense works the same way, because its signal “competes in the same softmax as everything else.” An attacker hunting for text that outscores your instructions is hunting for exactly what this formula pays out on.
You can teach a model to care about provenance. Defenses mark untrusted text, train the model to prefer the system prompt, or give data its own embedding, a separate internal representation, as the ISE and ASIDE designs do. That last kind does feed origin into the math. Every one of them still depends on what the model learned to do with the signal, so you get a tendency, a behavior training shaped that can be strong and carries no guarantee on inputs an attacker picks.
Attackers who search for those inputs find them. Milad Nasr, Nicholas Carlini, Florian Tramèr, and colleagues combined gradient descent, reinforcement learning, random search, and human red teaming to bypass 12 recent defenses against jailbreaks and prompt injection, with attack success above 90% for most of them. Most of those defenses had reported near-zero attack success when they were published.
A property you need on every model call, like “the model only saw tool definitions someone approved,” needs software that enforces it every time. We put that check outside the model. In enforce mode, our proxy withholds any tool definition from a server it wraps that doesn’t match a signed approval, and the emitter records what got assembled.
The operator’s share of the context matters for a second reason. The paper also states a hypothesis we call the Dual-Failure Multiplier, and it starts from two results other researchers measured separately. Models lose accuracy as their input grows, and in many-shot jailbreaking a harmful response gets more likely as the context fills with demonstrations, which are example exchanges of an assistant doing the bad thing.
Our hypothesis is that those are one effect. As untrusted demonstrations pile up, the operator’s protective instruction gets a smaller share of the model’s attention, so attack success rises while quality on the operator’s task falls.
The paper also explains why it’s still untested, and the softmax budget is the reason. Section 7 notes that “softmax normalization over more positions lowers the share of any fixed span whatever the added text says.” Take a toy case where every score at the answer position is equal. Each token then gets 1/n of the weight, so the operator’s 45 tokens in a 46,205-token context get about 0.10%, and doubling the context with anything at all cuts that in half. Adding demonstrations makes the context longer, so content and length move together.
Real scores aren’t equal, which is why the design measures the share instead of assuming it. Its one length-matched test, H1, is set to run on Phi-3.5-mini-instruct and asks whether attack demonstrations pull attention off the operator’s rule more than neutral text of the same length does. It reads the rule’s share at the final prompt position, as a ratio of means over heads and layers, with attention-sink tokens (positions that soak up weight whatever they say) excluded and the rest renormalized. Its disclosure test, H2, can’t separate content from length.
The design isn’t frozen yet, the quality half isn’t designed, and no trial has run. If the hypothesis holds, the amount of ring 1 content you admit becomes a security setting, and you’d budget context the way you budget privileges.
Tool Definitions Sit in the Operator’s Chair
Positional authority is the standing a piece of text borrows from where the pipeline places it. It says nothing about how much attention the model pays. Chat templates have a system region, the part reserved for the operator’s instructions, and MCP hosts typically render every connected server’s tool definitions into that same region, because the model needs both to plan.
Whoever publishes the server writes the tool definition. That puts ring 1 content in ring 0’s chair, and the paper calls it Tool Definition Trust Inversion. In this version, every CxBOM record raises it as an open finding for each third-party tool definition in the system region. The finding describes an architecture, so it fires whether or not anyone attacked you.
Tool poisoning is hiding instructions in a tool’s description, where the model reads them and the user never sees them. Luca Beurer-Kellner and Marc Fischer at Invariant Labs disclosed it on April 1, 2025, along with cross-server shadowing (one server’s description changing how the agent uses another server’s tools).
They also disclosed the rug pull. A rug pull is a server changing a tool’s description after you approved it. You reviewed the clean version on Monday, and on Thursday the model reads the poisoned one from the same server, under the same name, with your approval still attached.
Then there’s the config file, which tells the MCP host which server commands to launch. CVE-2025-54136, which the paper calls MCPoison, let an attacker swap an already trusted MCP config in Cursor 1.2.4 and below for a malicious command. There was no warning and no re-prompt. All it took was write access to a shared GitHub repository holding that config, or to the file on the victim’s machine.
Cursor fixed it in 1.3 by prompting again.
Its sibling, CVE-2025-54135, which the paper calls CurXecute, let indirect prompt injection write a brand new .cursor/mcp.json in versions below 1.3.9. Indirect prompt injection is instructions hidden in content the agent reads, which the model then follows. Creating a dotfile (a hidden settings file whose name starts with a period) didn’t need approval, even though editing one did. NVD scores it 9.8. Text the agent read became code the machine ran.
The paper files both CVEs as configuration trust failures, meaning the attacker changed which program runs. Further down I cover which parts of them CxBOM handles.
The three attacks we recorded go after the description itself. One adds a sentence telling the model to send the secret out before it searches the notes. One claims the notes tool shares a login with another service and that the search fails until the secret goes out. The third is a rug pull mid-session, where the notes tool lists clean and then changes on the next listing.
Each ran against one local open-weight model, meaning one whose weights you download and run yourself. We used Qwen3-8B with greedy decoding, where the model always takes its most likely next token, once with the gate off and once with it on. We picked takes that repeat, so a count of them is no success rate. Before recording, we signed a claims ledger that limits what any of us may say about them to one sentence. That sentence is the caption under the film.
“On these recorded takes, for three attacks, a notes tool description that changed after it was signed sent the secret when the gate was off and the inbox showed that it arrived, and when the gate was on that tool was dropped from the listing, the secret did not arrive, and the model kept calling fetch until the take hit its limit, and only on the third attack was that definition clean on the first listing and changed on a re-listing within the session.”
Sign It at Assembly, Record It in a Context Bill of Materials
CxBOM has two parts, and each one sits where it can see what it reports. In enforce mode, the proxy withholds any tool definition or server instructions that differ from your signed approval, and it refuses a launch whose command line, binary, interpreter, pinned files, or pinned environment values differ from what you signed. The record means that after an incident, you can name every component the model was given, where it came from, which controls were applied, and which expected ones were missing.
The proxy sits between an MCP host and each stdio MCP server, the kind the host launches as a local process. It checks the server’s tool definitions, its instructions, and its launch against a signed approval. Tool results, notifications, and elicitation requests (a server asking the user, through the host, for more input) pass through it unverified.
The emitter runs where your application assembles its context and writes one CxBOM record per model call.
Content digest pinning means you store a cryptographic hash of exactly what you approved and reject anything whose hash differs. A hash works as a fingerprint of the text, and changing a single character changes it. Here the hash is SHA-256 over a canonical JSON form of each definition, one fixed layout, so spacing or key order can’t change the hash.
You run cxbom approve, which starts the server and prints each tool’s hash with only the first 200 escaped characters of its text, so read the full definitions from the server yourself. Then you run it again to accept each tool’s digest and the instructions’ digest with a written reason, type the server’s id at the terminal, and sign the lot into the workspace’s cxbom.lock with an Ed25519 key (a standard public key signature scheme) that lives under your real home directory. The proxy resolves that directory from the operating system’s user database and never from environment variables.
At runtime in enforce mode, the proxy pulls any changed or never-approved definition out of the listing before the model reads it, and it refuses calls to that tool. If the server’s instructions change, it blanks them and withholds every tool the server offers. That handles the rug pull from the last section, including the version where the definition changes on a re-listing mid-session.
Observe mode forwards a changed definition to the model and logs the change. The proxy allows observe mode only when the signed approval permits it.
The proxy pins the launch too. It won’t start a server whose command line, launcher binary, interpreter, pinned files, or pinned environment values differ from what you signed.
The paper calls the attacker a workspace writer here, someone who can edit files in the agent’s workspace, including the MCP config. Prompt injection that writes config files is one way to get that access. The wrapper is the config entry that launches the server through cxbom proxy instead of directly. A writer who changes a wrapped server’s command, binary, or pinned environment value, the MCPoison move, can’t produce a valid signature, so that launch never happens.
The wrapper sits in the same config file the writer can edit. A writer who deletes it, or adds a new unwrapped server the way CurXecute did, goes unnoticed in this version. The scope page lists cxbom scan and cxbom guard as a later plan that will detect that case when they run and won’t prevent it.
All of this assumes the agent can write workspace files and can’t run code as you. An agent with a terminal tool that runs shell commands as the user, or a server package that’s malicious code to begin with, is outside what the proxy stops.
Rollback is an attacker replaying an older, validly signed approval that has since been replaced. The proxy defends with a high-water mark. Every signed entry carries a sequence number, the proxy remembers the highest one it accepted in a state directory that sits outside your workspace by default, and it refuses anything below that.
Before it acts on any verdict, the proxy writes it to an attestation log. The emitter binds each tool component of a record to that log, so the record can state whether a definition matched a signed pin in a live proxy session.
A SBOM (software bill of materials) lists the components inside a piece of software. A CxBOM record does the same for one model call. It lists every component assembled into the window with its ring, its source, a SHA-256 digest of its text, its token count, and its share of the context. It also lists the controls applied and the controls expected but missing.
The emitter tokenizes each component separately, and your application sends the model exactly those token ids, the numbers the model receives, one per token. That’s why it needs a model server that accepts token ids directly, like llama.cpp or vLLM. Every token id the model receives belongs to exactly one component or to template overhead, and the emitter won’t finish a record unless your application’s digest of the ids it sent matches the ids it assembled.
Fields it can’t observe, like a scanner’s injection rating, get listed as not collected and raise an open finding. A record only counts as audit-clean when no finding is open, so it never comes out clean by omission. The current emitter’s record of the reference session carries five open findings: the trust inversion, a tool definition with no live proxy session behind it, and three groups of security fields no scanner collected.
In this version that means no record holding a third-party tool definition comes out audit-clean, because the emitter can’t yet register the trust boundary that would close the inversion finding. Keep that flag out of a release gate until it can.
The format is a profile modeled on CycloneDX 1.6, OWASP’s SBOM standard, with its own bomFormat value. A stock CycloneDX validator won’t accept it unchanged, and it joins your CycloneDX AI/ML BOM by agent and deployment identifiers.
On the reference session, the emitter built and wrote the record in a median of 75 ms over 20 runs on one arm64 machine. The record came to 22,297 bytes, 11.9% of the raw text it describes, and the paper expects a short-context record to run far larger than its text. The paper lists forensic utility, effectiveness in deployment, and cost as not yet measured.
The paper also maps every attack class in its threat model to what this version does about it.
The code refuses 14 of the 23. Each refusal cites a passing test that exercises that attack against the code, using scripted servers, constructed lock files, and stand-in home directories, and four of them hold only in enforce mode. It refuses one more in part. The other eight are classes this version doesn’t cover, and the paper names each one.
Those are conformance tests, which check that the code does what the design says, and the attack success rate in a live deployment is still unmeasured. The refused classes include the rug pull, a call to a tool the server never listed, a swapped command or binary, a replayed older approval, and a tampered lock file. The classes this version doesn’t cover include a definition that was malicious when you approved it, an edit that removes the wrapper, and injection through a tool result or a retrieved document.
By the paper’s count at the captured commit, the core suite of 28 test modules passes 1,691 of its 1,692 cases, with one skipped. On the public v0.3.0 tag the same 1,692 cases give 1,676 passed and 16 skipped, because the tests that cross-check against our measurement lab or the demo skip there. The tag’s full test tree is larger than that core suite, so a plain test run there reports a bigger count.
The hardest objection comes from our own numbers. In the reference session, the part a proxy could pin is the 357-token tool definition, while 45,774 tokens of retrieved documents would reach the model with a record and no check. For that content, execution-time control limits what it can cause, and in-model defenses still cut the rate of attacks the assembly layer can’t see. The paper doesn’t claim in-model defenses fail or that CxBOM prevents prompt injection.
Execution-time control governs what an agent may do after it reads something. CaMeL pulls the control, and data flows out of the trusted query so retrieved data can’t steer the program. On AgentDojo, a benchmark of agent tasks under prompt injection, it solved 77% of tasks with provable security, against 84% for an undefended agent. CxBOM’s proxy governs which tool definitions the model was shown, and the emitter’s record keeps the receipt for the whole context. You want both.
The paper also compares CxBOM with ETDI, MCP-Scan, and MCP-Guard. MCP-Scan already pins tools by hashing their descriptions. ETDI adds immutable versioned tool definitions with cryptographic identity verification, and it authenticates the tool provider, which CxBOM doesn’t.
By the paper’s table, CxBOM is the only one of the four that pins the server’s launch, while the wrapper stays in the config. It’s also the only one that records each model call’s context, in pipelines that send token ids to an open-weight endpoint. It doesn’t scan a new definition for poisoning the first time it sees it, which MCP-Scan and MCP-Guard do.
You Just Moved the Risk to the Approval Click
A signed pin proves a definition didn’t change, and it proves nothing about whether the definition was evil on the day you approved it. That’s the strongest argument against pinning, and the paper concedes it. Approval is the only defense against a definition that was malicious when someone approved it.
“Poisoned definition at first sight” is one of the classes our own threat model lists as not covered. The approval summary prints only the first 200 characters of each new or changed item’s canonical form, so an instruction buried later in a long description, or in a nested parameter description, can get approved unseen.
MCPTox measured how often poisoned tool definitions work. Its authors built 1,348 malicious test cases on 45 live MCP servers and 353 real tools and ran them across 20 LLM agent settings. The most vulnerable, o1-mini, averaged 72.8% attack success, and the average across all 20 was 36.5%.
The paper puts the lock in the same problem space as TUF, in-toto, and Sigstore, which sign metadata for software artifacts. A signed pin proves the definition you’re running is the one a key approved and says nothing about whether it’s safe. The approver the record names is that key, which isn’t the same as an authenticated person.
Pinning moves the risk to the approval, and the check then runs on every listing a running server returns for the life of the session. We haven’t measured how often a reviewer catches a poisoned definition, or how many re-approvals a normal upstream release forces. The paper names the complement it hasn’t built, a learned detector at approval time tested against a published set of poisoned definitions like MCPTox, and the reference implementation has none.
Four weaknesses remain in this preview, and the approval display is one of them. No entry expires unless its signer set an end time, and this version has no command to revoke a key. A stolen owner key keeps signing valid entries until you delete it and the trust manifest by hand and approve every server again.
A --state-dir flag that a workspace writer controls can point the proxy at an empty directory. That resets the high-water mark and lets an older entry replay. Environment variables declared secret aren’t hashed either, so a writer can swap a credential or an endpoint held in one.
The proxy’s log and the records also aren’t tamper-evident against another process running as the same user. They help you investigate an attack that worked through the model. They won’t hold up on a host that’s already compromised.
The proxy works with any MCP host and any model, so the pinning protects you today whether you run Claude, GPT, or something local. The record is narrower. To get exact token counts per component, the emitter tokenizes each piece of the context itself and sends the model raw token IDs, which means it has to know the model’s chat template exactly.
Version 0.3.0 pins two templates by hash (Phi-3.5-mini-instruct and Qwen3) and refuses anything else. It also needs an endpoint that accepts token IDs, like llama.cpp or vLLM, so closed APIs such as OpenAI’s and Anthropic’s can’t produce a record yet. MCP hosts that build their own context still get the proxy’s pins and its log, just no record.
An MCP host that assembles its own context gets pins and a log with no CxBOM record. If your agents run inside a host that assembles its own context, the proxy is the part you can use today.
What would change my mind on the bigger thesis is an in-model provenance defense that survives adaptive attacks as strong as the 12-defense study’s, which combined automated search with human red teaming.
In our reference session, the operator wrote 45 of the 46,205 tokens, and its Context Bill of Materials record accounts for every one of the other 46,160.
What to do next
If you run stdio MCP servers on Linux or macOS, put the proxy in front of one this week. Follow the setup in the README, approve the server at an exact package version, accept each tool and the server’s instructions by hash with a written reason, and swap the proxy into your MCP config in place of the server command. The pin covers the top-level package and not its dependencies, so for code you need pinned in full, run the server from a local tree pinned at approval, with its venv made with --copies inside that tree.
If you sit on the governance side, ask your team which MCP servers your agents launch and who signed off on each tool definition they read. If nobody can answer, that’s your first finding.
Read the scope page and Table 4 of the paper before you tell anyone you’re covered. Between them they list the attack classes and channels this version leaves open, and Sections 4 and 9 of the paper add the rest of its limits.
Signed approvals belong in the Run stage of the CARE framework (Create, Adapt, Run, Evolve) we use with clients at RockCyber, and the classes this version doesn’t cover belong on your Evolve list. Send schema feedback to spec@cxbom.org, file issues on the reference repo, and find the back catalog at RockCyber Musings.
👉 Paper: https://doi.org/10.5281/zenodo.23653997
👉 Github: https://github.com/CXBOM/cxbom-reference/tree/v0.3.0
👉 Subscribe for more AI security and governance insights with the occasional rant.
👉 Visit RockCyber.com to learn more about how we can help with your traditional Cybersecurity and AI Security and Governance journey.
👉 Want to save a quick $100K? Check out our AI Governance Tools at AIGovernanceToolkit.com
👉 As a bonus, check out this conversation with Chris Hughes over at Resilient Cyber, who’s also a reviewer on the paper, to discuss all things about the 2026 update to the OWASP Top 10 for LLMs, including why we need to worry about model context.








