laravel/framework: Preserve unchanged values when encrypting readable environment files
Contributed by mathiasgrimm
Part of the framework v13.36.0 release
Pull request #61503 reworks how php artisan env:encrypt --readable treats an encrypted environment file that already exists. Instead of refusing to run without --force and re-encrypting every single value, the command now performs an incremental update: unchanged values keep their existing ciphertext, only new or changed values are encrypted, and variables you deleted from .env disappear from the encrypted file. The practical effect is that pull requests touching an encrypted environment file now show a minimal, reviewable diff instead of a wall of rewritten ciphertext.
What changed
Running env:encrypt --readable against an existing .env.encrypted file no longer requires --force. The command reads the existing file, decrypts each entry in memory, and compares it with the matching raw value from .env:
- Values that have not changed keep their previous encrypted representation.
- New or changed values are encrypted fresh.
- Variables removed from
.envare also removed from the encrypted file. - If the encrypted file does not exist, it is created, so first-time runs behave exactly as before.
The --force flag was redefined rather than removed: it now means "re-encrypt all values, overwriting the existing encrypted environment file" without reading the existing file at all. That is the path for key or cipher rotation and for replacing a corrupted or invalid baseline. Plain blob-mode encryption (without --readable) keeps its existing overwrite behavior.
Why it matters
Because AES ciphertext differs on every encryption, the old behavior meant that changing one variable rewrote every line of .env.encrypted. Reviews of those diffs were meaningless — you could not tell which secret actually changed without decrypting the whole file, and unrelated branches constantly conflicted over the same lines. With incremental updates, a one-line change in .env produces a one-line diff in the encrypted file. As a bonus, when no raw value has changed, the command skips the write entirely, preserving the baseline file byte-for-byte, including line endings and comments. Comparison happens in memory only; plain-text values are never written anywhere.
Before and after
Changing only DB_PASSWORD=1 to DB_PASSWORD=2 in .env and running:
php artisan env:encrypt --readable --key=3UVsEgGVK36XN82KKeyLFMhvosbZN1aF
Before (required --readable --force, touching every value):
-APP_NAME=<previous encrypted app name>
-DB_HOST=<previous encrypted host>
-DB_PASSWORD=<previous encrypted password>
+APP_NAME=<new encrypted app name>
+DB_HOST=<new encrypted host>
+DB_PASSWORD=<new encrypted password>
After (only the changed entry is rewritten):
APP_NAME=<unchanged encrypted app name>
DB_HOST=<unchanged encrypted host>
-DB_PASSWORD=<previous encrypted password>
+DB_PASSWORD=<new encrypted password>
For deliberate key or cipher rotation, keep using --force:
php artisan env:encrypt --readable --force --key=NEW_BASE64_KEY --cipher=AES-256-GCM
Safer failure handling
The update path validates the existing file before touching anything:
- If the existing file is a single encrypted blob rather than readable format, the command fails and tells you to use
--force. - Malformed entries (missing
=) and entries that cannot be decrypted — wrong key, wrong cipher, or corruption — fail with the offending line number, without overwriting the file and without--prunedeleting.env. - Updating requires the existing key. Interactively you are now prompted directly ("What is the encryption key?") instead of getting the generate/key select; an empty or missing key in non-interactive mode exits with "The existing encryption key is required to update the encrypted environment file." No key is silently generated when updating.
- A failed
put()(returningfalse) now aborts the command before--pruneruns, in every encryption mode, so a failed write can no longer lead to a deleted.env. - A missing source
.envis detected before any key prompt.
Duplicate variable names, multiline values, ${VAR} references, and reordering are all preserved correctly: occurrences are matched per value, and reordering entries in .env rewrites the file in the new order.
Upgrade impact for your scripts
- Automation that depended on the command failing with "Encrypted environment file already exists." will now succeed with an update. If you actually want a full re-encryption, pass
--force. - Non-interactive jobs that run against an existing encrypted file without
--keywill now exit with code 1 instead of generating a key; supply--key=base64:...(the key and cipher that produced the existing file). - Quote-only changes count as value changes, since comparison is on the raw value:
DB_PASSWORD="1"vsDB_PASSWORD=1triggers re-encryption. - The PR ships with an expanded test suite (190 tests, 1,060 assertions covering encryption, decryption, and command signatures), and the docs were updated in laravel/docs#11357.
Takeaways
env:encrypt --readablenow updates existing files in place, preserving unchanged ciphertexts; no extra option is needed.--forcemeans "re-encrypt everything from.envwithout reading the existing file" — use it for key/cipher rotation or corrupt baselines.- Removed variables are removed; missing encrypted files are created.
- Invalid or undecryptable baselines abort cleanly without writing or pruning, and failed writes abort before
--prunein all modes. - Updating requires the existing key, interactively or via
--key.