V
Vikas Luthra
Guest
SHA-256 hash chains, Merkle trees, boundary capture, and the engineering behind AgentLedger
An AI agent just priced a deal, approved a transaction, or updated a customer record. Months later, someone asks a simple question: what did the agent actually do, and can you show that the record has not been changed since?
That second question is easy to underestimate. Traditional automation is usually governed by predefined logic that can be inspected and tested. Agent decisions can depend on probabilistic model output and the state the agent saw at that moment. Re-running the same request later may produce a different path. That makes the contemporaneous record unusually important.
I built AgentLedger to make changes to that recorded history detectable. This article is about the engineering behind it: structured action records, deterministic serialization, SHA-256 hash chains, Merkle sealing, verification, where capture happens, and the limits of what the design can prove.
One framing note before the code. This does not replace observability tools like OpenTelemetry. Those tell an operator how the system ran and help you debug it. AgentLedger answers a different question: giving the organization a checkable record of what was captured about the autonomous actor’s actions. With an independent external anchor, that record can also be verified against a reference outside the system that stores it.
Disclosure: I am the author of AgentLedger, the open-source project described in this article.
Every meaningful step submitted to AgentLedger becomes an action record. A record captures which agent acted, what kind of action it was, the input context, the output or business result, and the agent-provided rationale. AgentLedger uses a fixed set of action types so the record shape stays consistent across platforms.
The TypeScript library is deliberately simple to use. Start a session, record actions as they happen, then seal and verify it.
The field called
Before you can reliably hash a record, you need one consistent way to turn it into text. That's what canonicalization means. A cryptographic hash is deterministic only if the bytes going into it are deterministic. Two objects can represent the same logical information and still serialize differently because field order, missing values, dates, or formatting differ. If serialization can drift, verification becomes unreliable. AgentLedger uses the same hashing-and-sealing model in two separate implementations: the platform-agnostic TypeScript core and a Salesforce-native implementation in Apex. In the Salesforce version, the canonicalization rule is explicit.
It builds an ordered list of fields, serializes that list as JSON, and hashes the resulting string. The order is locked:
The implementation also normalizes null string fields to an empty string before serialization. A missing sequence number falls back to 0. The timestamp is converted with Apex
One detail matters: the code does not arbitrarily trim the text inside input, output, or rationale fields. If someone changes whitespace inside one of those hashed fields, that is a content change and should change the hash. The deterministic part is the serialization structure, field order, and null handling.
This format becomes part of the protocol. Change the field order or serialization rules after deployment and old records will hash differently. That is why canonicalization should be versioned deliberately rather than treated as an implementation detail that can be changed casually.
A cryptographic hash is essentially a fingerprint of some data. Once the canonical representation exists, AgentLedger computes a SHA-256 hash for the record and includes the previous record’s hash in the next record. In simplified form, the records form a hash-linked sequence:
If record 1's content changes after the fact, its recomputed hash no longer matches its stored hash, so verification detects the modification. If someone also replaces record 1's stored hash with the newly computed value, record 2 still points to the original H1, so the chain linkage breaks. Rewriting one record cleanly therefore requires rewriting the downstream chain as well. The structure assumes append-only behavior, but it does not magically make the underlying database immutable.
The verifier checks more than the current record hash. It also checks that sequence numbers start at zero and increase by one with no gaps, that the first record has no previous hash, and that each later Previous Hash matches the stored hash of the preceding record.
A hash chain is good at showing sequence integrity. Think of the Merkle root as one fingerprint for the ordered list of record hashes in the session. Change one of those hashes and the root changes. The tree structure also lets you verify that an individual record belongs to the sealed session without supplying every other record.
When the session is sealed, AgentLedger takes the record hashes in sequence order, hashes adjacent pairs, duplicates the last leaf when the count is odd, and repeats the process until one root remains. That root is stored as the session fingerprint.
A Merkle proof lets you verify that one record belongs to a sealed session using only the sibling hashes along its path to the root. That lets you verify one record without having to supply every other record in the session.
Verification does not ask a flag whether the session is valid. It rebuilds the evidence from the stored records and compares the result with what was sealed.
Those checks let the verifier distinguish a changed record, a broken sequence, and a count mismatch instead of returning one generic red light.
The Salesforce implementation has a simple end-to-end test that demonstrates the behavior without any special tooling:
That last step is important. AgentLedger proves the current record set is consistent with the sealed fingerprint. By itself, it does not prove that a temporary alteration never happened. That distinction becomes important when you talk about custody and external anchoring.
There is an obvious objection to everything above. If the agent has to call
The answer is to stop relying on the agent to record itself. In the MCP reference demo, capture does not live inside the agent’s logic. It lives in middleware that wraps the MCP client’s
This also handles work that crosses systems. In the demo, one agent run touches five separate MCP servers, and because every call routes through the same gateway, all of it correlates into a single ordered, sealed session with one Merkle root. The record does not fragment just because the work spanned several services.
Here is the honest boundary, and it matters. Every call that routes through the gateway is captured. That is a real guarantee, and it is far stronger than hoping the agent logged itself. It is not the same as proving the agent had no other path at all. An agent given a second client or an out-of-band channel could act outside the gateway, and the receipt would not know. So the accurate claim is that every call through the capture boundary is recorded, not that every action the agent could conceivably take is recorded. Capture is enforced for calls routed through that boundary. Guaranteeing there is no path around the boundary is a deployment problem, not something the cryptography settles.
Hashing does not remove the need for a trust boundary. If the records and the Merkle root live in the same system under the same privileged administrator, someone with enough access could rewrite the records, recompute the hashes, recompute the root, and leave behind a self-consistent history that is still false.
The core framework therefore gives tamper evidence against changes that are not accompanied by a full privileged rewrite. Stronger assurance requires the sealed fingerprint to leave the system that stores the records. The external anchor does not have to be a blockchain. Any independently controlled, tamper-resistant store, such as a transparency log or trusted timestamping service, could serve the same purpose.
In the reference demo, I use Arbitrum Sepolia, a public test network outside the record store that provides an independently maintained reference for the sealed root. The only AgentLedger audit payload sent outside the enterprise boundary is the 32-byte Merkle root; the underlying business data and recorded rationale remain inside.
The point is to put the fingerprint somewhere it cannot be silently rewritten along with the internal records. Anchoring addresses one specific weakness: retrospective privileged rewriting of both the records and the locally stored fingerprint. It does not solve completeness or source authenticity, and it remains a pluggable extension rather than something bundled into the core package.
A provenance mechanism becomes less useful when it claims more than it can establish. These boundaries are part of the design:
This is also why I use the term tamper-evident rather than tamper-proof. The framework is built to make inconsistent changes detectable. Stronger guarantees depend on how the fingerprint is anchored and how capture is enforced.
AgentLedger is open source under the MIT license. The repository includes the TypeScript core, the Salesforce implementation, the MCP gateway demo that captures across five servers and anchors a real Merkle root to a public test network, the hashing and Merkle logic, and the roadmap for the harder problems that are not solved yet.
If you run agents that make decisions that matter, try the failure path instead of trusting the happy path. Seal a session, verify it, change a recorded field, and verify again. Then look at the boundary conditions and decide what additional controls your environment needs.
An AI agent just priced a deal, approved a transaction, or updated a customer record. Months later, someone asks a simple question: what did the agent actually do, and can you show that the record has not been changed since?
That second question is easy to underestimate. Traditional automation is usually governed by predefined logic that can be inspected and tested. Agent decisions can depend on probabilistic model output and the state the agent saw at that moment. Re-running the same request later may produce a different path. That makes the contemporaneous record unusually important.
I built AgentLedger to make changes to that recorded history detectable. This article is about the engineering behind it: structured action records, deterministic serialization, SHA-256 hash chains, Merkle sealing, verification, where capture happens, and the limits of what the design can prove.
One framing note before the code. This does not replace observability tools like OpenTelemetry. Those tell an operator how the system ran and help you debug it. AgentLedger answers a different question: giving the organization a checkable record of what was captured about the autonomous actor’s actions. With an independent external anchor, that record can also be verified against a reference outside the system that stores it.
Disclosure: I am the author of AgentLedger, the open-source project described in this article.
Step 1: Capture the action as a structured record
Every meaningful step submitted to AgentLedger becomes an action record. A record captures which agent acted, what kind of action it was, the input context, the output or business result, and the agent-provided rationale. AgentLedger uses a fixed set of action types so the record shape stays consistent across platforms.
Code:
enum ActionType {
QUERY, // Agent reads or retrieves data
UPDATE, // Agent modifies existing records
CREATE, // Agent creates new records
DELETE, // Agent removes records
DECISION, // Agent makes a decision or selects a path
ESCALATION, // Agent escalates to a human operator
TOOL_CALL, // Agent invokes an external tool or API
CALCULATION, // Agent performs a calculation
VALIDATION // Agent validates data against rules
}
The TypeScript library is deliberately simple to use. Start a session, record actions as they happen, then seal and verify it.
Code:
import { AgentLedgerSession, ActionType } from '@vluthra/agent-ledger';
const session = new AgentLedgerSession({
agentId: 'pricing-agent',
platform: 'salesforce',
initiator: 'quote-workflow',
});
session.record({
agentId: 'pricing-agent',
actionType: ActionType.CALCULATION,
input: { basePrice: 200, discountPct: 15 },
output: { finalPrice: 170 },
reasoning: 'Applied Gold tier discount to base price',
});
const sealed = session.seal();
const result = session.verify();
console.log(result.valid); // true
The field called
reasoning is a recorded rationale supplied by the agent or workflow. It is not a claim that the framework captures a model’s hidden internal chain of thought. The point is to preserve the explanation the system chose to record alongside the inputs and output.Step 2: Canonicalize before you hash
Before you can reliably hash a record, you need one consistent way to turn it into text. That's what canonicalization means. A cryptographic hash is deterministic only if the bytes going into it are deterministic. Two objects can represent the same logical information and still serialize differently because field order, missing values, dates, or formatting differ. If serialization can drift, verification becomes unreliable. AgentLedger uses the same hashing-and-sealing model in two separate implementations: the platform-agnostic TypeScript core and a Salesforce-native implementation in Apex. In the Salesforce version, the canonicalization rule is explicit.
It builds an ordered list of fields, serializes that list as JSON, and hashes the resulting string. The order is locked:
Code:
sessionId, sequenceNumber, timestamp, agentId, actionType, inputContext, outputResult, reasoning, previousHash
The implementation also normalizes null string fields to an empty string before serialization. A missing sequence number falls back to 0. The timestamp is converted with Apex
String.valueOf(), and JSON.serialize() produces the final canonical representation.
Code:
List<String> parts = new List<String>();
parts.add(safeString(record.Audit_Session__c));
parts.add(String.valueOf(record.Sequence_Number__c != null ? record.Sequence_Number__c.intValue() : 0));
parts.add(safeString(String.valueOf(record.Action_Timestamp__c)));
parts.add(safeString(record.Agent_Id__c));
parts.add(safeString(record.Action_Type__c));
parts.add(safeString(record.Input_Context__c));
parts.add(safeString(record.Output_Result__c));
parts.add(safeString(record.Reasoning__c));
parts.add(safeString(record.Previous_Hash__c));
String canonical = JSON.serialize(parts);
return sha256(canonical);
One detail matters: the code does not arbitrarily trim the text inside input, output, or rationale fields. If someone changes whitespace inside one of those hashed fields, that is a content change and should change the hash. The deterministic part is the serialization structure, field order, and null handling.
This format becomes part of the protocol. Change the field order or serialization rules after deployment and old records will hash differently. That is why canonicalization should be versioned deliberately rather than treated as an implementation detail that can be changed casually.
Step 3: Link every record to the one before it
A cryptographic hash is essentially a fingerprint of some data. Once the canonical representation exists, AgentLedger computes a SHA-256 hash for the record and includes the previous record’s hash in the next record. In simplified form, the records form a hash-linked sequence:
Code:
Record 0: hash(content0 + null) -> H0
Record 1: hash(content1 + H0) -> H1
Record 2: hash(content2 + H1) -> H2
If record 1's content changes after the fact, its recomputed hash no longer matches its stored hash, so verification detects the modification. If someone also replaces record 1's stored hash with the newly computed value, record 2 still points to the original H1, so the chain linkage breaks. Rewriting one record cleanly therefore requires rewriting the downstream chain as well. The structure assumes append-only behavior, but it does not magically make the underlying database immutable.
The verifier checks more than the current record hash. It also checks that sequence numbers start at zero and increase by one with no gaps, that the first record has no previous hash, and that each later Previous Hash matches the stored hash of the preceding record.
Step 4: Seal the session under a Merkle root
A hash chain is good at showing sequence integrity. Think of the Merkle root as one fingerprint for the ordered list of record hashes in the session. Change one of those hashes and the root changes. The tree structure also lets you verify that an individual record belongs to the sealed session without supplying every other record.
Code:
Root Hash
/ \
H(0,1) H(2,3)
/ \ / \
H(0) H(1) H(2) H(3)
When the session is sealed, AgentLedger takes the record hashes in sequence order, hashes adjacent pairs, duplicates the last leaf when the count is odd, and repeats the process until one root remains. That root is stored as the session fingerprint.
A Merkle proof lets you verify that one record belongs to a sealed session using only the sibling hashes along its path to the root. That lets you verify one record without having to supply every other record in the session.
Step 5: Verify by recomputing, not by trusting status
Verification does not ask a flag whether the session is valid. It rebuilds the evidence from the stored records and compares the result with what was sealed.
- Hash-chain integrity: recompute each record hash from its current content.
- Link integrity: confirm each Previous Hash matches the prior record hash and sequence numbers have no gaps.
- Merkle-root integrity: rebuild the root from the record hashes and compare it with the sealed fingerprint.
- Record count: confirm the number of records still matches the count captured for the session.
Those checks let the verifier distinguish a changed record, a broken sequence, and a count mismatch instead of returning one generic red light.
A tamper test you can reproduce
The Salesforce implementation has a simple end-to-end test that demonstrates the behavior without any special tooling:
- Run an agent flow that creates a session, records actions, and seals it.
- Click Verify. The session shows Verified.
- Edit a hashed content field on one audit record, for example the recorded rationale.
- Run Verify again. The session shows Tampered because the recomputed hash no longer matches the stored hash.
- Restore the field to its original value.
- Run Verify again. The session returns to Verified.
That last step is important. AgentLedger proves the current record set is consistent with the sealed fingerprint. By itself, it does not prove that a temporary alteration never happened. That distinction becomes important when you talk about custody and external anchoring.
Where capture happens: move it to the boundary
There is an obvious objection to everything above. If the agent has to call
record() itself, what stops it from simply not recording an action? A provenance trail the agent can opt out of is not worth much.The answer is to stop relying on the agent to record itself. In the MCP reference demo, capture does not live inside the agent’s logic. It lives in middleware that wraps the MCP client’s
tools/call. Every tool request routed through the gateway, and every response or error returned through it, becomes a receipt before the run continues. Declared decisions are captured at defined checkpoints the same way. For calls routed through the gateway, the agent does not choose whether to record. Recording happens because the call is on the path.This also handles work that crosses systems. In the demo, one agent run touches five separate MCP servers, and because every call routes through the same gateway, all of it correlates into a single ordered, sealed session with one Merkle root. The record does not fragment just because the work spanned several services.
Here is the honest boundary, and it matters. Every call that routes through the gateway is captured. That is a real guarantee, and it is far stronger than hoping the agent logged itself. It is not the same as proving the agent had no other path at all. An agent given a second client or an out-of-band channel could act outside the gateway, and the receipt would not know. So the accurate claim is that every call through the capture boundary is recorded, not that every action the agent could conceivably take is recorded. Capture is enforced for calls routed through that boundary. Guaranteeing there is no path around the boundary is a deployment problem, not something the cryptography settles.
The part that is easy to get wrong: independence
Hashing does not remove the need for a trust boundary. If the records and the Merkle root live in the same system under the same privileged administrator, someone with enough access could rewrite the records, recompute the hashes, recompute the root, and leave behind a self-consistent history that is still false.
The core framework therefore gives tamper evidence against changes that are not accompanied by a full privileged rewrite. Stronger assurance requires the sealed fingerprint to leave the system that stores the records. The external anchor does not have to be a blockchain. Any independently controlled, tamper-resistant store, such as a transparency log or trusted timestamping service, could serve the same purpose.
In the reference demo, I use Arbitrum Sepolia, a public test network outside the record store that provides an independently maintained reference for the sealed root. The only AgentLedger audit payload sent outside the enterprise boundary is the 32-byte Merkle root; the underlying business data and recorded rationale remain inside.
The point is to put the fingerprint somewhere it cannot be silently rewritten along with the internal records. Anchoring addresses one specific weakness: retrospective privileged rewriting of both the records and the locally stored fingerprint. It does not solve completeness or source authenticity, and it remains a pluggable extension rather than something bundled into the core package.
What it deliberately does not prove
A provenance mechanism becomes less useful when it claims more than it can establish. These boundaries are part of the design:
- Completeness: it detects changes to records that were submitted and sealed. Capturing at the boundary, as the reference demo does, enforces recording for every call that routes through the gateway, which is a real guarantee. It still does not prove the agent had no path around the gateway, so completeness depends on how capture is deployed, not on the cryptography.
- Source authenticity: it protects submitted values after recording. It does not independently prove that a recorded output exactly matches the underlying business-system mutation.
- Continuous custody: verification proves present consistency with the fingerprint. If an altered value is later restored, the record can verify again.
- Decision quality: it does not decide whether an agent was correct, unbiased, or compliant with a regulation. It preserves and verifies the recorded evidence.
This is also why I use the term tamper-evident rather than tamper-proof. The framework is built to make inconsistent changes detectable. Stronger guarantees depend on how the fingerprint is anchored and how capture is enforced.
Design constraints worth stealing
- Zero external dependencies in the TypeScript core: hashing uses the platform crypto primitive, which keeps the verification path small.
- Deterministic hashing: identical logical inputs must map to identical bytes before hashing.
- One model, two implementations: the TypeScript package and the Salesforce-native implementation are separate codebases that share the same hashing-and-sealing pattern.
- Capture at the boundary: in the reference demo, enforcing capture in middleware around the call, rather than trusting the agent to log itself, is the single biggest credibility gain.
- Append-only by convention: the framework creates audit records and verifies them later rather than updating them as part of normal operation.
- Explicit limits: source authenticity, completeness beyond the boundary, multi-agent orchestration, and binding records to actual mutations are separate problems, stated openly rather than hidden as assumptions.
Try to break it
AgentLedger is open source under the MIT license. The repository includes the TypeScript core, the Salesforce implementation, the MCP gateway demo that captures across five servers and anchors a real Merkle root to a public test network, the hashing and Merkle logic, and the roadmap for the harder problems that are not solved yet.
- GitHub: https://github.com/cluthrv/agentledger
- npm: https://www.npmjs.com/package/@vluthra/agent-ledger
If you run agents that make decisions that matter, try the failure path instead of trusting the happy path. Seal a session, verify it, change a recorded field, and verify again. Then look at the boundary conditions and decide what additional controls your environment needs.