# .env Merge *Per-key merge for dotenv files, with secret detection and masked diffs.* ## Overview `.env` files are a special hell. Half the industry stores them in `.gitignore` and ships them out of band. The other half commits `.env.example` and reconstructs production values by hand. Either way, when two developers touch the same `.env` file — adding a feature-flag here, bumping a timeout there — Git's line merge produces spurious conflicts because every key sits on its own line and any new key shifts the blame map. Aura treats `.env` as what it is: an ordered list of `KEY=VALUE` bindings. Its merger operates per key. Adding `STRIPE_TIMEOUT=30` and adding `SENTRY_DSN=https://...` to the same file are disjoint operations. Only modifying the same key on both sides is a conflict. On top of that, Aura knows `.env` files carry secrets. Its diff and conflict output **masks values that look sensitive**, so secrets do not leak into terminal scrollback, CI logs, or screenshots. > A secret that appears in a terminal is a secret that appears on LinkedIn six months later. Mask by default. ## How It Works ### Parse into an ordered map Aura's dotenv parser accepts the common dialects: - `KEY=value` - `KEY="quoted value"` with `\n` and `\t` escapes - `KEY='single-quoted, no escapes'` - `KEY=` (empty) - `export KEY=value` - `# comment lines` - blank lines The result is an ordered map of entries: ```text Entry { key: "STRIPE_API_KEY", value: "sk_live_xxx", quote: None | Double | Single, export: bool, leading: comment[], trailing: comment[], } ``` Order is preserved for round-trip fidelity. Comments attach to the next key (or to the footer if no next key exists). ### Three-way diff per key Exactly the table from [JSON deep merge](/json-deep-merge), applied per key: | Ancestor | Ours | Theirs | Action | |----------|------|--------|--------| | present | unchanged | modified | take theirs | | present | modified | unchanged | keep ours | | present | modified | modified, same value | dedupe | | present | modified | modified, different | conflict | | absent | inserted | absent | keep ours | | absent | absent | inserted | take theirs | | absent | inserted | inserted, same value | dedupe | | absent | inserted | inserted, different | conflict | | present | deleted | modified | conflict | | present | deleted | deleted | delete | ### Secret detection Before printing anything to a terminal, Aura scans each entry to decide whether to mask its value. The classifier uses: 1. **Key name heuristics.** Substrings: `KEY`, `SECRET`, `TOKEN`, `PASSWORD`, `PASS`, `PWD`, `DSN`, `PRIVATE`, `CREDENTIAL`, `CERT`, `SALT`, `SEED`, `SIGNATURE`, `WEBHOOK`. Names like `PUBLIC_KEY`, `PUBLISHABLE_KEY`, `NEXT_PUBLIC_*` are allow-listed. 2. **Value shape heuristics.** Vendor-prefixed tokens (`sk_live_`, `pk_test_`, `xoxb-`, `AKIA`, `ghp_`, `eyJ...` JWTs), base64 blobs longer than 24 chars, PEM headers (`-----BEGIN`). 3. **Project overrides.** `.aura/secrets.json` can force-mask or force-show specific keys. Masked output renders as `KEY=` — enough hint to disambiguate two different values, none of the actual secret. ### Output Merged files are serialized in the observed style: - Original ordering of surviving keys. - New keys from ours appended after their nearest sibling, then new keys from theirs. - Original quote style preserved per key. - Comments travel with their keys. ## Examples ### A: Two new keys Ancestor `.env.example`: ```bash DATABASE_URL=postgres://localhost/app PORT=3000 ``` Ours: ```bash DATABASE_URL=postgres://localhost/app PORT=3000 STRIPE_API_KEY=sk_test_placeholder ``` Theirs: ```bash DATABASE_URL=postgres://localhost/app PORT=3000 SENTRY_DSN=https://public@sentry.io/1 ``` Per-key diff: ```text ours = [Insert("STRIPE_API_KEY")] theirs = [Insert("SENTRY_DSN")] disjoint -> clean merge ``` Merged (displayed on terminal): ```bash DATABASE_URL=postgres://localhost/app PORT=3000 STRIPE_API_KEY= SENTRY_DSN=https://public@sentry.io/1 ``` On disk, the real values are written — masking is a display concern, not a storage concern. ### B: Same key, different values Ours bumps `PORT` to `8080`. Theirs bumps it to `4000`. Aura raises: ```text CONFLICT: .env :: PORT ancestor: 3000 ours: 8080 theirs: 4000 ``` Non-secret, so values are shown in full. A `resolve` decision affects only this key; the rest of the file merges cleanly. ### C: Conflict on a secret, masked ```text CONFLICT: .env :: DATABASE_URL ancestor: postgres://localhost/app ours: theirs: ``` The conflict is real and must be resolved, but the competing values are not splashed to the terminal. The [interactive conflict picker](/interactive-conflict-picker) offers a `reveal` keystroke that prints the full values under a one-line confirmation, useful when the masked hint is ambiguous. ### D: Reformatting without semantic change Ours reformats `PORT=3000` to `PORT="3000"`. Theirs adds `LOG_LEVEL=info`. Aura treats the quote change as a style-only edit (value unchanged), applies theirs's insert, and keeps ours's quote style: ```bash DATABASE_URL=postgres://localhost/app PORT="3000" LOG_LEVEL=info ``` ### E: Deletion + modification Ours removes a deprecated key `OLD_API_URL`. Theirs changes its value. Aura raises a conflict: ours wanted it gone, theirs wanted it different. The picker offers `keep deleted (ours)`, `keep modified (theirs)`, or `keep both` (theirs wins, key stays). ## Edge Cases **Multiline values.** Values containing `\n` (either literal in double-quoted form or as real newlines when the parser dialect allows) are treated as opaque scalars. Conflicting multiline values raise a normal conflict; the picker prints them with line numbers. **Interpolation.** Some tools support `${OTHER_KEY}` interpolation. Aura does not evaluate it — it merges the literal string. A change from `${API_HOST}` to a hard-coded URL is a value change like any other. **Export prefix.** `export FOO=bar` is parsed as a normal entry with `export: true`. The flag is preserved through merge; if one side adds `export` and the other removes it, Aura takes the non-default (keeps the export). **Comment-only files.** `.env.example` files sometimes hold only commented documentation. Aura still parses them structurally; comments merge cleanly unless both sides rewrite the same comment block. **Duplicate keys.** Dotenv behavior varies by loader — last-wins in most, first-wins in some. Aura preserves the order and emits a warning. Merges pick the later occurrence as authoritative. **Gitignored `.env` files.** If your real `.env` is not tracked, Aura cannot three-way merge it — there is no ancestor. Aura detects this and offers a **two-way reconciliation**: `aura env reconcile` prompts per differing key with masked output, using the last local snapshot (see [snapshots](/how-ast-merge-works)) as an implicit ancestor when available. **`.env.example` drift.** A common workflow: keep keys in sync between `.env.example` (committed) and `.env` (local). Aura can enforce this with `aura env lint`, which reports keys present in one but not the other. **Leaked mask.** If a user explicitly passes `--no-mask`, Aura prints values in full and records the choice in the audit log. There is no way to opt out silently. **Key ordering conventions.** Some teams alphabetize `.env` files; others group by feature. Aura preserves the observed ordering on the ours side and inserts new keys from theirs at the nearest grouped position — after the last key that shares a prefix, or at the end of the file when no grouping is evident. The ordering heuristic is deliberately simple; a project that needs strict alphabetical order can run `aura env sort` as a post-merge hook. **Diff output.** The default `aura diff` command renders `.env` changes in a key-oriented form: ```text .env + LOG_LEVEL = info ~ PORT = 3000 -> 8080 - OLD_API_URL = SENTRY_DSN = ``` Additions, modifications, deletions, and unchanged-but-secret entries each get a distinct marker. This output format is stable and suitable for parsing by review tools. **File-name variants.** `.env`, `.env.local`, `.env.production`, `.env.example`, `.env.test` — Aura recognizes the common variants and applies dotenv merge to each. The suffix is preserved in the path; values do not bleed across variants. **Per-environment precedence.** In projects that layer `.env.production` over `.env`, Aura merges each file independently. There is no cross-file coupling during merge; precedence is a runtime concern of whichever loader the application uses. **False positives on detection.** A key like `MONKEY_SEED` will match the `SEED` substring and get masked even though it may not be sensitive. The conservative default is to over-mask; an explicit allow-list in `.aura/secrets.json` overrides the heuristic: ```json { "never_mask": ["MONKEY_SEED", "DEMO_TOKEN"], "always_mask": ["LEGACY_KEY"] } ``` **Secret scanning integration.** Aura's masker shares its rules with `aura audit secrets`, a separate command that scans the working tree for committed secrets. The same regexes power both features, so a key recognized as sensitive in `.env` is also flagged if it accidentally lands in a source file. **CI environment.** In CI, the terminal is not interactive and mask reveals are never permitted. Aura detects `CI=true` and `NO_COLOR=1` and adjusts — values remain masked regardless of flags, and conflict resolution must be scripted via `aura resolve` invocations in the job definition. **Audit log retention.** Every resolution on an `.env` file is recorded with masked values by default. Reveals are recorded with a marker that reveal happened, but never with the revealed value. This keeps audit logs safe to archive without redaction. **Shell-sourced dotenv variants.** Some projects use full shell syntax (`FOO=$(date)`) in files named `.env`. Aura's parser accepts simple command substitutions as opaque values but flags them; treating a dynamic value as a static string on one side and a different string on the other would be misleading. For these repos, text merge may be a better default. ## See Also - [JSON deep merge](/json-deep-merge) - [YAML merge](/yaml-merge) - [Conflict resolution](/conflict-resolution) - [Interactive conflict picker](/interactive-conflict-picker) - [Merge strategies](/merge-strategies)