Core concepts /Redaction

Redaction

Redaction strips a sensitive field before the receipt is signed, so the value never reaches HESO but the receipt still proves the field was there. Pick one of two modes by whether you might need the value back.

A receipt records the fields of an action so the verdict means something — what amount was paid, which account changed, which host was called. 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, while the receipt still proves the field was part of the action.

Two modes

Choose the mode by one question: will you ever need the value back?

Destructive — you never need the value back

The value is dropped and replaced; there is no salt and no sidecar, so the original is gone for good. Use this when recording that a field existed is enough — a member ID you only need to attest was deleted, a token you must not persist. The field still appears as a marker, but its commitment is the empty string (never omitted).

Commit-and-reveal — you might reveal it later

HESO stores a salted BLAKE3 commitment, never the value. Use this when an auditor might later need the value: you hand them the value and the salt, they re-hash and confirm the result matches the commitment in the receipt. The per-field salt — random bytes mixed into the hash — is kept out of the receipt in a sidecarheld by an authorized party, so a small or predictable value can’t be recovered by guessing inputs and hashing each one.

Redact in code

In Python, declare the argument names on @heso.tool. Those fields are committed and stripped before signing; the rest of the call is signed normally.

agent.pypython
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. The
    # rest of the call is captured and signed normally.
    return vendor.request(endpoint, key=api_key)

In Node you call the primitives directly. Both take the fields as a JSON string and a list of field paths. redactCommitJs additionally takes one 32-byte Buffer salt per field and returns the record plus the sidecar of salts. See the Node core reference for full signatures.

redact.tsts
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

How it appears in the receipt

When any field is redacted, the receipt’s content.redaction carries a RedactionRecord: the mode, one marker per redacted field, and (in commit-and-reveal only) a merkle_root tying the markers together. Each marker is { field_path, algorithm, commitment }.

field_pathstringrequired
Dotted path to the redacted field, e.g. action.fields.api_key.
mode"destructive" | "commit_and_reveal"required
Set once on the record; decides what commitment holds.
commitmentstringrequired
A 64-hex BLAKE3 digest in commit-and-reveal mode; the empty string in destructive mode. Never omitted in either mode.

Commit-and-reveal carries a real digest and a Merkle root:

receipt.json (excerpt)json
"redaction": {
  "mode": "commit_and_reveal",
  "markers": [
    {
      "field_path": "action.fields.api_key",
      "algorithm": "blake3",
      "commitment": "7d4a…b9f1"
    }
  ],
  "merkle_root": "c1e8…02ab"
}

Destructive carries the same markers with an empty commitment and no Merkle root:

receipt.json (excerpt)json
"redaction": {
  "mode": "destructive",
  "markers": [
    {
      "field_path": "action.fields.member_id",
      "algorithm": "blake3",
      "commitment": ""
    }
  ]
}

The RedactionRecord is part of the signed content, so the action_hash covers it and verification still passes — the commitment is part of the signed bytes. The values in action.fields on a finished receipt are the post-redaction values, so the cleartext is never what gets signed, chained, or verified.

Note

An empty commitment is a well-formed marker — the verifier expects it for destructive redaction. A missing commitment key is malformed and fails verification.

What redaction proves — and what it doesn't

A 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. 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.

Next steps