# Secrets and redaction
Source: https://docs.treeship.dev/concepts/secrets-and-redaction

> What Treeship hashes, what it scrubs, what it misses, and why the difference matters.

Receipts get published. So the question is not "does Treeship try to hide
secrets" — it is **which parts of a receipt are safe by construction, and
which are safe only by best effort.**

Two different mechanisms are at work, and conflating them is how a credential
ends up in a signed artifact.

## The guarantee: hashes

Everything load-bearing in a receipt is a **digest, not content**.

| field                  | what it holds                                            |
| ---------------------- | -------------------------------------------------------- |
| `args_hash`            | `sha256` of the input                                    |
| `output_hash`          | `sha256` of the output                                   |
| `readback`             | digest of an independent post-state read                 |
| `output_digest`        | `sha256` of stdout + stderr                              |
| `env_allowlist_digest` | digest over environment variable **names**, never values |

A hash proves a specific input produced a specific output without disclosing
either. You can hand someone a chain proving forty tool calls ran in a given
order with given outcomes and disclose nothing about the data they touched. If
you later want to prove *what* one call did, you reveal that one input and let
them recompute the digest.

**This is the part that is safe by construction.** A digest cannot leak what it
never contained.

## The courtesy: command scrubbing

`treeship wrap` also records a human-readable `command` string and an `argv`
vector, so a receipt is legible to a person. Those hold real text, so they run
through a redactor first.

The redactor is a **pattern denylist**. It splits on whitespace and replaces any
token containing a known secret-ish pattern (`TOKEN=`, `--api-key=`,
`AWS_SECRET`, and similar) with `[REDACTED]`.

Denylists catch what they know about. Measured against the shipping binary:

| command                                | recorded as                        | caught |
| -------------------------------------- | ---------------------------------- | ------ |
| `--token=sk-SECRET`                    | `[REDACTED]`                       | ✅      |
| `AWS_SECRET_ACCESS_KEY=sk-SECRET`      | `[REDACTED]`                       | ✅      |
| `--token sk-SECRET`                    | `--token sk-SECRET`                | ❌      |
| `-H 'Authorization: Bearer sk-SECRET'` | `Authorization: Bearer sk-SECRET`  | ❌      |
| `postgres://user:PASSWORD@host/db`     | `postgres://user:PASSWORD@host/db` | ❌      |
| `-p sk-SECRET`                         | `-p sk-SECRET`                     | ❌      |

The pattern is consistent: **secrets attached with `=` are caught; secrets
passed as a separate argument, inside a header string, or embedded in a URL are
not.** The redactor sees `--token` and `sk-SECRET` as two unrelated words, and
nothing about the second one looks like a secret.

So: treat scrubbing as a seatbelt, not a vault.

## What follows from that

**Do not put secrets in argv.** This is true regardless of Treeship — argv is
visible in `ps` output to every process on the machine, and shells record it in
history. Treeship makes an existing exposure durable and signed, which is worse,
but the exposure is not new. Use environment variables or a file.

**Environment values are never recorded.** `env_allowlist_digest` covers the
sorted **names** of a fixed set of behavior-affecting variables. The allowlist
deliberately excludes anything whose *presence* is itself sensitive (`*_TOKEN`,
`*_KEY`, `AWS_*`), because a digest that changes when a credential appears leaks
that a credential appeared.

**Review before publishing.** Publishing is explicit and never automatic. A
receipt sits locally until you push it, and `treeship package inspect` shows
exactly what a reader would see.

**A receipt is a capability, not a publication.** Receipt pages are excluded
from search indexes, so a receipt is readable by whoever holds the URL and not
discoverable without one. That limits blast radius; it does not undo a leak.

## If a secret does land in a receipt

Rotate the credential. That is the whole answer.

Receipts are immutable and content-addressed by design — editing one to remove a
secret would change its ID and break every chain link after it, which is exactly
the property that makes them evidence. There is no scrub-in-place, and offering
one would be worse than not: a record you can quietly edit is not a record.

If the receipt was published, assume it was read.

## Where this is heading

The denylist should be wider, and per-field disclosure rules would beat one
global scrubber. But the honest framing today is:

> **Hashes are the guarantee. Scrubbing is best-effort. Keep secrets out of
> argv and the guarantee is enough on its own.**

## Related

* [What a receipt proves](/docs/concepts/what-receipts-prove) — and does not
* [Effect receipts](/docs/concepts/effect-receipts) — hashed evidence in detail
* [Artifacts](/docs/concepts/artifacts) — the signed object