Scrubbed environment for tool subprocesses.
Tools that spawn OS processes (bash, ripgrep in file_grep) must not
inherit the BEAM's environment: it routinely holds API keys, OAuth tokens,
and vault credentials, and an LLM is one printenv away from leaking them.
Shell-loader hooks (LD_PRELOAD, DYLD_INSERT_LIBRARIES) are dropped for the
same reason.
Every subprocess-spawning tool must use this module so the allowlist has exactly one definition.
Applying it is not as simple as an :env option
Both spawn mechanisms Nous uses get this wrong in opposite directions, so
scrubbed/0 on its own is not a control:
NetRunnerhas no:envoption at all.run/2forwards unknown options to the port layer, which ignores them, and the shepherdexecvps — so the child inherits the BEAM's entire environment. Passingenv: Nous.Tools.Env.scrubbed()there is silently discarded. Usewith_scrubbed_env/1, which puts the environment in argv where it cannot be ignored.System.cmd/3's:envmerges. It sets what you list and leaves everything else in place, soOPENAI_API_KEYsurvives. Only{name, nil}removes a variable. Usescrubbed_overrides/0.
Summary
Functions
The environment to pass to tool subprocesses: allowlisted variables that
are currently set, as {name, value} tuples.
An :env value for System.cmd/3 that genuinely leaves only the allowlist.
Wrap argv so it execs with only the scrubbed environment.
Functions
The environment to pass to tool subprocesses: allowlisted variables that
are currently set, as {name, value} tuples.
This is the definition of the allowlist. It is not directly usable as an
:env option — see the moduledoc, then with_scrubbed_env/1 or
scrubbed_overrides/0.
An :env value for System.cmd/3 that genuinely leaves only the allowlist.
Erlang's {env, _} merges rather than replaces, so this returns the
allowlisted pairs plus {name, nil} for every other variable currently set
in the BEAM. nil is how System.cmd/3 removes a variable (it maps it to
Erlang's false internally); passing false here raises, because
System.cmd/3 only special-cases nil.
Wrap argv so it execs with only the scrubbed environment.
Returns ["/usr/bin/env", "-i" | pairs] ++ argv. env -i clears the
environment before exec'ing, and the NAME=VALUE pairs are argv elements —
no shell parses them, so a value containing quotes, $, or spaces is inert.
This exists because NetRunner silently ignores an :env option (see the
moduledoc). Putting the environment in argv is the only way to make it
effective on that path, and it composes with Nous.Sandbox.confine/2, which
also works by wrapping argv.
/usr/bin/env is addressed absolutely: a bare env would resolve through
PATH, which is exactly the substitution this function exists to prevent.
Examples
iex> Nous.Tools.Env.with_scrubbed_env(["/bin/sh", "-c", "printenv"]) |> Enum.take(2)
["/usr/bin/env", "-i"]