Skip to content

Redacted

Secrets — API keys, passwords, tokens — have a way of escaping into log lines, error messages, and serialized output. Redacted<A> wraps a sensitive value so that its string, JSON, and inspection representations all render as <redacted>, never the underlying secret. The real value is held off to the side and is only recoverable by explicitly calling Redacted.value, which makes every point of exposure visible and greppable in your code.

import { Redacted } from "effect"
// Wrap a secret — the value is now protected from accidental logging
const apiKey = Redacted.make("sk-1234567890abcdef")
console.log(apiKey) // <redacted>
console.log(`Bearer ${apiKey}`) // "Bearer <redacted>"
console.log(JSON.stringify({ apiKey })) // {"apiKey":"<redacted>"}
// You must opt in, explicitly, to read the real value
console.log(Redacted.value(apiKey)) // "sk-1234567890abcdef"

Every accidental path — console.log, string interpolation, JSON.stringify — shows <redacted>. Only the deliberate Redacted.value(apiKey) call exposes the secret, so a code reviewer can audit exactly where secrets are read.

import { Redacted } from "effect"
const secret = Redacted.make("my-password")
// Recover the wrapped value (use sparingly, at the point of use)
const plain = Redacted.value(secret)

You can attach a label, which appears in the rendered output. The label itself is not secret, so it helps you tell redacted values apart in logs without revealing anything:

import { Redacted } from "effect"
const token = Redacted.make("secret-token", { label: "api-token" })
console.log(token) // <redacted:api-token>

Redacted compares by the wrapped value, so two redacted wrappers holding the same secret are Equal.equals — without either of them exposing the secret:

import { Redacted, Equal } from "effect"
console.log(Equal.equals(Redacted.make("abc"), Redacted.make("abc"))) // true
console.log(Equal.equals(Redacted.make("abc"), Redacted.make("xyz"))) // false

The most common source of a secret is configuration. Config.redacted reads an environment value and wraps it in a Redacted automatically, so a misconfigured log statement can never print it. See Configuration for the full configuration story.

import { Config, ConfigProvider, Effect } from "effect"
const program = Effect.gen(function* () {
// apiKey is a Redacted<string>, not a raw string
const apiKey = yield* Config.redacted("API_KEY")
console.log(apiKey) // <redacted>
})
const provider = ConfigProvider.fromEnv({
env: { API_KEY: "sk-1234567890abcdef" }
})
Effect.runSync(
program.pipe(
Effect.provideService(ConfigProvider.ConfigProvider, provider)
)
)
// Output: <redacted>

When you actually need to send the key — as an Authorization header, say — call Redacted.value at that single, intentional point. Everywhere else the value stays protected.

For extra safety, Redacted.wipeUnsafe deletes the stored value, after which any further Redacted.value call on that wrapper fails. Use it once a short-lived secret is no longer needed so it cannot be recovered later.

When a secret crosses a boundary that decodes input — an HTTP API request body, a config file — use Schema.Redacted from the Schema module so the value is wrapped as part of decoding. That is exactly what Config.redacted does under the hood (Schema.Redacted(Schema.String)).