Summary
Introduce a local override map file: a personal, gitignored layer placed on
top of a committed base map file. The same mental model as dotenv's
.env.local, applied to Envilder map files.
Design is recorded in ADR-0011.
This issue is the implementation tracker and the full specification.
Motivation
A committed envilder.json is the shared, PR-reviewed contract. But some values
are per-developer, not per-project. The driving case is the AWS profile:
- A committed
$config.profile forces every developer onto one named profile.
- Because
$config.profile takes precedence over the AWS_PROFILE environment
variable in
AwsSecretProviderFactory.ts,
a hardcoded profile actively defeats each developer's own credentials.
Beyond the profile, developers occasionally need to redirect a single mapping to
a personal secret path, switch provider locally, or add a machine-only variable —
without editing the shared file or polluting a PR.
Existing narrower mechanisms do not cover the general case:
AWS_PROFILE resolves only the profile (once the committed file stops
hardcoding it).
- "One map file per environment +
--map" selects a whole file but offers no
layering.
The golden rule
For any base map file XXXXX.json, if a sibling XXXXX.local.json exists in
the same directory, it is always layered on top.
- Name is derived from the base by inserting
.local before the extension:
envilder.json → envilder.local.json
config/prod.json → config/prod.local.json
- The override file has no override of its own (
XXXXX.local.local.json is
never sought).
- When the override file is absent, behavior is unchanged (silent).
Layering semantics
The two sections of a map file merge differently because they have different
shapes:
$config — replaced wholesale
A $config describes one coherent connection to one provider. Merging it
field-by-field produces incoherent mixes (e.g. an Azure vaultUrl next to a
leftover AWS profile).
- If the override carries a
$config → it replaces the base's entirely.
- If the override has no
$config → the base's $config stands.
Variable mappings — merged per key
Mappings are independent VAR → path entries.
- An override key replaces the same base key.
- Base-only keys are kept.
- Override-only keys are added.
Worked example
Precedence
CLI flag > local override > base
A CLI flag (--profile, --provider, --vault-url) is an explicit one-off and
must beat the per-machine default. This preserves the existing flags > $config
rule and inserts the local layer between flags and base.
An incoherent result (e.g. the override forces Azure while --profile is
passed) is rejected by the existing cross-provider validation, never silently
merged.
Scope: all surfaces
The override applies in the CLI, the GitHub Action, and every runtime
SDK (.NET, Python, Node.js). One mental model — "if the file exists, it wins" —
regardless of how the code runs. This matches the dotenv .env.local precedent,
which auto-loads across CLIs, frameworks, and running apps alike.
Guardrails (mandatory)
The override can redirect which cloud identity and which secrets load — not
merely override a literal value as dotenv does. Three guardrails are required:
- Visible warning on every application. e.g.
⚠ local override applied: envilder.local.json. In the SDKs this is a
deliberate exception to the silent-by-default rule (ADR-0003 / ADR-0010):
a silent identity redirect is worse than a log line, and the warning is what
makes an accidental leak detectable.
- Cross-language opt-out
ENVILDER_NO_LOCAL. Setting it disables override
resolution deterministically. CI/production use it as a kill switch. A single,
uniform mechanism across all four surfaces.
.dockerignore documented as a hard requirement for *.local.json —
because .gitignore does not protect built artifacts (a COPY . .
without .dockerignore would bake a developer's local override into a
production image).
No environment auto-detection (NODE_ENV / ASPNETCORE_ENVIRONMENT): there
is no uniform cross-language "production" signal, and it would reintroduce the
environment-selection question this feature deliberately avoids. Trigger is file
presence; opt-out is explicit.
Phase 0 — prerequisite quick wins (independent of the override)
These resolve the original profile pain cheaply and should land first (they may
already be partly covered by #384):
Implementation plan (vertical slices)
Implement as tracer bullets; CLI first (git-protected), then replicate the shared
convention to each SDK.
Acceptance criteria
Risk accepted (see ADR-0011 Consequences)
If a *.local.json leaks into a built artifact (missing .dockerignore), an SDK
will apply it in production. This is the same risk the dotenv .env.local
ecosystem accepts; mitigated (not eliminated) by the mandatory warning, the
opt-out, and documented .dockerignore guidance.
References
Summary
Introduce a local override map file: a personal, gitignored layer placed on
top of a committed base map file. The same mental model as dotenv's
.env.local, applied to Envilder map files.Design is recorded in ADR-0011.
This issue is the implementation tracker and the full specification.
Motivation
A committed
envilder.jsonis the shared, PR-reviewed contract. But some valuesare per-developer, not per-project. The driving case is the AWS
profile:$config.profileforces every developer onto one named profile.$config.profiletakes precedence over theAWS_PROFILEenvironmentvariable in
AwsSecretProviderFactory.ts,a hardcoded profile actively defeats each developer's own credentials.
Beyond the profile, developers occasionally need to redirect a single mapping to
a personal secret path, switch provider locally, or add a machine-only variable —
without editing the shared file or polluting a PR.
Existing narrower mechanisms do not cover the general case:
AWS_PROFILEresolves only the profile (once the committed file stopshardcoding it).
--map" selects a whole file but offers nolayering.
The golden rule
.localbefore the extension:envilder.json→envilder.local.jsonconfig/prod.json→config/prod.local.jsonXXXXX.local.local.jsonisnever sought).
Layering semantics
The two sections of a map file merge differently because they have different
shapes:
$config— replaced wholesaleA
$configdescribes one coherent connection to one provider. Merging itfield-by-field produces incoherent mixes (e.g. an Azure
vaultUrlnext to aleftover AWS
profile).$config→ it replaces the base's entirely.$config→ the base's$configstands.Variable mappings — merged per key
Mappings are independent
VAR → pathentries.Worked example
Precedence
A CLI flag (
--profile,--provider,--vault-url) is an explicit one-off andmust beat the per-machine default. This preserves the existing
flags > $configrule and inserts the local layer between flags and base.
An incoherent result (e.g. the override forces Azure while
--profileispassed) is rejected by the existing cross-provider validation, never silently
merged.
Scope: all surfaces
The override applies in the CLI, the GitHub Action, and every runtime
SDK (.NET, Python, Node.js). One mental model — "if the file exists, it wins" —
regardless of how the code runs. This matches the dotenv
.env.localprecedent,which auto-loads across CLIs, frameworks, and running apps alike.
Guardrails (mandatory)
The override can redirect which cloud identity and which secrets load — not
merely override a literal value as dotenv does. Three guardrails are required:
⚠ local override applied: envilder.local.json. In the SDKs this is adeliberate exception to the silent-by-default rule (ADR-0003 / ADR-0010):
a silent identity redirect is worse than a log line, and the warning is what
makes an accidental leak detectable.
ENVILDER_NO_LOCAL. Setting it disables overrideresolution deterministically. CI/production use it as a kill switch. A single,
uniform mechanism across all four surfaces.
.dockerignoredocumented as a hard requirement for*.local.json—because
.gitignoredoes not protect built artifacts (aCOPY . .without
.dockerignorewould bake a developer's local override into aproduction image).
No environment auto-detection (
NODE_ENV/ASPNETCORE_ENVIRONMENT): thereis no uniform cross-language "production" signal, and it would reintroduce the
environment-selection question this feature deliberately avoids. Trigger is file
presence; opt-out is explicit.
Phase 0 — prerequisite quick wins (independent of the override)
These resolve the original profile pain cheaply and should land first (they may
already be partly covered by #384):
"profile": "mac"from the committedenvilder.jsonsoAWS_PROFILE/ the local override drive theper-developer profile.
(ADR-0010 pattern) — never a silent fallback to the
defaultprofile.Implementation plan (vertical slices)
Implement as tracer bullets; CLI first (git-protected), then replicate the shared
convention to each SDK.
XXXXX.local.jsonfrom--map, layer it(
$configwholesale, mappings per key), applyflag > local > base, emit thewarning, honor
ENVILDER_NO_LOCAL. e2e test (LocalStack) proving an overriddenpath from
.localwins.ContainerConfigurationpath.MapFileParser/ facade + warning..gitignore+.dockerignoreguidance, changelog entries per surface.Acceptance criteria
envilder.local.jsonpresent,$configis replaced wholesale andmappings merge per key, on CLI / GHA / each SDK.
flag > local > base; cross-provider incoherence failsvalidation with a clear message.
ENVILDER_NO_LOCAL=1fully disables resolution on all surfaces.*.local.jsonadded to.gitignore;.dockerignorerequirement documented.Should_<Expected>_When_<Condition>+ AAA per stack.Risk accepted (see ADR-0011 Consequences)
If a
*.local.jsonleaks into a built artifact (missing.dockerignore), an SDKwill apply it in production. This is the same risk the dotenv
.env.localecosystem accepts; mitigated (not eliminated) by the mandatory warning, the
opt-out, and documented
.dockerignoreguidance.References
docs/adr/0011-local-override-map-file.mdLocal override map fileinCONTEXT.md