Concepts
Redaction and purge
Heddle is content-addressed and immutable on purpose — but leaked credentials, PII, and accidentally-committed secrets need to come out. Redaction is the first-class primitive that reconciles both: an attributed (and optionally signed) operation that swaps a stub into every materialized view of the blob, leaving a Redaction tombstone in the DAG. Purge is the hard form, for the cases where the bytes themselves must leave local disk.
The tension
Immutability is the feature. Every state has a content address, every capture is forever, every reviewer can trust that the history they're reading wasn't quietly rewritten. That guarantee is what makes the rest of Heddle work.
But sometimes the bytes themselves are wrong. Someone pasted an API key into a comment. A test fixture contains real customer data. A blob got committed that violates GDPR. The bytes need to come out — yet you don't want to undermine the immutability story.
The naive answer is "amend history and re-sign everything." That answer has burned every team that's tried it. Heddle's answer is redaction: the bytes leave, the receipt stays.
The receipt model
A redaction operation does five things:
- Resolve the blob. The operator names a
(state, path)pair on the command line; Heddle looks up the blob's BLAKE3 hash at that location. - Build a Redaction object — the tombstone — carrying the redacted blob hash, the state, the path, the operator's reason, the redactor identity (name + email from the principal), and the timestamp.
- Sign optionally. When
--sign-withis passed, Heddle signs the canonical payload with Ed25519, RSA, or P-256 (per the key's PEM header or--sign-algo) and attaches the signature to the Redaction. Signing is the auditor's verification anchor; the redactor identity is recorded either way. - Store the tombstone in the redactions
store. The store is keyed by blob hash, so a single
declaration causes the stub to render everywhere that
blob's address surfaces — across every state, every
thread, every mounted view.
--all-statesadds an extra audit OpRecord entry for each additional(state, path)the blob occupies. - Append
OpRecord::Redactto the oplog. The audit trail lives in the same append-only record as every other action — see the oplog.
After redact: the blob's address returns a stub on materialize, but the original bytes stay on disk. The Redaction tombstone is in the DAG forever — auditors can verify the operation by re-walking the canonical payload and checking the signature, if any.
Purge vs redact
Redaction is the soft form: bytes stay on disk; the stub
plus the tombstone do the work. Purge is the hard form:
the loose blob is deleted from local storage, and an OpRecord::Purge entry is appended to the
oplog. The Redaction tombstone itself is unchanged in
shape — only its purged_at field is set. All
audit fields (redactor, reason, timestamp, signature) are
preserved.
Two honest gotchas:
- Pack files. If the blob has been packed into a packfile, purge can remove the loose copy but the packed copy persists until you repack. Purge warns when this happens.
- Replicas need a signed purge to propagate. A purge ships over the wire as a Redaction carrying
purged_at, and receivers fail closed — unsigned, tampered, and untrusted-key records are all refused at the receive boundary. Runheddle purge apply --sign-with <key.pem> --forceso the operation can travel (--forceis the irreversibility confirmation purge always requires), and make sure each receiving replica has the signer's public key in[redact] trusted_keys(manage it viaheddle redact trust add --from-pem <key.pem>). An unsigned purge still applies locally; it just doesn't propagate. Note that a purge only drops the loose copy on each replica — packfile repack after wire-purge is an operator follow-up.
Purge is irreversible and refuses to run without --force. Use it only when policy requires
the bytes themselves to leave disk — leaked credentials
that need to be unrecoverable, certain regulated PII.
For routine secret-out operations where the audit trail
is enough, redact alone is the right tool.
The audit trail
Every redaction writes an OpRecord::Redact to
the oplog and stores a Redaction tombstone in the DAG. The
tombstone carries the redactor identity (name + email from
the principal), the timestamp, the redacted blob hash, the
state and path the operator named, the reason, and — when --sign-with was passed — a cryptographic
signature over the canonical payload. Auditors verify the
operation via heddle redact show, which
reports a three-state signature status: verified, unsigned,
or tampered.
Redact a leaked API key from a test fixture
bash$ heddle redact apply HEAD --path src/test-fixtures/auth.json --reason "leaked API key" --sign-with ~/.heddle/keys/ops.pemredacted src/test-fixtures/auth.json (b3a8e201) in d01a8b4e (redaction r0a1b2c3) reason: leaked API keyWhy this isn't an escape hatch
Redaction is intentionally not a general-purpose history editor. You can't redact a thread's resolution. You can't redact someone's signature. You can't redact an oplog entry. The only thing you can redact is a blob — and even then, the redaction itself appears in the oplog as a first-class event.
The promise to reviewers stays intact: history wasn't rewritten. Specific bytes were withdrawn, with a paper trail.
The commands
heddle redact— apply a redaction, list active redactions, show a tombstone. The soft form: the bytes leave materialized views; the receipt stays for audit.heddle purge— harden a redaction into a physical delete. The hard form: the bytes themselves leave disk. Irreversible.
Related: attribution and signing for how the redactor's identity is recorded; the oplog for where redaction tombstones land.