Your logs end up in places you do not control. A Lambda console.log({ req }) writes every Authorization header into CloudWatch for the length of the retention policy. Sentry.captureException(err, { extra: payload }) ships that payload to Sentry's servers. A Datadog agent indexes your stdout JSON field by field, so an sk_live_ Stripe key or a cookie value lands in a search index the whole workspace can query. Once a credential reaches any of those, "rotate" is the only safe response.
npm install autoredactpnpm add autoredactyarn add autoredactZero configuration required.
import { createLogger } from 'autoredact'
const logger = createLogger()
// Sensitive keys redact regardless of where they sit in the tree.
logger.info({ user: { id: 'u_1', api_key: 'sk_live_xxx', email: 'abc@xyz.com' } }, 'user signed in')
// {
// "time": "...",
// "level": "info",
// "msg": "user signed in",
// "user": {
// "id": "u_1",
// "api_key": "[REDACTED]",
// "email": "abc@xyz.com"
// }
// }
// Credentials buried inside a free form string are caught by shape, not by path.
logger.info({ note: 'rotated AKIAIOSFODNN7EXAMPLE yesterday' })
// {
// "time": "...",
// "level": "info",
// "note": "rotated [REDACTED] yesterday"
// }
// Errors get scrubbed inside both message and stack, even though the surrounding
// keys are 'message' and 'stack', not 'databaseUrl'.
logger.error(new Error('cannot connect to postgres://app:hunter2@db.example.com/mydb'))
// {
// "time": "...",
// "level": "error",
// "msg": "cannot connect to [REDACTED]",
// "err": {
// "name": "Error",
// "message": "cannot connect to [REDACTED]",
// "stack": "Error: cannot connect to [REDACTED]\n at ..."
// }
// }About thirty default phrases plus their snake, camel, and kebab variants. The full default list:
password, passwd, pwd, passphrase, secret, secrets, credential, credentials, token, tokens, bearer, refresh_token, id_token, access_token, authorization, auth, cookie, cookies, set-cookie, api_key, access_key, private_key, client_secret, session, otp, mfa, totp, ssn, sin, credit_card, card_number, cvv, cvc, cvv2, tax_id, ein, iban, routing_number, pin, pincode.
The tokenizer splits keys at camel boundaries, underscores, and dashes, so userApiKey, user_api_key, and x-api-key all match the phrase ['api', 'key'] once. There is no per case duplication in the deny list.
Patterns scan every string value in the tree, including string values inside Error messages and stack traces:
eyJ...JWT (three base64url segments)AKIA...AWS access keys,ASIA...AWS STS tokenssk_live_...,sk_test_...,pk_live_...,rk_...Stripe keysghp_...,gho_...,ghu_...,ghs_...,ghr_...GitHub classic tokensgithub_pat_...GitHub fine grained PATsxox[a-z]-...Slack tokens-----BEGIN ... PRIVATE KEY-----PEM blocksBearer <token>in free textprotocol://user:password@hostdatabase connection strings- 13 to 19 digit numbers that pass the Luhn checksum (credit card numbers)
The Luhn gate keeps the credit card pattern from redacting innocent digit runs (order ids, invoice numbers).
const logger = createLogger({
level: 'info', // 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent'
transport: 'json', // 'json' | 'pretty' | (line, record) => void
redact: { extraPhrases: [['hmac']] },
base: { service: 'api' },
})
logger.info('msg')
logger.info({ obj: 1 }, 'msg')
logger.error(error)
logger.error(error, 'context')
const child = logger.child({ requestId })
logger.isLevelEnabled('debug')The logger is generic on the binding shape. The base option's shape threads through every record and every child:
const logger = createLogger({ base: { service: 'api', region: 'sg' } })
// logger: Logger<{ service: string; region: string }>
const child = logger.child({ requestId: 'r1' })
// child: Logger<{ service: string; region: string; requestId: string }>
const transport = (line: string, record: LogRecord<{ service: string }>) => {
// record.service is typed string, no cast needed
}The standalone redactor for ad hoc use outside the logger. Useful before sending payloads to third party error reporters or analytics services.
import { redact } from 'autoredact'
sentry.captureException(error, { extra: redact(payload) })All fields are optional. Defaults cover the security baseline.
type RedactOptions = {
// Add to the default phrase list. The recommended way to extend.
// Default: []
extraPhrases?: ReadonlyArray<ReadonlyArray<string>>
// Replace the default phrase list entirely. Advanced.
// Default: DEFAULT_PHRASES
phrases?: ReadonlyArray<ReadonlyArray<string>>
// Path allowlist. Values at these paths pass through untouched.
// Default: []
allow?: ReadonlyArray<string>
// Replacement string for redacted values.
// Default: '[REDACTED]'
censor?: string
// Whether to scan string values for credential shapes.
// Default: true
valueShapes?: boolean
// Add to the default value shape regex set. Each entry is coerced to
// carry the `g` flag automatically.
// Default: []
extraValuePatterns?: ReadonlyArray<RegExp>
// Maximum object or array nesting depth before emitting a marker.
// Default: 8
maxDepth?: number
// Maximum string length before truncation.
// Default: 8192
maxStringLen?: number
// Optional callback invoked once per redaction event. Receives the path,
// kind, matched substring, and reason. Useful for metrics and alarms.
// Default: undefined
onLeak?: (info: LeakInfo) => void
}
type LeakInfo = {
path: string // 'user.api_key', 'env[3].token'
kind: 'key' | 'value' // key match or string content match
matched: string // the key name or the matched substring
reason: 'phrase' | 'pattern' | 'luhn' // which detector fired
}createLogger accepts the same callback at the top level so the logger forwards every redaction it performs:
const logger = createLogger({
base: { service: 'api' },
onLeak: (info) => metrics.increment('autoredact.leak', { reason: info.reason }),
})A child logger inherits its parent's onLeak automatically.
const logger = createLogger({
redact: { extraPhrases: [['internal', 'id'], ['hmac']] },
})
logger.info({ internalId: 'abc', hmac: 'xyz' })
// internalId and hmac both redactUseful when one specific field at a known path is safe to log even though its key name would otherwise match.
const logger = createLogger({
redact: { allow: ['user.email', 'request.session.id'] },
})const logger = createLogger({
redact: { censor: '***' },
})const logger = createLogger({
redact: {
extraValuePatterns: [/internal-id-[a-f0-9]{16}/i],
},
})const logger = createLogger({
transport: (line, record) => {
fetch('https://logs.example.com/ingest', { method: 'POST', body: line })
},
})Wire onLeak to your metrics or alarm system to surface what would otherwise be silent. Useful both as a tripwire (a Stripe key fires a redaction, you want to know which call site put it there) and as a long term signal of where sanitisation is missing upstream.
const logger = createLogger({
onLeak: (info) => {
metrics.increment('autoredact.leak', { reason: info.reason, kind: info.kind })
if (info.reason === 'pattern') {
alarm.send(`credential shape redacted at ${info.path}`)
}
},
})The same callback is available on redact() directly:
import { redact } from 'autoredact'
const safe = redact(payload, {
onLeak: (info) => metrics.increment('autoredact.leak', { reason: info.reason }),
})import { redact } from 'autoredact'
const safePayload = redact({ user, requestBody, headers })
sentry.captureException(error, { extra: safePayload })autoredact-scan audits log files offline for leaks the application missed. Useful as a CI gate against historical files or as a periodic scan against a centralized log store.
npx autoredact-scan log.jsonl
npx autoredact-scan logs/*.jsonl
cat app.log | npx autoredact-scan
npx autoredact-scan --json log.jsonl > out.json
npx autoredact-scan --strict log.jsonl # exit 1 if leaks found, for CIRun autoredact-scan --help for the full flag list.
The walker is hardened against the inputs that crash naive implementations. Logging never crashes the calling app:
- Cycles emit
[CIRCULAR]and recursion stops. Shared object references that are not cycles still walk normally. - Depth caps at
maxDepth(default 8) and emits[TRUNCATED:DEPTH]. - String length caps at
maxStringLen(default 8192) with a[TRUNCATED]suffix appended after scrubbing, so a credential straddling the cap does not leak a partial. - Throwing getters substitute
[GETTER_THREW]rather than crashing. - Walker errors degrade to a single
_redact_errorfield on the record. - Transport errors log once via
console.errorand silence for the lifetime of the logger.
MIT License © 2026-present Cong Nguyen