A receipt records the fields of an action so the verdict means something — you can see what amount was paid, which account was touched, which host was called. But some of those values should never leave your process: an API key, a member ID, a token. Redaction rewrites those fields before the receipt is signed, so HESO sees a commitment — a one-way fingerprint of the value — or nothing at all, yet the receipt still proves the field was part of the action.
Why redact
The fields on an action are part of the signed receipt, so they stick around, get replicated, and can be checked by anyone who holds the receipt. That is the point — but it is the wrong place for a secret. Redaction lets you keep the proof without keeping the cleartext.
- You want a receipt for a vendor call but the call carried an
api_keyyou must not persist. - You must record that a member record was changed without writing the member’s identifier into a log that outlives the action.
- A reviewer needs to confirm a field existed and matched policy, but is not entitled to read its value.
Redaction always happens before signing. The values in action.fields on a finished receipt are the post-redaction values, and the action_hash covers exactly those — so the cleartext is never what gets signed, chained, or verified.
Destructive
In destructivemode the value is simply removed. The field still appears in the redaction record as a marker, but the marker’s commitment is the empty string — it is never omitted. There is no salt and no sidecar: the original is gone, and you cannot later reveal it. Use destructive mode when you never need the value back and only need to attest that the field was present.
"redaction": { "mode": "destructive", "markers": [ { "field_path": "action.fields.member_id", "algorithm": "blake3", "commitment": "" } ]}
An empty commitment is a real, well-formed marker — the verifier checks that markers are well-formed, and an empty string is the correct, expected value for destructive redaction. A missing commitment key is malformed and fails verification.
Commit-and-reveal
In commit-and-reveal mode the value is replaced by a BLAKE3 commitment — a 64-character lowercase hex digest hashed from the value plus a per-field salt(random bytes mixed in so the hash can’t be guessed). The salts are kept out of the receipt in a separate sidecar, held by an authorized party. Later, that party can take the original value, the salt, and the commitment, hash them again, and show the result matches — a reveal. A merkle_root over the markers ties them together, and it is present only in commit-and-reveal mode.
"redaction": { "mode": "commit_and_reveal", "markers": [ { "field_path": "action.fields.api_key", "algorithm": "blake3", "commitment": "7d4a…b9f1" } ], "merkle_root": "c1e8…02ab"}
The salt matters: without it, a small or predictable value (a member ID, a yes/no flag) could be recovered by guessing inputs and hashing each one. A fresh salt per field makes the commitment safe to publish while keeping the reveal exact.
The redaction record
When any field is redacted, the receipt’s content.redaction carries a RedactionRecord. It is part of the signed content, so the verifier checks its markers are well-formed as one of the verify gates.
- mode"destructive" | "commit_and_reveal"required
- Which redaction mode produced these markers.
- markersRedactionMarker[]required
- One marker per redacted field, in order.
- merkle_rootstring
- Merkle root tying the markers together. Present only for
commit_and_reveal; omitted in destructive mode.
Each entry in markers is a RedactionMarker:
- field_pathstringrequired
- Dotted path to the redacted field, e.g.
action.fields.member_id. - algorithm"blake3"required
- The commitment hash function — always BLAKE3.
- commitmentstringrequired
- A 64-hex BLAKE3 digest in
commit_and_revealmode; the empty string indestructivemode. Never omitted in either mode.
Redacting in code
In Python the SDK does the work for you. Declaring redact=[…] on @heso.tool runs commit-and-reveal on those argument names automatically, before the receipt is signed.
import heso heso.init() @heso.tool(redact=["api_key"])def call_vendor(api_key: str, endpoint: str) -> dict: # api_key is committed (BLAKE3 + per-field salt) before the # receipt is signed; the cleartext never reaches HESO. return vendor.request(endpoint, key=api_key)
In Node you call the redaction primitives directly. Both take the fields as a JSON string and a list of field paths, and return the modified fields. redactCommitJs additionally takes one 32-byte Buffer salt per field and returns the record and the sidecar of salts. See the Node core reference for the full signatures.
import * as core from "@hesohq/core"import { randomBytes } from "node:crypto" const fields = JSON.stringify({ member_id: "m_4821", region: "eu" }) // Destructive: the value is gone — the marker commitment is "".const stripped = core.redactDestructiveJs(fields, ["member_id"]) // Commit-and-reveal: one 32-byte salt per redacted field.const salts = [randomBytes(32)]const out = core.redactCommitJs(fields, ["member_id"], salts)// out.fields → fields JSON with the value replaced by a commitment// out.redactionRecord → the RedactionRecord to embed in the receipt// out.sidecar → the salts an authorized party keeps to later reveal
What stays private
Because redaction runs before signing, the cleartext never reaches HESO — not the API, not the audit chain, not a reviewer’s browser. What the receipt carries is a commitment (or, in destructive mode, nothing) plus a marker proving the field was part of the action. The reveal, when it happens, is between you and whoever holds the sidecar of salts.
A redaction marker proves the field was present in the action and was redacted under a known mode. In commit-and-reveal it also lets an authorized holder later prove a specific value matches the commitment.
It does not reveal the value to anyone without the sidecar, and in destructive mode the value is unrecoverable by design — HESO never held it. As with every receipt, this records what was authorized — the redacted field included — not whether the action succeeded downstream.
