How to set an environment variable for a child process
Short answer: A child process gets a copy of your process’s environment at the moment it starts, plus whatever you add for it. In the shell the add is a prefix assignment — GREETING=hello printenv GREETING; in Rust it is Command::env("GREETING", "hello") on the Command you are about to spawn. Neither one touches your own environment: std::env::var("GREETING") still returns what it returned before the child existed.
The blocks below are plain POSIX shell and work in any terminal on macOS or Linux. printenv NAME plays the part of the child program: it prints the value of one environment variable and nothing else, which makes inheritance visible. Run the blocks in order in one terminal — later ones assume the export from an earlier one.
A child starts from a copy of your environment
Section titled “A child starts from a copy of your environment”printenv HOME/Users/rangoYour line will be your own home directory, not this one. That is the whole inheritance: at spawn time the child receives a copy of the parent’s environment, in full. Every process you have run today — ls, git, cargo — started that way. The copy is not a live link, and it goes one way: changes the parent makes afterwards never reach a child that is already running, and nothing the child does to its copy comes back.
Setting a variable for one command
Section titled “Setting a variable for one command”GREETING=hello printenv GREETINGhelloThe prefix assignment puts GREETING into the environment of exactly one process — the printenv on the same line. More assignments fit the same way: A=1 B=2 command hands that one child both.
What the bare assignment does not do is the classic bug:
GREETING=helloprintenv GREETINGecho $?1GREETING=hello on its own line creates a shell variable, which lives inside the shell and is invisible to children. printenv looked the name up in its environment, found nothing, printed nothing, and exited 1 — which is why echo $? is the only output. This is the difference people mean by “exported”: a shell variable becomes an environment variable when it is exported, and only environment variables are inherited.
export: every child from now on
Section titled “export: every child from now on”export GREETING=helloprintenv GREETINGsh -c 'printenv GREETING'hellohelloOne export and every child from now on inherits GREETING — including an entirely new shell, which is what the sh -c line starts.
The copy goes one way
Section titled “The copy goes one way”sh -c 'GREETING=bye printenv GREETING'printenv GREETINGbyehelloThe inner shell overrode its own copy and printed bye; the parent still has hello. A child may override, remove, or add variables freely — none of it is visible to the process that spawned it.
Removing variables and starting empty
Section titled “Removing variables and starting empty”env -u GREETING printenv GREETING || echo "GREETING is gone"env -i printenv HOME || echo "with env -i, even HOME is gone"GREETING is gonewith env -i, even HOME is goneenv -u NAME command removes NAME from that child’s copy of the environment. env -i command hands the child an empty environment — no GREETING, no HOME, not even PATH.
The same moves in Rust
Section titled “The same moves in Rust”std::process::Command starts every child from the same baseline — your process’s environment, copied at spawn — and layers the operations above on top of that copy:
| the shell above | the Command spelling |
|---|---|
GREETING=hello printenv GREETING |
Command::new("printenv").env("GREETING", "hello") |
A=1 B=2 command |
Command::new("sh").envs([("A", "1"), ("B", "2")]) |
env -u GREETING command |
Command::new("sh").env_remove("GREETING") |
env -i command |
Command::new("sh").env_clear() |
Each method chains onto the Command you are already building, and none of them takes effect until the child actually spawns. The semantics are exactly what the shell sections showed:
- With no env method at all, the child inherits everything. Spawned from a process where
GREETINGis exported,Command::new("printenv").arg("GREETING").output()printshello. .env("GREETING", "from-rust")overrides that one name for that one child; the parent’sstd::env::var("GREETING")still returns the old value..env_remove("GREETING")produces a child in which the name is genuinely absent —printenv GREETINGexits 1, the same failure the bare shell assignment produced..env_clear().envs([("ONLY", "yes")])produces a child whose environment is exactly what you listed:printenvprintsONLY=yesand nothing else.
For several variables at once, .envs takes any iterator of name/value pairs.
The wrong tool: set_var
Section titled “The wrong tool: set_var”std::env::set_var("GREETING", "hello") also exists, and it does something different: it changes your own process’s environment. Every child you spawn afterwards inherits the change — including children that had no business seeing it — and the change lasts for the rest of your process. Since edition 2024 the function is also unsafe, so the compiler refuses a plain call:
printf 'fn main() {\n std::env::set_var("GREETING", "x");\n}\n' > set-var.rsrustc --edition 2024 set-var.rserror[E0133]: call to unsafe function `set_var` is unsafe and requires unsafe block --> set-var.rs:2:5 |2 | std::env::set_var("GREETING", "x"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe function | = note: consult the function's documentation for information on how to avoid undefined behavior
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0133`.For “a variable for one child process”, Command::env is the tool: it is safe, it is per-child, and the code says which spawn the variable belongs to. set_var has its place — when every child your process spawns from now on should see a variable — but that is a decision about your own process, and an unsafe one; a per-child setting belongs on the Command.
Related
Section titled “Related”- How to read environment variables — the other side of the same deal: reading the variables back inside the child
- Processes, environment and signals — the rest of the chapter