Build1 publisher3 min readPublished
A Rust .env parser that edits your secrets, and why SecretSpec forked instead of patching
SecretSpec's dotenv-ng 1.0 makes dollar signs literal after dotenvy silently dropped parts of a bcrypt hash. The damage surfaced as an authentication failure, far from the parser that caused it.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- Domen Kozar, the founder of Cachix, released dotenv-ng 1.0 on August 15 after SecretSpec found that the Rust dotenvy parser it used could silently change a credential while reading a .env file.
- SecretSpec said the new library began as a fork of dotenvy 0.15.7.
- The failure that triggered the fork was documented in SecretSpec issue #73, opened on April 17.
- A test .env file contained bcrypt fragments beginning with dollar-prefixed fields such as $2a$10.
- The .env file itself was intact, but dotenvy interpreted the bcrypt fragments as variable substitutions, and the issue's reproducer shows the loaded value losing pieces of the bcrypt strings.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
Domen Kozar, the founder of Cachix, released dotenv-ng 1.0 on August 15 after SecretSpec found that dotenvy, the Rust .env parser it depended on, could silently change a credential while reading a file [1]. The consequence worth noting is not the bug itself but its category: a library that loads secrets is inside the security boundary of everything downstream of it, which makes its correctness a supply-chain property rather than a convenience choice [17].
The failure was documented in SecretSpec issue #73, opened on April 17 [3]. A test .env file contained bcrypt fragments beginning with dollar-prefixed fields such as $2a$10 [4]. The file on disk was intact, but dotenvy read those fragments as variable substitutions, and the issue's reproducer shows the loaded value losing pieces of the bcrypt strings [5]. There was no parse error, because the parser returned a syntactically valid value [7]. SecretSpec met the damage much later, as an authentication failure, which pointed the investigation away from the parser [6]. That is the expensive shape of a bug. A visible error stops execution near its cause; a quietly modified token, password or hash travels into a database, an auth service or a third-party API and presents as bad credentials or a misconfiguration [8].
The behavior was already known upstream. A dotenvy request to disable substitution was opened on July 29, 2024, warning that dollar signs in environment values could disappear unexpectedly [9]. A pull request adding the option arrived in 2026 but targeted an unreleased API, so no published dotenvy release offered configurable substitution [10]. The repository still identifies 0.15.7, released on March 22, 2023, as its latest published release [11]. That is roughly three years between the last shipped release and the arrival of the fix in unreleased form [2], and 0.15.7 is also the version dotenv-ng forked from, so the only paths available were an unpublished API or a fork [2][1].
This dependency chain has already been handed off once: the original dotenv crate stopped releasing in 2020 and RustSec classified it as unmaintained, pointing users toward dotenvy [12]. Dotenvy describes itself as a well-maintained fork, yet its release gap had passed three years by the time the bug blocked SecretSpec [13]. Kozar acknowledged that maintainers do not owe downstream users a release, but said SecretSpec needed "breaking fixes on a schedule we control" [14]. SecretSpec moves credentials among local files, password managers and cloud secret stores, so a parser in that path becomes part of its security boundary regardless of who maintains it [17].
SecretSpec first considered carrying a small patch, then found enough problems around JSON, Windows paths, Unicode names, precedence and process-environment mutation to justify a breaking 1.0 rewrite [15]. In dotenv-ng, dollar signs are literal by default and substitution must be enabled explicitly [18]. The rewrite adds source-aware structured errors, validates complete inputs before changing the process environment, and puts an explicit Rust unsafe boundary around environment mutation [19]. Its grammar accepts dashes, leading digits, leading dots and Unicode in keys [20]. The README does not promise compatibility with every dotenv dialect [21]. The cost is a standing obligation for Cachix, which launched in 2018 to make reproducible builds easier for engineering teams [22], to track Rust changes, parser edge cases and divergent .env dialects [16].