2. Environment sealing
Stage 2 turns your local .env into the values the deployment will use.
- Read every key and value in
.env. - Classify each value: a real value, a placeholder, or plain configuration.
- Check that
.env.exampledeclares every key the code reads. - Ask Git whether
.env.productionis ignored. - Only then write
.env.production, with real values only.
A project with no .env has nothing to seal, and the stage is recorded as not applicable rather than failed.
No second secret detector
Section titled “No second secret detector”Values are classified with the same placeholder patterns, provider prefixes and entropy thresholds the audit’s secret rules use, and key coverage is the same comparison ENV-001 makes. The audit and the deploy cannot disagree about what a secret is or which keys are missing.
Placeholders are never written through
Section titled “Placeholders are never written through”A value such as CHANGEME is reported for you to supply and left out of the generated file. Deploying it would configure a live service with the word CHANGEME, which fails later in a way that is much harder to trace.
Why an unignored file is a hard failure
Section titled “Why an unignored file is a hard failure”.env.production holds real values. If Git would not ignore it, it is one git add away from being committed, so the check runs before anything is written and nothing is written at all. The question goes to git check-ignore, the only thing that reads nested ignore files, negations and global excludes the way Git itself does. If Git cannot answer, the check fails rather than guessing.
Real output
Section titled “Real output”A copy of the demo project whose .gitignore does not list .env.production, and whose .env holds a placeholder:
{ "project": "seal-bad", "ok": false, "reason": ".env.production is not covered by .gitignore, so it was not written", "ignored": false, "written": "", "sealed": ["PORT"], "blocked": ["CORS_ORIGIN"], "uncovered": [], "values": [ { "key": "PORT", "kind": "plain", "reason": "too short to be a credential, read as configuration", "value": "10000" }, { "key": "CORS_ORIGIN", "kind": "placeholder", "reason": "the value looks like a stand-in, not a real credential", "value": "CHANGEME" } ]}With the demo’s own .gitignore and real values, the stage passes and writes:
# Generated by ProdPilot. Real values, never commit this file.# Regenerate it rather than editing it by hand.PORT=10000CORS_ORIGIN=https://app.example.comA value that looks like a credential is masked in the result, the way the audit masks a finding. The sealed values then go to the Docker build test and to Render, and nowhere else.