Library / SDK
allan2/dotenvy avatar
allan2/dotenvy

dotenvy replaces a process-wide side effect with a returned map

A well-maintained fork of the dotenv crate

1,259 stars84 forksRustMIT

At a glance

What is it?
dotenvy is the maintained fork of the Rust dotenv crate, recommended in security advisory RUSTSEC-2021-0141, and its main design change is that loading an env file returns a map instead of mutating the process environment. The recommendation from that fork is also its limitation: the crates.io release is from 2023 while main carries an unpublished 0.16.
Who is it for?
Adopt dotenvy if you are starting a Rust project that reads a .env file, since it is the fork the Rust security advisory points to and the non-modifying API removes a global side effect. Do not start a new project expecting features from the unpublished 0.16 on main, since the README says that version is still subject to change.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 66 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why the fork exists

The reason for dotenvy's existence is a maintenance gap with a security angle.

The original dotenv crate has not been updated since June 26, 2020, and attempts to reach the authors and present maintainer were not successful, which the README documents with a link to the issue where that attempt was made. The fork's stated purpose is to serve as the development home for the dotenv implementation in Rust.

The advisory reference is what pushes most people to move. The README names dotenvy as the suggested alternative to dotenv in security advisory RUSTSEC-2021-0141, so if your dependency tree currently pulls in the original crate, this is the migration the Rust ecosystem points you at.

The list of differences from the original is concrete rather than a rewrite: the non-modifying API, a configurable `EnvLoader`, optional loading with ergonomic dev and prod handling, reader support for any `io::Read` source, a more informative `Error` type carrying the file path and variable name, the `load` attribute macro, multiline support, and more examples and documentation.

The workspace also publishes three crates rather than one. `dotenvy` is the library, `dotenvy_macro` provides compile-time .env inspection as a fork of dotenv_codegen, and `dotenvy-macros` is the runtime library holding the `load` attribute macro. There is also a `dotenvy` binary that loads an env file before executing a specified command.

The non-modifying API is the point

The library exposes two APIs, and the README recommends the one that does not touch your process environment.

The non-modifying API builds an `EnvLoader` and calls `load`, which returns a map you read from directly:

rs
use dotenvy::{EnvLoader};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let env_map = EnvLoader::new().load()?;
    println!("HOST={}", env_map.var("HOST")?);
    Ok(())
}

Nothing about that mutates global state. In a program that loads several environment files for different contexts, or in a test that loads fixtures, that difference is not cosmetic.

Configuration is done on the loader, and construction cannot fail. `EnvLoader::with_path` takes an explicit path, `EnvLoader::new` is shorthand for the default, `substitution(true)` opts into variable expansion, and `with_reader` loads from any reader, which is what makes loading from a string or a test fixture possible.

The sequencing option is the part worth understanding. `EnvSequence::InputThenEnv` reads the file first and lets existing environment values win, while `EnvSequence::EnvThenInput` lets the file override what is already set, which the README describes as overriding existing values in the program environment.

Because loader construction is infallible and I/O is deferred to the `load` call, you can build a loader conditionally and decide at runtime whether to load at all. That is what the optional loading and dev/prod examples exist to demonstrate.

The modifying API exists for child processes

There is a case where not modifying the environment is impossible, and the README names it: you are spawning a child process that reads the environment.

For that, there is the `load` attribute macro from the `dotenvy-macros` crate, enabled through a feature:

rs
#[dotenvy::load]
#[tokio::main]
async fn main() {
    println!("HOST={}", std::env::var("HOST").unwrap());
}

The ordering of the attributes is the detail to notice, and the README explains it. Because `set_var` is not thread-safe, the macro modifies the environment before the async runtime starts. If you applied it inside an async context you would be mutating global state on arbitrary threads.

The macro is configurable, and the default expansion is shown in the README:

rs
#[dotenvy::load(path = "./env", required = true, override_ = false)]

So path, whether the file is required, and whether values override are all parameters. For cases the macro cannot express, `EnvLoader::load_and_modify` is the escape hatch.

For compile-time reading there is a third option: the `dotenv!` macro from `dotenvy_macro`, which inspects .env files while the crate compiles. That removes the runtime read entirely, at the cost of a rebuild whenever the file changes.

File format and the substitution question

An environment file is a plain text file of key-value pairs, and the shape is the one everyone expects:

sh
HOST=foo
PORT=3000

Common names are `.env`, `.env.dev` and `.env.prod`, though any name works, and `.env` is the default path for the crate.

Variables can span multiple lines, which matters for secrets and certificates pasted into a file.

Substitution is disabled by default and enabled with `EnvLoader::substitution(true)`. The README refers to an example file in the repository for the rules, and there is an example directory for multiple files and one for non-UTF-8 content, which is the case that catches people out when a file was edited on a machine with a different default encoding.

The default being off is a reasonable choice. Substitution means a value can depend on another value, which turns a config file into something with evaluation semantics, and most deployments do not need it. Turning it on is one method call, which is the right way to do it.

Examples in the repository cover dev/prod separation, optional loading, finding values, multiple files, non-UTF-8 input, and several variants of the modifying API including the tokio interaction.

The release gap is the thing to check

There is a version discrepancy that anyone reading the repository should notice immediately.

The README carries an unpublished version notice: the main branch contains the 0.16 version which is still subject to change, and the contents of the README are for a version not yet published to crates.io. The current stable release is 0.15.7.

The release dates confirm it. The most recent tags are v0.15.7 on 2023-03-22, v0.15.6 in October 2022 and v0.15.5 in September 2022. The workspace package version field in Cargo.toml also reads 0.15.7, so the published surface is a three-year-old patch line while the interesting work sits on main.

The last push was on 2026-08-18 and the repository is not archived. Commits are landing, so this is an ongoing project rather than an abandoned one, but the crates.io version and the repository head are different code.

The README closes by saying the project is actively maintained. Whether or not you accept that characterisation, the checkable facts are the ones above, and they mean a dependency decision should be made against a specific published version rather than against the main branch.

On compatibility, the minimum supported Rust version is 1.74.0 in Cargo.toml, and the stated policy is to support the latest eight rustc versions, approximately one year, with an MSRV increase explicitly not considered a semver-breaking change. That last policy is unusual and useful to know: you can be moved forward by a minor release.

The workspace sets edition 2021 and denies several clippy lint groups, all clippy, pedantic, nursery and cargo, at the workspace level, so lint policy is enforced for every crate in the repository including the examples.

The licence is MIT.

Editorial conclusion

Adopt dotenvy if you are starting a Rust project that reads a .env file, since it is the fork the Rust security advisory points to and the non-modifying API removes a global side effect. Do not start a new project expecting features from the unpublished 0.16 on main, since the README says that version is still subject to change. Verify first which version your lockfile resolves, whether your runtime needs the `load` attribute macro for spawning child processes, and that your toolchain meets the stated MSRV of 1.74.0 or roughly the latest eight rustc versions.

Frequently asked questions

Why should I use dotenvy instead of dotenv in Rust?

Because the original crate has not been updated since June 2020, attempts to reach its maintainers were unsuccessful, and the Rust security advisory RUSTSEC-2021-0141 names dotenvy as the suggested alternative. The fork adds a non-modifying API, a configurable EnvLoader, reader support and multiline values.

What is the difference between the two dotenvy APIs?

The non-modifying API returns an env map you read from without touching the process environment, and is recommended for most cases. The modifying API uses the `load` attribute macro from dotenvy-macros, which is needed when you spawn a child process that reads the environment.

Why does dotenvy recommend putting the load macro above the tokio main attribute?

Because set_var is not thread-safe. The README states the macro modifies the environment before the async runtime starts, so applying it inside an async context would mutate global state on arbitrary threads.

Which version of dotenvy should I depend on?

The published 0.15.7 line, which is the current stable release on crates.io. The README warns that main holds an unpublished 0.16 version that is still subject to change, and its contents describe a version not yet published.

Official sources

  1. allan2/dotenvy on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/allan2-dotenvy.svg)](https://hysenlabs.com/projects/allan2-dotenvy)

Was this useful?