Stop deleting runtime env vars when booting from a build-baked snapshot - #1053
Stop deleting runtime env vars when booting from a build-baked snapshot#1053theoephraim wants to merge 13 commits into
Conversation
|
The changes in this PR will be included in the next version bump.
|
📦 Bundle size
dist/ only; native binaries are versioned separately and not counted here. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
varlock-website | 277627e | Commit Preview URL Branch Preview URL |
Sep 01 2026, 03:49 AM |
There was a problem hiding this comment.
Important
The build provenance marker can outlive the blob it describes, so fresh runtime resolutions can be rejected as if they were baked snapshots.
Reviewed changes Reviewed the build-baked snapshot guard across the shared runtime, Next.js and Vite injection paths, standalone framework scenarios, and deployment documentation.
- Conflict guard Detects ambient schema values that differ from a marked build snapshot, with an explicit warning-only escape hatch.
- Injection provenance Marks Next.js webpack, Turbopack, and Vite
resolved-envfallbacks and mirrors provenance to child processes. - Fresh Next resolution Makes
@next/envretry marked snapshots and clears provenance after a successful CLI load. - Regression coverage Adds runtime unit tests and standalone Next.js boot scenarios for conflicting, blob-only, and
varlock rundeployments. - User guidance Documents resolution-time overrides, container behavior, the recommended runtime entrypoint, and the escape hatch.
azure/gpt-5.6-sol | 𝕏
varlock
@varlock/astro-integration
@varlock/cloudflare-integration
@varlock/nextjs-integration
@varlock/nuxt-integration
@varlock/vite-integration
@varlock/native-helper-darwin
@varlock/native-helper-linux-arm64
@varlock/native-helper-linux-x64
@varlock/native-helper-win32-x64
commit: |
…apshot When a server boots from the env blob baked into build output (e.g. Next.js standalone in a container where the varlock CLI is unreachable), initVarlockEnv treated the blob like a fresh resolution: runtime env vars for schema-declared keys were silently ignored, and items that resolved to undefined at build time DELETED the corresponding runtime values from process.env. A schema item like REDIS_URL= would actively clear docker-run-provided values at boot, silently breaking the service. The baked snapshot stays authoritative, but runtime values are now checked against it. Absence is not divergence: blob-only deploys (serverless, deliberate bake-into-image) see no change. A runtime value that CONFLICTS with the snapshot is evidence of misconfiguration (config being supplied that cannot be validated or applied), so the boot fails loudly, naming the keys and pointing at varlock run, which re-resolves against the runtime env with full validation. Setting _VARLOCK_ALLOW_BAKED_ENV_CONFLICTS=1 downgrades the failure to a warning and boots on the baked values, without deleting runtime vars from process.env. Mechanics: the injection preludes (nextjs webpack/turbopack, vite resolved-env SSR entry) mark the blob via globalThis.__varlockEnvInjectedAtBuild only when the baked fallback actually engages; the marker is mirrored to _VARLOCK_ENV_INJECTED_AT_BUILD so child/worker processes inheriting the blob apply the same semantics; the @next/env compat skips the cached blob and clears the marker when it can re-resolve (covers runtimes loading before the compat, e.g. via instrumentation). Framework tests gain three standalone-container scenarios per bundler: conflicting runtime env fails loudly, blob-only boots serve baked values, and a varlock run entrypoint resolves runtime env properly. Docs now state that process-env overrides apply at resolution time and how baked deployments differ.
1a850dd to
91d3a97
Compare
__VARLOCK_ENV_INJECTED_AT_BUILD (double underscore: set by varlock) is the child-process mirror of the baked-fallback marker; _VARLOCK_ALLOW_BAKED_ENV_CONFLICTS (single underscore: user-settable) is the conflict-guard escape hatch. Both added to the reserved-vars registries and the reserved-variables docs page.
Snapshot-neutral name that stays correct when explicit freezing replaces implicit build-time baking.
There was a problem hiding this comment.
ℹ️ No critical new issues in this delta; one stale documentation anchor is noted inline.
Reviewed changes Reviewed the escape-hatch rename since the prior Pullfrog review and rechecked it against the complete conflict-guard change.
- Escape hatch naming Renamed the warning-only override to
_VARLOCK_ALLOW_ENV_SNAPSHOT_CONFLICTSacross runtime behavior, tests, reserved-variable registration, deployment guidance, and release notes. - Existing conflict behavior Preserved the build-snapshot conflict detection, warning path, and process environment handling while changing the public variable name.
azure/gpt-5.6-sol | 𝕏
…rkers The globalThis/env-var marker could outlive the blob it described: fresh resolutions (auto-load, vite reload) replaced the blob without clearing it, misclassifying validated runtime data as a baked snapshot. The injectedAtBuild flag now lives inside the payload itself, so provenance travels with the blob (child processes, encryption round-trips) and a fresh resolution inherently clears it. Also skip conflict-flagging values whose raw pre-coercion form was never recorded (sensitive coerced overrides, file-sourced coerced values), since they cannot be verified against the ambient env.
There was a problem hiding this comment.
Important
The payload-scoped provenance fix resolves the prior stale-marker issue, but Vite's baked payload still overrides the fresh blob supplied by the documented varlock run remedy.
Reviewed changes Reviewed the payload-provenance rewrite since the prior Pullfrog review and rechecked its Next.js, Vite, encryption, runtime, test, and documentation effects.
- Payload provenance Moved
injectedAtBuildinto the serialized graph so provenance follows the exact payload through encryption and child processes. - Fresh resolution handling Updated Next.js compatibility loading to reject flagged cache data and naturally replace it with an unflagged fresh resolution.
- Conflict matching Avoided false conflicts for coerced values whose raw source form was not serialized.
- Regression coverage Reworked runtime tests around payload-local provenance and confirmed unflagged fresh blobs retain existing semantics.
- Internal cleanup Removed the process marker registration and its obsolete documentation.
azure/gpt-5.6-sol | 𝕏
There was a problem hiding this comment.
ℹ️ No new issues in this delta. The existing Vite runtime-blob precedence thread remains open because this scenario does not exercise a fresh
__VARLOCK_ENVsupplied byvarlock run.
Reviewed changes Reviewed the Vite framework-test addition since the prior Pullfrog review and checked it against the complete snapshot-conflict implementation.
- Vite conflict scenario Added an end-to-end
resolved-envSSR build that boots with a conflicting ambient value and verifies a nonzero exit plus the conflict diagnostic andvarlock runguidance. - Existing Vite precedence concern Confirmed the new scenario launches without
__VARLOCK_ENV, so it validates the baked-snapshot guard but does not change or cover the previously reported fresh-runtime-blob precedence path.
azure/gpt-5.6-sol | 𝕏
…plicit bake warns injectedAtBuild becomes a mode: 'fallback' (Next.js standalone implicit bake, where nobody chose baking and a conflicting runtime value is a misconfiguration -> boot fails) vs 'explicit' (vite resolved-env, where bake-into-build is the declared contract and a stray runtime value must not kill a working deploy -> loud warning, boots on baked values). Both modes still suppress the stale-echo cleanup so runtime vars are never deleted.
There was a problem hiding this comment.
Important
The new warning-only contract for explicit Vite and Nuxt snapshots is not reflected in the public deployment guidance, which still says these conflicts fail the boot.
Reviewed changes Reviewed the explicit-versus-fallback snapshot behavior introduced since the prior Pullfrog review.
- Split baked intent Replaced the boolean provenance marker with
fallbackandexplicitmodes so implicit Next.js snapshots fail on conflicts while chosen Vite snapshots warn. - Changed explicit boot behavior Kept explicit baked values authoritative, preserved conflicting ambient values, and allowed the process to continue after logging the conflict.
- Updated regression coverage Changed the Vite end-to-end scenario and added runtime assertions for warning-only explicit snapshots.
- Updated release notes Documented the distinct behavior of explicit Vite baking in the package changeset.
⚠️ Deployment guidance still promises fail-closed behavior
The environments, Docker, and reserved-variable pages say a conflicting runtime value fails any baked-snapshot boot. A Vite or Nuxt deployment using the newly explicit resolved-env mode now logs a warning and continues, so operators following those pages may believe a conflicting deployment was stopped when it was not.
Technical details
# Document the explicit snapshot contract
## Affected sites
- `packages/varlock-website/src/content/docs/guides/environments.mdx:22` - describes failure as the general behavior for framework-baked snapshots
- `packages/varlock-website/src/content/docs/integrations/docker.mdx:54-56` - includes Vite SSR in the baked-image topology, then says every conflict fails
- `packages/varlock-website/src/content/docs/reference/reserved-variables.mdx:69` - presents the escape hatch and fail-closed behavior without limiting them to fallback snapshots
- `packages/varlock-website/src/content/docs/integrations/vite.mdx:121` - documents `resolved-env` without its new conflict behavior
- `packages/varlock-website/src/content/docs/integrations/nuxt.mdx:195` - documents `resolved-env` without its new conflict behavior
## Required outcome
- Distinguish implicit fallback snapshots, which throw unless the escape hatch is enabled, from explicit Vite and Nuxt `resolved-env` snapshots, which warn and continue on baked values.
- Make the scope of `_VARLOCK_ALLOW_ENV_SNAPSHOT_CONFLICTS` clear, since it does not change explicit-mode behavior.azure/gpt-5.6-sol | 𝕏
…ocs scope A blob injected at boot (varlock run) now wins over the baked payload, matching the Next.js preludes, so the conflict warning's varlock run remedy actually works for resolved-env artifacts. Docs now say explicit bakes warn on conflicts rather than failing the boot.
There was a problem hiding this comment.
Important
The plaintext Vite precedence fix works, but a supported encrypted runtime-blob path still fails before initialization.
Reviewed changes Reviewed the Vite runtime-blob precedence fix and documentation corrections added since the prior Pullfrog review.
- Fresh Vite precedence Deferred both plaintext and encrypted baked payloads when a runtime
__VARLOCK_ENVis already present. - Regression coverage Added a Vite SSR scenario proving that a fresh plaintext runtime blob controls runtime-resolved reads without a baked-snapshot warning.
- Documentation scope Distinguished explicit Vite baking from implicit fallback baking in the environments and reserved-variable references.
azure/gpt-5.6-sol | 𝕏
varlock run --inject blob under @encryptInjectedEnv hands the child a varlock:v1: ciphertext blob; now that the baked payload defers to an ambient blob, the prelude must decrypt it in place (mirroring nextjs init-server) instead of letting initVarlockEnv throw on ciphertext.
There was a problem hiding this comment.
ℹ️ No new issues in this delta. The encrypted runtime-blob failure is fixed; the previously reported deployment-documentation scope concern remains outstanding.
Reviewed changes Reviewed the encrypted ambient-blob fix added since the prior Pullfrog review and rechecked it against the complete snapshot-conflict implementation.
- Ambient blob decryption Decrypted a fresh
varlock:v1:process blob beforeinitVarlockEnv()so encryptedvarlock run --inject blobdata remains authoritative over the baked Vite payload. - Regression coverage Added a Vite SSR scenario that supplies an encrypted ambient blob and verifies successful initialization without a snapshot-conflict warning.
azure/gpt-5.6-sol | 𝕏
…t vite resolved-env conflict behavior
There was a problem hiding this comment.
ℹ️ No new issues in this delta. The general, Docker, and Vite guidance now reflects the explicit snapshot behavior; the previously reported Nuxt documentation gap remains outstanding.
Reviewed changes Reviewed the deployment-documentation corrections added since the prior Pullfrog review and rechecked them against the complete snapshot-conflict implementation.
- Explicit bake guidance Distinguished implicit fallback snapshots, which fail on conflicts, from explicit Vite
resolved-envsnapshots, which warn and continue on baked values. - Deployment references Updated the environments guide, Docker topology, Vite integration page, and reserved-variable reference with the warning-only explicit behavior and fresh-blob precedence.
azure/gpt-5.6-sol | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes Reviewed the Nuxt documentation correction added since the prior Pullfrog review and checked it against the current Nuxt and Vite integration behavior.
- Nuxt explicit-bake guidance Documented that
resolved-envconflicts warn and continue on baked values, while a fresh runtime__VARLOCK_ENVblob takes precedence.
azure/gpt-5.6-sol | 𝕏
…ambient blob resolved-env bakes config into the artifact on purpose, and its static values are inlined at build time - deferring to a boot-time blob would override only the runtime-resolved subset, leaving the artifact reading from two resolutions. Reverts the precedence change; the explicit-mode conflict message now says so instead of suggesting varlock run. The ambient-blob decryption stays: with no baked payload (init-only) an encrypted blob from 'varlock run --inject blob' would otherwise reach init as ciphertext.
There was a problem hiding this comment.
Important
The new Vite precedence contract still processes an ambient encrypted blob that should be ignored, so irrelevant ciphertext can abort a resolved-env artifact.
Reviewed changes Reviewed the Vite resolved-env precedence reversal since the prior Pullfrog review.
- Frozen payload precedence Made the baked Vite payload authoritative over an ambient runtime blob to keep static and runtime-resolved values on one resolution.
- Mode-specific encrypted blobs Moved encrypted ambient-blob coverage to
init-only, where the runtime blob remains the selected source. - Deployment guidance Updated Vite, Nuxt, and Docker guidance to state that
varlock rundoes not replace aresolved-envpayload.
azure/gpt-5.6-sol | 𝕏
| lines.push(`globalThis.__varlockEncryptedEnv = ${JSON.stringify(encrypted)};`); | ||
| } else { | ||
| lines.push(`globalThis.__varlockLoadedEnv = ${JSON.stringify(varlockLoadedEnv)};`); | ||
| lines.push(`globalThis.__varlockLoadedEnv = ${serialized};`); |
There was a problem hiding this comment.
Making the baked global unconditional means an ambient __VARLOCK_ENV is no longer a selected source in resolved-env, but the shared block below still decrypts any varlock:v1: ambient value and can throw for a missing key, wrong key, or malformed ciphertext before initialization. Skip ambient-blob decryption when the baked payload is authoritative, and cover resolved-env with encrypted invalid or keyless ambient data.
Technical details
# Ignore ambient ciphertext in resolved-env mode
## Affected sites
- `packages/integrations/vite/src/index.ts:425-430` - installs the authoritative baked payload on `globalThis`
- `packages/integrations/vite/src/index.ts:461-465` - still validates and decrypts the now-ignored process blob in every non-auto-load mode
- `framework-tests/frameworks/vite/vite-shared.ts:415-464` - covers plaintext ambient data under `resolved-env` and ciphertext only under `init-only`
## Required outcome
- A `resolved-env` artifact must initialize from its baked payload regardless of an irrelevant ambient `__VARLOCK_ENV` value.
- `init-only` must continue decrypting its selected ambient blob and rejecting unusable ciphertext.…g the boot The reported bug is that 1.17.1 DELETES runtime-provided env vars on a baked boot. Failing the boot on a mismatch was new enforcement of a condition that has been silently true in every version, and it can take down deployments that work today (docker sets HOSTNAME in every container; promoted images and 'fly secrets set' drift the same way) - including the reporter, who is restored by the delete fix alone but would stay down under a throw. Baked values stay authoritative for ENV, runtime-provided vars are never deleted, and a mismatch is logged so it is visible rather than silent. With nothing throwing, the fallback/explicit mode split and the _VARLOCK_ALLOW_ENV_SNAPSHOT_CONFLICTS escape hatch are both unnecessary and are removed. Proper enforcement belongs with freeze.
There was a problem hiding this comment.
Important
The warning-only contract leaves one build scenario stale and introduces one inaccurate deployment claim.
Reviewed changes Reviewed the warning-only baked-snapshot behavior introduced since the prior Pullfrog review.
- Warning-only conflicts Replaced fallback and explicit provenance modes with a single build-baked marker and changed every snapshot mismatch to log without blocking startup.
- Runtime preservation Kept runtime values for items that were undefined in the baked snapshot while retaining baked values as authoritative for
ENVand definedprocess.enventries. - Contract cleanup Removed the conflict escape hatch and updated release notes, deployment guidance, Vite coverage, and runtime unit tests for the new behavior.
azure/gpt-5.6-sol | 𝕏
| } | ||
| if (conflictingKeys.length) { | ||
| // eslint-disable-next-line no-console | ||
| console.error([ |
There was a problem hiding this comment.
Changing this path to warning-only leaves the Next.js standalone scenario asserting the removed fail-closed behavior and old conflicts with... diagnostic. The build scenario will fail when enabled and no longer verifies that the server boots while preserving the runtime value for the undefined baked item.
Technical details
# Align the Next.js standalone scenario with warning-only behavior
## Affected sites
- `packages/varlock/src/runtime/env.ts:587` - now logs `Runtime environment differs` and continues
- `framework-tests/frameworks/nextjs/nextjs-shared.ts:615-653` - still describes a failed boot and requires the removed `conflicts with the build-time env snapshot` text
## Required outcome
- Update the standalone scenario to assert the warning, a successful response, baked `ENV` behavior, and preservation of the runtime-provided `process.env` value.|
|
||
| Use when a [framework integration](/integrations/overview/) (Next.js, Vite SSR, Cloudflare Workers, etc.) resolves and injects env during `build`. Varlock belongs in the **builder** stage; the runtime image receives env via the platform or an encrypted blob, not via the CLI. | ||
|
|
||
| The baked snapshot is authoritative: it was resolved at build time, so values passed in the container's runtime environment (e.g. `docker run -e REDIS_URL=...`) cannot be validated or applied to it, and `ENV` uses the baked values. If a runtime env var differs from the snapshot for a schema-declared key, varlock logs a warning naming the keys so the mismatch is visible instead of silent; the boot is not blocked, and runtime-provided vars are left untouched in `process.env`. To have runtime env values apply, boot through `varlock run` (see the Runtime injection tab), which re-resolves against the runtime environment with full validation and coercion. Blob-only deployments, where no schema values are set in the runtime env, are unaffected. |
There was a problem hiding this comment.
“Runtime-provided vars are left untouched” is only true when that item was undefined in the baked snapshot. For a defined item, initVarlockEnv overwrites process.env with the baked value, and the new unit test explicitly asserts that behavior, so narrow this sentence to avoid promising preservation for all keys.
|
Superseded by #1055, which is the regression fix alone (~250 lines vs 585). The scope here drifted a long way past the reported bug: a mismatch warning, a #1055 keeps the one thing that was hard-won here: the |



Fixes a destructive silent failure reported from production: a Next.js standalone container passing env vars at boot (
docker run -e REDIS_URL=...) had those vars deleted fromprocess.envby varlock at startup, taking the service down.Root cause
In a container where the varlock CLI is unreachable, the standalone server falls back to the env blob baked into the bundled runtime at build time.
initVarlockEnvtreated that blob like a fresh resolution, so items that resolved to undefined at build time (REDIS_URL=in the schema, no value in the build env) triggered the stale-echo cleanup from #1038, deleting the runtime-provided value. That cleanup assumes a resolution happened in this process (a genuine ambient value would have acted as an override and resolved to it), which is false for a blob resolved on a build machine.The deletion shipped only in
varlock@1.17.1; 1.17.0 and earlier are unaffected.Behavior change
Scoped deliberately to the regression plus visibility, so no currently-booting deployment breaks:
ENV, as before: it was resolved at build time, and runtime values cannot be validated or coerced against it (the blob carries no schema info).FLAG=YES→true) are skipped rather than reported.Enforcing the mismatch (failing the boot) was deliberately left out: silent-ignore of runtime env on a baked boot predates this bug and exists in every released version, and failing on it would take down deployments that work today (docker sets
HOSTNAMEin every container; promoted images andfly secrets setdrift the same way). That belongs withvarlock freeze(#1049), where baking becomes explicit and the contract can be enforced honestly.Mechanics
resolved-envSSR entry) bake aninjectedAtBuild: trueflag INSIDE the serialized payload, before encryption where applicable. Provenance therefore travels with the blob to child/worker processes and through encryption round-trips, and can never outlive it: any fresh resolution produces an unflagged blob, so no marker clearing is needed anywhere.varlock run) wins over the baked fallback, as before. The viteresolved-envpayload stays authoritative by design: that artifact is frozen on purpose, and its static values are inlined at build time, so honoring a boot-time blob would override only the runtime-resolved subset.@next/envcompat does not trust a flagged blob as its cached env and attempts a real load instead, covering runtimes that load before the compat (e.g. viainstrumentation.js).init-onlymode an encrypted ambient blob (varlock run --inject blobunder@encryptInjectedEnv) reachedinitVarlockEnvas ciphertext and threw. The vite prelude now decrypts it, mirroring the nextjs init-server behavior.Testing
init-onlyencrypted-blob decrypt.Docs
The environments guide now clarifies that process-env overrides apply at resolution time; the Next.js, vite, nuxt, and Docker pages describe what a baked boot does with runtime env and how to make runtime values apply.
Follow-ups (separate PRs, discussed): land
varlock freeze(#1049) as the single explicit pinning mechanism, then rework the integrations so baking is explicit or platform-automatic, and enforce the frozen contract there.