Skip to content
Select theme

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”
Terminal window
printenv HOME
/Users/rango

Your 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.

Terminal window
GREETING=hello printenv GREETING
hello

The 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:

Terminal window
GREETING=hello
printenv GREETING
echo $?
1

GREETING=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.

Terminal window
export GREETING=hello
printenv GREETING
sh -c 'printenv GREETING'
hello
hello

One export and every child from now on inherits GREETING — including an entirely new shell, which is what the sh -c line starts.

Terminal window
sh -c 'GREETING=bye printenv GREETING'
printenv GREETING
bye
hello

The 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.

Terminal window
env -u GREETING printenv GREETING || echo "GREETING is gone"
env -i printenv HOME || echo "with env -i, even HOME is gone"
GREETING is gone
with env -i, even HOME is gone

env -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.

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 GREETING is exported, Command::new("printenv").arg("GREETING").output() prints hello.
  • .env("GREETING", "from-rust") overrides that one name for that one child; the parent’s std::env::var("GREETING") still returns the old value.
  • .env_remove("GREETING") produces a child in which the name is genuinely absent — printenv GREETING exits 1, the same failure the bare shell assignment produced.
  • .env_clear().envs([("ONLY", "yes")]) produces a child whose environment is exactly what you listed: printenv prints ONLY=yes and nothing else.

For several variables at once, .envs takes any iterator of name/value pairs.

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:

Terminal window
printf 'fn main() {\n std::env::set_var("GREETING", "x");\n}\n' > set-var.rs
rustc --edition 2024 set-var.rs
error[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.