Skip to content

redact process stdout/stderr writes in-process (opt-in) - #1181

Merged
theoephraim merged 25 commits into
mainfrom
redact-process-streams
Oct 7, 2026
Merged

theoephraim merged 25 commits into
mainfrom
redact-process-streams

Conversation

@theoephraim

@theoephraim theoephraim commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Fixes #1149. varlock/auto-load only redacted console output, so anything writing to process.stdout/process.stderr directly (pino, CLI frameworks, child.stdout.pipe(process.stdout), Bun.write(Bun.stdout)) leaked secrets unless the app ran under varlock run.

What changed

  • @redactLogs renamed to @redact, with an options object: @redact={console=true, stdout=true}. The decorator now covers stdout data and varlock run output, not just console logs, so the old name was misleading. @redactLogs keeps working as a deprecated alias (loads with a warning; using both is an error). The blob carries it as settings.redact = { console, stdout? }, mirroring the decorator; settings.redactLogs is still written as a mirror of redact.console for runtimes older than 1.22, and new runtimes fall back to it when a blob from an older CLI has no redact key. Alias and mirror both go away in the next major. stdout=true patches process.stdout.write / process.stderr.write (and Bun.write to them), redacting streams that are not a TTY, the same rule varlock run uses. One setting now controls both in-process and varlock run redaction; @redact=false still turns everything off, and @redact={stdout=false} turns off only stream redaction (for a process whose stdout is a data channel). console is separate because console redaction also applies on an interactive terminal, so {console=false, stdout=true} shows values in your own terminal while redacting anything piped.
  • Opt-in for now. In-process stdout redaction only turns on with stdout=true or _VARLOCK_REDACT_STDOUT=1. varlock run keeps redacting by default. The plan is to make in-process redaction the default in the next major, since it can change output that is data rather than logs ($(node print-token.js), generators writing to a file).
  • @sensitive={redact=false} exempts a value that is meant to be printed from console/stdout redaction and redactSensitiveConfig(), while keeping it sensitive: it is still leak-scanned, kept out of client bundles, and still scrubbed from responses and Next.js build output when a leak is caught (redactAllSensitiveValues).
  • Where it is applied: patchProcessStreams is exported from varlock/patch-console and called next to patchGlobalConsole by auto-load, init-server, and the Vite, Next.js and Expo integrations. It is deliberately not called from patchGlobalConsole itself, so the edge bundles (init-edge, the Cloudflare init) do not carry node-only stream code.
  • Stream patch details: each write (and end()'s final chunk) is redacted and passed straight to the original write with the same arguments, callback and return value. Nothing is held back, so stream behavior is unchanged. A value split across separate writes (including mixed process.stdout.write / Bun.write) is caught when its second part arrives: that part is masked and a one-time warning names the item, since the first part was already printed. Binary buffers and non-UTF-8 encodings pass through. varlock run still holds back partial matches, and tags its child with __VARLOCK_REDACTED_STREAMS, so its direct child skips streams the parent already redacts.
  • No double redaction on Node: the console hook skips its own pass when the target stream is already patched.
  • Faster redaction for all output, including varlock run and response scanning:
    • varlock run's holdback never cuts through a complete value (a value like secret-token-s used to print in two unredacted pieces);
    • when varlock run flushes a held partial match after its 100ms timeout, it now remembers it, so if the child pauses mid-value the rest is masked when it arrives (with a one-time warning) instead of the whole value printing;
    • the partial-match (holdback) check is indexed instead of scanning every suffix (~3.5µs to ~0.05µs per log line on Node);
    • the redaction regex no longer wraps the secret list in optional unmask-marker groups, which made every scan 2-5x slower.
  • revealSensitiveConfig() fix: it printed its 👁 markers around the value and did not work on Bun. The layer that writes output last now strips them.
  • Benchmark: bun run bench:redaction (manual, not in CI) measures the cost with a realistic 10-secret schema under Node and Bun.

Cost

With realistic output (secrets almost never present), in-process stdout redaction adds roughly 0.5-1µs per log line on Node and Bun, and processes bulk output at roughly 0.4-1GB/s. See packages/varlock/scripts/bench-stdout-redaction.ts.

Scope

The secrets guide gains a Scope and known limitations section and SECURITY.md a matching scope list: redaction and leak prevention are guardrails against accidental exposure by trusted code, not a sandbox, and the known limitations (uncovered write paths, transformed values, TTY output, split values, overlapping values, encodings, opt-outs, the visible two-character prefix, what leak prevention does not scan) are written down so they are triaged as issues rather than advisories.

Not covered in-process

Writes that bypass the stream objects (fs.writeSync(1, ...), pino.destination(), worker-thread transports) are only redacted under varlock run.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
varlock-website a33978e Commit Preview URL

Branch Preview URL
Oct 07 2026, 10:46 PM

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Held output can be lost when a patched stream is ended before the holdback timer flushes.

Reviewed changes across the in-process stdout/stderr redaction path, its schema and serialized settings, parent-child handoff, shared redactor optimizations, reveal handling, tests, docs, and benchmark tooling.

  • Stream interception: Adds opt-in patching for Node process streams and Bun writes, including UTF-8 handling and split-secret holdback.
  • Configuration: Adds independent console and stdout controls plus per-item log-redaction exemptions.
  • Parent handoff: Marks streams already redacted by varlock run or varlock proxy run to avoid duplicate processing.
  • Shared redaction: Optimizes regex and holdback lookup behavior and moves unmask-marker removal to the final output layer.
  • Coverage and guidance: Adds unit and smoke tests, reference documentation, a changeset, and a manual benchmark.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-5.6-sol | 𝕏

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle size

⚠️ grows the bundle by 168.1 KB (+3.6%)

Metric main This PR Δ
Total dist 4726.4 KB 4894.5 KB +168.1 KB (+3.6%)
JS 1786.0 KB 1856.7 KB +70.7 KB (+4.0%)
Sourcemaps 2823.1 KB 2911.7 KB +88.6 KB (+3.1%)
Type defs 117.3 KB 126.1 KB +8.7 KB (+7.5%)
Other 0.0 KB 0.0 KB —

dist/ only; native binaries are versioned separately and not counted here.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

This update can leave leak-detected secrets in client-facing Next.js artifacts.

Reviewed changes since the prior Pullfrog review: the per-item log-redaction exemption was extended to the public redaction helper while leak-prevention scrubbing received a separate all-values path.

  • Redaction semantics: Changed redactSensitiveConfig() to honor @sensitive={redactLogs=false} and added redactAllSensitiveValues() for contexts that must scrub every leak-scanned value.
  • Response protection: Updated patched server responses to use all-values scrubbing and added coverage for log-exempt secrets.
  • Documentation: Clarified that redactLogs=false also affects redactSensitiveConfig() but not response leak detection.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-5.6-sol | 𝕏

Comment thread packages/varlock/src/runtime/env.ts Resolved
@pkg-pr-new

pkg-pr-new Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@varlock/ci-env-info

npm i https://pkg.pr.new/@varlock/ci-env-info@1181

varlock

npm i https://pkg.pr.new/varlock@1181

@varlock/astro-integration

npm i https://pkg.pr.new/@varlock/astro-integration@1181

@varlock/cloudflare-integration

npm i https://pkg.pr.new/@varlock/cloudflare-integration@1181

@varlock/nextjs-integration

npm i https://pkg.pr.new/@varlock/nextjs-integration@1181

@varlock/nuxt-integration

npm i https://pkg.pr.new/@varlock/nuxt-integration@1181

@varlock/vite-integration

npm i https://pkg.pr.new/@varlock/vite-integration@1181

@varlock/1password-plugin

npm i https://pkg.pr.new/@varlock/1password-plugin@1181

@varlock/aws-secrets-plugin

npm i https://pkg.pr.new/@varlock/aws-secrets-plugin@1181

@varlock/aws-sigv4-plugin

npm i https://pkg.pr.new/@varlock/aws-sigv4-plugin@1181

@varlock/azure-key-vault-plugin

npm i https://pkg.pr.new/@varlock/azure-key-vault-plugin@1181

@varlock/bitwarden-plugin

npm i https://pkg.pr.new/@varlock/bitwarden-plugin@1181

@varlock/dashlane-plugin

npm i https://pkg.pr.new/@varlock/dashlane-plugin@1181

@varlock/doppler-plugin

npm i https://pkg.pr.new/@varlock/doppler-plugin@1181

@varlock/google-secret-manager-plugin

npm i https://pkg.pr.new/@varlock/google-secret-manager-plugin@1181

@varlock/hashicorp-vault-plugin

npm i https://pkg.pr.new/@varlock/hashicorp-vault-plugin@1181

@varlock/infisical-plugin

npm i https://pkg.pr.new/@varlock/infisical-plugin@1181

@varlock/keepass-plugin

npm i https://pkg.pr.new/@varlock/keepass-plugin@1181

@varlock/keeper-plugin

npm i https://pkg.pr.new/@varlock/keeper-plugin@1181

@varlock/kubernetes-plugin

npm i https://pkg.pr.new/@varlock/kubernetes-plugin@1181

@varlock/pass-plugin

npm i https://pkg.pr.new/@varlock/pass-plugin@1181

@varlock/passbolt-plugin

npm i https://pkg.pr.new/@varlock/passbolt-plugin@1181

@varlock/proton-pass-plugin

npm i https://pkg.pr.new/@varlock/proton-pass-plugin@1181

@varlock/native-helper-darwin

npm i https://pkg.pr.new/@varlock/native-helper-darwin@1181

@varlock/native-helper-linux-arm64

npm i https://pkg.pr.new/@varlock/native-helper-linux-arm64@1181

@varlock/native-helper-linux-x64

npm i https://pkg.pr.new/@varlock/native-helper-linux-x64@1181

@varlock/native-helper-win32-x64

npm i https://pkg.pr.new/@varlock/native-helper-win32-x64@1181

commit: a33978e

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

bumpy-frog

The changes in this PR will be included in the next version bump.

minor Minor releases

  • @varlock/native-helper-darwin 1.21.1 → 1.22.0
  • @varlock/native-helper-linux-arm64 1.21.1 → 1.22.0
  • @varlock/native-helper-linux-x64 1.21.1 → 1.22.0
  • @varlock/native-helper-win32-x64 1.21.1 → 1.22.0
  • varlock 1.21.1 → 1.22.0

patch Patch releases

  • @varlock/nextjs-integration 1.2.3 → 1.2.4

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@theoephraim

Copy link
Copy Markdown
Member Author

@pullfrog - review this

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

The new stream protection still leaks complete secrets in two reproducible cases. Pending write errors and framework initialization also need attention before merging.

Reviewed changes across the process-stream patch, configuration serialization, console hooks, leak remediation, documentation, benchmarks, and tests.

  • Process-stream redaction: Opt-in stdout/stderr patching, UTF-8 buffering, callbacks, final flushing, Bun writes, and parent-redaction markers.
  • Configuration: Independent console/stdout settings and per-item log-redaction exemptions, while retaining leak detection.
  • Output handling: Indexed holdback matching, faster replacement, reveal-marker handling, and Next.js artifact scrubbing.
  • Verification: The package build and 128 focused tests passed; separate reproductions exposed the inline findings. Smoke tests could not run because the standalone smoke package lacks installed Vitest (vitest: command not found).

⚠️ Vite SSR initialization never enables the new protection

The advertised framework support is incomplete: Vite's init-only and resolved-env SSR initialization calls the existing patches but never patchProcessStreams(). Setting @redactLogs={stdout=true} therefore leaves direct stream writes unprotected on these paths, including Astro configurations that select resolved-env.

Technical details
# Missing framework stream initialization

## Affected sites
- `packages/integrations/vite/src/index.ts:344`: `init-only` is the default SSR injection mode.
- `packages/integrations/vite/src/index.ts:466-493`: generated non-auto-load initialization imports/calls console and response patches, but no process-stream patch.
- `packages/integrations/astro/src/index.ts:128,146`: configurations selecting `resolved-env` use this generated path.

## Required outcome
- Non-edge SSR initialization must enable process-stream protection when the schema or environment opts in, without requiring an additional application import.
- Keep edge initialization free of Node-only stream dependencies.
- Cover the generated initialization path with a test that verifies direct stdout output, not just console output.

## Suggested approach
- Wire the stream patch into Vite's non-edge initialization with a lightweight published runtime entry point, consistent with its existing patch imports.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Mixed Bun and process-stream writes can report callback success before an earlier write fails.

Reviewed changes since 7f4412f3, focusing on the shared streaming state, callback error handling, and framework initialization.

  • Preserved complete matches: Added boundary selection that avoids cutting complete sensitive values, including self-overlap, in both stream writers.
  • Shared Bun holdback: Routed Bun text writes through the process-stream holdback state and extended split-write smoke scenarios.
  • Observed prefix errors: Added error callbacks for emitted prefixes and real-Writable tests for both auto-destroy modes.
  • Unified initialization: Moved process-stream patching into patchGlobalConsole() so existing framework initialization enables it, without a static filesystem import.

The package build and 52 focused tests passed. Built-package Bun split-write checks also passed; the smoke suite could not load because vitest/config is missing from its local dependency resolution.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new write route fixes the earlier callback-transfer bug, but introduces two error-handling regressions that should be addressed before merging.

Reviewed changes since 80837bad, focusing on Bun text routing and the revised acknowledgement of held writes.

  • Routed Bun text writes through the process-stream writer instead of an independent Bun promise.
  • Changed write acknowledgement to complete after the emitted portion, allowing sequential awaited writes to retain split-secret protection.
  • Added regression coverage for mixed-API errors and sequential split writes, and updated held-write callback assertions.

The package build and all 54 focused tests passed. Separate Node and real-Bun reproductions exposed the inline findings.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Adds @redactLogs={console, stdout} options. stdout=true patches
process.stdout/stderr.write (and Bun.write to them) in auto-load and
init-server, redacting non-TTY streams like varlock run does. Opt-in for
now via the schema or _VARLOCK_REDACT_STDOUT=1.

Also adds @sensitive={redactLogs=false} to exempt a value from output
redaction, skips the in-process patch on streams a varlock run parent
already redacts, and makes the streaming holdback check much faster.

Fixes #1149
bench:redaction measures per-write CPU cost and end-to-end cost under node
and bun for no redaction, console only, in-process stdout, and varlock run,
with a realistic 10-secret schema generated at run time.

The holdback check now only tries suffix lengths whose end char can match,
which cuts it from ~3.5us to ~0.05us per log line under node.
Node's console writes through process.stdout/stderr, so with the stream
patch active the kWriteToConsole hook redacted every line twice. Also make
the benchmark realistic: secrets appear in one line per run by default,
with --secret-every N for a worst case.
Optional unmask-marker groups around the secret alternation made every scan
2-5x slower; check the markers in the replace fn instead, and wrap the
alternation in a single group (faster in JSC). Also pass a write callback
straight through when nothing is held back.
The markers were never stripped, so revealed values printed as "👁 value 👁",
and on bun the value was not marked at all (and got redacted). The layer
that writes output last now strips the markers: the stream patch, node's
console internals, or the console method wrapper where nothing runs after it.
- accept "true"/"false" strings for dynamic @redactLogs values (env
  overrides resolve to strings), instead of failing the load
- tag the parent redaction marker with varlock run's pid, so a grandchild
  piping elsewhere (node app | tee log) redacts its own output
- keep redactLogs=false values in response scrubbing and the public
  redactSensitiveConfig; only log/stdout redaction skips them
- an opt-out on one item no longer weakens a value it shares with another
- hold back a trailing unmask marker so split revealed values print
- drop settings that disappear on env reload
The public redact fn now leaves log-exempt values alone; only leak
prevention's response scrubbing still redacts them.
…facts

- the stream patch now writes held-back text before end(), so it is not
  lost or written after the stream closes
- Next.js leak remediation for built files uses all-values redaction, so a
  redactLogs=false value caught by the leak scan is not written back
- remove a stray file committed by accident
…reams from patchGlobalConsole

- the stream holdback never cuts through a complete value; a value whose
  ending is also the start of a value (e.g. secret-token-s) was emitted in
  two unredacted pieces (in-process and in varlock run)
- Bun.write to stdout/stderr shares holdback state with process.stdout.write,
  so values split across writes of either kind are caught
- a write error on already-emitted text reaches callbacks waiting on held text
- patchGlobalConsole() also patches the streams, so integrations that only
  call it (vite/astro SSR init) get stdout redaction; the stream patch no
  longer imports node:fs (uses process.getBuiltinModule) so it is edge-safe
Bun.write text now goes through the patched process stream, so it shares the
write queue (ordering and errors) as well as the holdback state.

Write callbacks fire once the writable part is out instead of waiting on
held-back text. Waiting made writers that await each write (e.g. awaited
Bun.write calls) force every held prefix out by timeout, unredacted, before
the rest of the value arrived. Held text still goes out on the next write,
end(), the flush timer, or process exit.
@theoephraim
theoephraim force-pushed the redact-process-streams branch from 74c5148 to c57e6fd Compare October 6, 2026 22:42
- Bun.write shares holdback state with the process stream but writes with
  Bun's original writer again, so a failure (e.g. EPIPE) only rejects the
  promise instead of also emitting an unhandled stream error
- writes to an ended, destroyed or errored stream pass straight to the
  original write, so they report ERR_STREAM_WRITE_AFTER_END etc. instead of
  being buffered and acknowledged

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

No new issues found, but the existing Bun failure-handling concern is only partially addressed: a caught failed write with a held suffix still causes an uncaught EPIPE during the later flush. Remaining prior feedback prevents approval.

Reviewed changes since the prior substantive Pullfrog review, including the mechanical intermediate update and the latest writer/error-handling fixes.

  • Restored Bun's original writer while retaining shared split-secret holdback with process-stream writes.
  • Delegated closed process-stream writes to the original writer before buffering, preserving ended/destroyed errors.
  • Added regression coverage for Bun promise-only errors and writes after stream closure.

The package build and all 56 focused tests passed. Real-Bun closed-pipe checks confirmed that plain-text promise rejection is fixed, but the existing held-tail failure persists. The closed-process-stream thread was resolved.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

The in-process stream patch no longer buffers partial matches between
writes. Each write (and end()'s final chunk) is redacted and passed straight
to the original write with the same arguments, callback and return value,
so stream semantics are untouched by construction.

A value split across writes is still detected: the stream remembers the end
of the previous write that could start a value, masks the part that arrives
in the next write, and prints a one-time warning naming the item, since the
first part was already written. varlock run keeps its holdback buffering.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

The new buffer handling can emit a complete secret unchanged when a write boundary falls inside one of its UTF-8 characters.

Reviewed changes since 8518662f, focusing on the no-holdback rewrite and its updated output contract.

  • Replaced delayed output buffering with immediate redaction that forwards each call to the original writer, including end() final chunks.
  • Added split-value detection using shared carry across process-stream and Bun writes, masking the later portion and issuing a one-time warning.
  • Revised UTF-8 handling and coverage to pass boundary bytes through immediately, and updated tests and documentation for partial exposure.

The package build and all 54 focused tests passed. Separate source and built-package reproductions confirmed the inline leak; the earlier Bun and callback error threads were resolved.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment on lines +114 to +118
const start = leadingContinuationBytes(bytes);
const end = bytes.length - incompleteUtf8TailLength(bytes.subarray(start));
let text: string;
try {
text = utf8Decoder.decode(bytes.subarray(start, end));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] A character split across buffers is excluded from both decoded chunks, so a sensitive value containing that character never matches even though all its bytes are printed. Registering super-sécret-value-12345 and splitting its buffer between é's C3 and A9 bytes emits the entire secret unchanged, with no split warning, rather than masking the later portion.

Technical details
# Split UTF-8 characters bypass carry detection

## Affected sites
- `packages/varlock/src/runtime/patch-process-streams.ts:114-125`: excludes incomplete trailing bytes and leading continuation bytes from decoded matching text, then forwards both buffers unchanged when no match is found.
- `packages/varlock/src/runtime/env.ts:533-545`: searches `carry + text`, which is missing the split character.
- `packages/varlock/src/runtime/test/patch-process-streams.test.ts:132-137`: splits a multibyte character outside the secret, leaving the ASCII secret complete in the second buffer.

## Reproduction
- Register sensitive value `super-sécret-value-12345` and patch a recording stream.
- Create `Buffer.from(secret + '\n')` and split at `bytes.indexOf(0xC3) + 1`.
- Write each slice and concatenate the emitted buffers.
- Actual output is the complete secret followed by a newline, with no warning. Matching instead sees `super-scret-value-12345`.
- Confirmed against source under Bun and the built package under Node. A boundary immediately before `é` is correctly detected.

## Required outcome
- Carry detection must include multibyte characters completed across buffer boundaries so the later portion is masked and the warning is emitted.
- Preserve immediate output and native callback/error behavior without restoring delayed flushes.
- Add regression coverage with the boundary inside a multibyte character that belongs to the registered sensitive value.

… output

When a chunk ends with what may be the start of a sensitive value, varlock
run holds it back for up to 100ms. After that it was written as-is and
forgotten, so if the child paused mid-value (e.g. block-buffered piped
output) the rest arrived unrecognized and the whole value was printed.

The writer now remembers the flushed fragment; when the next chunk completes
a value across it, that part is masked and a one-time warning names the
item. The held text is still flushed on time, so prompts are not stalled,
and a flushed lookalike that never completes a value is left alone.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

No new issues found in this delta. The existing split UTF-8 secret leak remains reproducible and unresolved, so approval is still blocked.

Reviewed changes since 716010cb, focusing on CLI redaction after timeout flushes.

  • Added CLI carry tracking so varlock run masks the remainder of a value whose prefix was already printed after the 100ms timeout, with a one-time warning.
  • Extracted shared matching helpers for split-value completion and trailing partial-value carry without changing the in-process output contract.
  • Added timeout regression coverage for delayed completions, multiple pauses, and lookalike prefixes, and documented the behavior in the secrets guide.

All 57 focused tests passed. An additional exact-output check passed all 253 three-chunk flush partitions of the test secret. Build and smoke tests were not rerun for this delta.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

… stream code out of edge bundles

- Remove @sensitive={redactLogs=false} for now (and with it the second
  redaction regex, the module-level all-values toggle, redactAllSensitiveValues
  and the Next.js leak-scrub shim). It only matters once in-process stdout
  redaction is the default, so it will ship with that change.
- @redactLogs takes only {stdout=...}; console redaction is off only via
  @redactLogs=false. On Node stdout=true already covers console output.
- patchGlobalConsole no longer patches the process streams. patchProcessStreams
  is exported from varlock/patch-console and called by auto-load, init-server
  and the node integrations, so init-edge and the Cloudflare init stay flat.
- Move the streaming helpers (redactStreamWrite, getStreamHoldbackLength, ...)
  out of env.ts into runtime/lib/redact-stream.ts.
- Holdback index uses two Maps instead of parallel ascii-array/Map buckets.
- Benchmark uses Math.random (closes the CodeQL biased-random alert).
- Docs: note the one split case in-process redaction cannot detect (a
  multi-byte character split across byte writes inside a sensitive value).

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

No new issues found in this delta. The existing split UTF-8 secret leak is now documented but remains reproducible, so that thread remains open and approval is still blocked.

Reviewed changes since the prior substantive review at dd657f77, including the documentation-only update at 19b53452 and the latest scope reduction.

  • Removed per-item log exemptions and the separate all-values scrubbing helper, restoring one redactor for logs and leak remediation.
  • Simplified configuration to the stdout option, with console redaction disabled only by @redactLogs=false.
  • Separated stream initialization from patchGlobalConsole() and added explicit calls in Node entry points and framework integrations, including generated non-edge SSR code.
  • Consolidated streaming helpers and simplified the holdback index to Map-based buckets.
  • Documented split-write limitations, exposure guidance, and process-level stdout opt-outs.

Runtime and modified integration builds passed, along with 210 focused tests. Edge-bundle checks found no new runtime failure. Smoke tests and the website build were not rerun.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

…s=false}

The console option is not redundant with stdout: stream redaction skips
interactive terminals, so console redaction is the only thing hiding values
from a human at the keyboard. {console=false, stdout=true} is a coherent
setting (see secrets in your terminal, never in piped output).

The per-item opt-out keeps an item sensitive (leak-scanned, kept out of
client bundles) while letting a program print it. Leak remediation scrubs
these values too via redactAllSensitiveValues. Reimplemented by passing the
active find/replace down through the redaction walk instead of a
module-level toggle.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Restoring per-item log exemptions introduces a reproducible leak of a different, non-exempt secret across in-process writes. The existing split UTF-8 leak also remains unresolved.

Reviewed changes since 34827597, focusing on the restored configuration options and separation of output redaction from leak remediation.

  • Restored independent console controls alongside the opt-in stdout/stderr setting.
  • Restored per-item log exemptions in schema validation, serialization, and recursive output redaction, while retaining leak scanning.
  • Restored all-values scrubbing for server responses and Next.js build artifacts, including older-peer fallback.
  • Extended tests and documentation for exempt values, configuration independence, and intentional output.

Runtime and Next.js integration builds passed, along with 179 focused tests. Independent source and built-runtime reproductions confirmed the new inline finding. Smoke tests and the website build were not rerun.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment thread packages/varlock/src/runtime/env.ts Resolved
The decorator now covers stdout data and varlock run output, not just
console logs, and will cover everything a process emits once stream
redaction becomes the default. @redact is the name for that.

- @redactLogs still works and warns; using both is an error.
- @sensitive={redact=false} replaces redactLogs=false on items, with the
  old option name accepted as a deprecated alias.
- The serialized settings key stays redactLogs so blobs stay readable
  across CLI/runtime versions.
- Docs, CLI help, VSCode catalog and the smoke test use the new name.
@theoephraim theoephraim changed the title Redact process stdout/stderr writes in-process (opt-in) Rename @redactLogs to @redact and redact process stdout/stderr writes in-process (opt-in) Oct 7, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

No new issues found in this delta. The existing overlapping-value leak and split UTF-8 leak remain reproducible, so approval is still blocked.

Reviewed changes since ec2b186e, focusing on the decorator rename and deprecated aliases.

  • Renamed the root decorator to @redact, retaining @redactLogs with a warning and rejecting schemas that use both names.
  • Renamed the per-item output option and serialized item metadata to redact, retaining redactLogs as a deprecated schema option while preserving the shipped settings.redactLogs root key.
  • Updated documentation and tooling across CLI help, VS Code completion, benchmarks, fixtures, and regression tests to use the new names.

Runtime, Next.js integration, and website builds passed, along with 168 focused tests. Source reproductions confirmed both existing leak findings; smoke tests were not rerun.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Mirrors the decorator one to one and folds in the never-shipped
redactStdout key. settings.redactLogs is still written as a mirror of
redact.console for runtimes older than 1.22, and the runtime falls back
to it when a blob from an older CLI has no redact key. Both go away in
the next major with the @redactLogs alias.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

No new issues found in this delta. The existing overlapping-value leak and split UTF-8 leak remain reproducible, so approval is still blocked.

Reviewed changes since 7affeb71, focusing on root-setting serialization and its consumers.

  • Serialized root redaction options as settings.redact = { console, stdout? }, preserving the distinct defaults when stdout is omitted.
  • Retained older-runtime compatibility through the boolean settings.redactLogs mirror and a runtime fallback for older CLI blobs.
  • Updated console, process-stream, and CLI consumers to read the new object, and adjusted schema, stream, and telemetry assertions.

The runtime build and all 143 focused tests passed. Independent checks verified legacy fallback, new-setting precedence, stale-setting cleanup on reload, and both existing leak cases. Smoke tests and website/integration builds were not rerun.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

The completion and boundary-crossing checks walked the redaction regex,
whose exec() only yields non-overlapping matches: a complete value inside
already-written text hid an overlapping occurrence extending into the
current write. With values aabaa (protected) and aabaab (log-exempt),
writing aabaabaa then baa printed the protected value intact. The same
happens with two protected values, so this was not specific to the
exemption.

Both checks now test each candidate start position directly (bounded by
the longest value). varlock run's holdback also now holds a value that
would be cut by the boundary back whole, instead of emitting it early:
an occurrence overlapping an emitted one could otherwise be completed by
the next chunk unnoticed.

Benchmark uses crypto.randomInt, which is unbiased and keeps CodeQL
quiet about secret-looking variable names.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The revised CLI holdback can retain continuously arriving overlapping output without limit. The existing split UTF-8 leak also remains reproducible and unresolved.

Reviewed changes since 4ac44aa8, focusing on overlapping-value matching and CLI emission boundaries.

  • Replaced regex traversal with candidate-start checks that detect overlapping protected values, including overlaps with exempt values.
  • Changed CLI holdback to retain complete values crossed by an emission boundary instead of emitting them early.
  • Added overlap regressions for in-process and CLI writers, and switched benchmark value generation to crypto.randomInt.

The runtime build and all 71 focused tests passed. Independent reproductions verified the overlap fix, the remaining UTF-8 leak, and the new holdback issue. Smoke tests and website/integration builds were not rerun.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Random overlapping secrets over a two-letter alphabet, random chunking,
fixed seeds. The varlock run writer must reproduce single-shot redaction
exactly when chunks arrive back to back, and must never emit a protected
value when its flush timeout fires between chunks. The in-process patch
must never emit a protected value for any split. Re-introducing either of
the two bugs fixed in the previous commit fails these tests.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

No new issues found in this delta. The existing unbounded CLI holdback and split UTF-8 leak remain reproducible and unresolved, so approval is still blocked.

Reviewed changes since d525334f, focusing on the new randomized streaming regression coverage. No runtime code changed.

  • Added single-shot equivalence checks for back-to-back CLI chunks and whole-write in-process output.
  • Added split-output leak checks for CLI timeout flushes and immediate in-process writes, using fixed seeds, overlapping ASCII values, and output exemptions across 12,000 randomized rounds.

All 60 focused tests passed, including the four new fuzz tests, and lint of the new file passed. Independent source checks confirmed both existing findings. Builds and smoke tests were not rerun for this tests-only delta.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Still fails on both re-introduced bugs; runs in about 130ms.
@theoephraim theoephraim changed the title Rename @redactLogs to @redact and redact process stdout/stderr writes in-process (opt-in) redact process stdout/stderr writes in-process (opt-in) Oct 7, 2026
Holding a value that would be cut by the holdback boundary back whole
means an overlap chain (secret-token-s, ecret-token-s, ecret-token-s, ...)
can never be cut without splitting a value. With the flush timer reset on
every write, continuous output of that shape was buffered until the child
went quiet. The timer is now a deadline from the first held byte, so held
output is bounded by one interval of output and the whole chain goes
through single-shot redaction, which masks overlapping occurrences
correctly.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

No new issues found in this delta. The existing split UTF-8 leak remains reproducible and unresolved, so approval is still blocked.

Reviewed changes since the previously reviewed version at 926d1b36, focusing on sustained CLI output and flush timing.

  • Changed CLI flushing from a per-write debounce to a deadline from the first held text, allowing continuous overlapping output to progress.
  • Added timer regressions for sustained overlap chains and writes that arrive before the original deadline.

The runtime build and all 62 focused tests passed. An independent real-time reproduction confirmed output progress without a complete-value leak. Smoke tests and website/integration builds were not rerun.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

A Buffer write that ends mid-character left the dangling bytes out of the
text used for matching on both sides, so a sensitive value containing an
accented letter or emoji, split exactly inside that character, printed
whole with no warning. The incomplete sequence is now kept, completed by
the next write, and fed to the matcher as already-written text (like the
carry), so the rest of the value is masked and the one-time warning
fires. Output bytes are unchanged. Drops the doc caveat for this case.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

No new issues found, but the existing split UTF-8 leak is only partially addressed. Splitting super-secret-value-12345é between the final character's C3 and A9 bytes still emits the complete secret unchanged, without a warning, so that thread remains open and approval is blocked.

Reviewed changes since e4758c6e, focusing on UTF-8 reconstruction across immediate byte writes.

  • Added pending-byte reconstruction so characters completed by later writes participate in split-value matching without delaying output.
  • Added exact-output regressions for splits inside non-final two-byte and three-byte characters, including a character spread over three writes.
  • Removed the documented multibyte limitation from the secrets guide.

The runtime build and all 67 focused tests passed. Independent source partition checks and built-export Node reproductions confirmed the original internal-character fix and the remaining final-character leak: the completed character is added to already-written carry, but matching requires the value to extend beyond that boundary. Smoke tests and website/integration builds were not rerun.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

A guide section and a SECURITY.md scope list stating what redaction and
leak prevention are (guardrails against accidental exposure by trusted
code) and are not (a sandbox), with the known limitations spelled out:
uncovered write paths, transformed values, TTY output, split values,
overlapping values, encodings, opt-outs, the visible two-char prefix, and
what leak prevention does not scan. Reports that reduce to one of these
are handled as issues, not advisories.
@theoephraim
theoephraim merged commit fca7c11 into main Oct 7, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: auto-load redacts console output but not process.stdout/stderr writes

2 participants