Write-only environments
Values in a write-only environment can be set and rotated, and only workload identities read them back. People and static tokens get the key marked redacted.
A write-only environment withholds its values from people. They are still delivered to deploys and CI.
Who reads a value
| Caller | Gets |
|---|---|
| Credential exchanged from OIDC, AWS IAM or a bound keypair | The value |
| Integration sync to its destination | The value |
Console, penv login credential (pcu_) | redacted |
Static pck_ token issued in the console | redacted |
| Reveal approval | Refused before anyone is asked |
A static token counts as a person: anyone who can copy it can read with it. Move CI and servers to an exchange before you turn this on.
Who may add a workload
A workload reads this environment's values, so adding one needs what turning write-only off needs.
| Action on a write-only environment | Needs |
|---|---|
| Add an OIDC or AWS trust, bind a keypair | environment:update, two-factor authentication and a fresh step-up |
| Connect a CI platform or a GitHub repository | The same |
Trusts and keypairs that exist when you turn write-only on keep reading. Review them under Machine identities first (machine identities).
Writes are unaffected. penv set, penv push and the console still set and rotate values. They can never show a value back.
Turn it on
Project, environment, Write-only. Pro and Enterprise (plans).
| Action | Needs |
|---|---|
| Turn on | environment:update and a plan that includes it |
| Turn off | environment:update, two-factor authentication and a fresh step-up, on any plan |
New environment named production | On by default, on Pro and Enterprise |
| Duplicate a write-only environment | The copy is write-only |
After a downgrade it stays on until someone turns it off.
What penv pull writes
penv pull writes a marker, not a value, for each withheld key:
penv pull --env productionLOG_LEVEL=info
# penv:redacted DATABASE_URL
# penv:redacted STRIPE_KEYThe marker is a comment, so no dotenv loader reads it as a value. With the @penv= header removed (local mode), penv still refuses to run without the key:
penv --json run --env production -- node server.js{
"error": "redacted",
"message": "DATABASE_URL in production is write-only in penv-cloud.",
"fix": "Set DATABASE_URL in .env.production.local. penv-cloud keeps the production value write-only."
}A value in .env.production.local overrides the marker (value files).
What a person sees
| Surface | Result |
|---|---|
penv run, penv bundle | Refused with redacted, exit 6, naming every key |
penv check, penv ls, penv why | The key is present and redacted, never missing |
penv reveal | Refused, exit 6 |
| Console value cell | Write-only badge; the pencil still sets a new value |
| Console export | Redacted keys are named and left out of the file |
GET /api/v1/envs/... | The key with its schema and "redacted": true, and "writeOnly": true on the body |
| Read a secret | 409 redacted |
| Bulk read | The key in redacted, apart from skipped |
redacted is never an absence. The value exists; do not overwrite it.
Audit
| Action | Row |
|---|---|
| Turn on, turn off | environment.write_only_enabled, environment.write_only_disabled |
| A value withheld from a caller | secret.read_redacted, one per key |
Encryption
Write-only controls who can read a value. It does not change how the value is encrypted: every environment uses the same envelope (encryption in the cloud). We can still decrypt the value to deliver it to a workload, so this is not zero-knowledge.
Short-lived values
A dynamic key has no stored value. On request we ask the upstream system to mint a credential, record the lease, and let the credential expire.
The audit log
We record every secret, credential, member, billing and configuration action in your workspace as one append-only entry that nobody in your workspace can edit or delete.