The environment a spawned OS process gets: the node's own, minus the secrets the BEAM consumes.
An Erlang port inherits the whole environment of the node. Every place raxol
spawns something a model or a third party chose -- a directive's shell
command, an MCP stdio server, a benchmarked agent package -- would otherwise
hand that process the Sleuth key the fx tool spends, a share-link secret,
a signing key or a bearer token, for a prompt-injected printenv to read.
port_env/1 builds the {:env, _} value for Port.open/2: the caller's own
variables, plus each secret the caller did not set, mapped to false, which
unsets it in the child. A caller that passes a secret explicitly keeps it,
and a caller's own false is passed through as Port's unset marker.
cmd_env/1 is the same for System.cmd/3's :env option, whose unset
marker is nil.
Configuration
Read when a child is spawned, so a runtime change applies to the next one.
The config must be a keyword list and both keys lists of variable names
(strings matching [A-Za-z_][A-Za-z0-9_]*); anything else raises an
ArgumentError naming the key at the next spawn, instead of scrubbing less
than the operator meant:
config :raxol_core, Raxol.Core.ChildEnv,
# More names to unset, e.g. a custom `Wallets.Env` `env_var:`.
extra_secrets: ["MY_WALLET_KEY"],
# Names to let through after all, to EVERY child (see below).
pass: ["RAXOL_SLEUTH_API_KEY"]pass: is global: a passed name reaches every child the node spawns --
directive shell commands, MCP stdio servers, the earn bench's agent
packages -- not only the one that needs it. A nested raxol node that needs
the Sleuth key should get it from its own
config :raxol_agent, :web3, fx: [sleuth_api_key: ...] instead; a single spawn site that must hand one child a secret names
it in that call's env (port_env/1 keeps an explicitly given secret).
Using it
Port honours only the LAST {:env, _} in its options, so port_env/1 must
be the only one, or the last: a later {:env, _} drops the scrub silently.
For System.cmd/3, pass cmd_env/1 as :env, never port_env/1: it
takes nil, not false, as the unset marker.
What this does not do
It stops inheritance, not every read. A process running as the same user
can still read the node's launch environment (ps -E on macOS,
/proc/<pid>/environ on Linux). Keeping a secret away from those means not
putting it in the node's environment at all: a config provider, a 0600 file
or op.
Nor does it hide what a caller hands a child on purpose: a variable named
in a spawn's env is readable by everything that child runs.
Summary
Functions
A System.cmd/3 :env value: env (string names and values, or nil to
unset) followed by every secret it does not name, mapped to nil.
A Port.open/2 :env value: env (names and values as strings or
charlists, or false to unset) followed by every secret it does not name,
unset.
The variables unset in every child: the built-in set plus :extra_secrets, minus :pass.
Functions
A System.cmd/3 :env value: env (string names and values, or nil to
unset) followed by every secret it does not name, mapped to nil.
@spec port_env([{String.t() | charlist(), String.t() | charlist() | false}]) :: [ {charlist(), charlist() | false} ]
A Port.open/2 :env value: env (names and values as strings or
charlists, or false to unset) followed by every secret it does not name,
unset.
@spec secrets() :: [String.t()]
The variables unset in every child: the built-in set plus :extra_secrets, minus :pass.