Environment variables are authority, not inert data:
they're invisible, often hold secrets (DATABASE_URL,
AWS_SECRET_ACCESS_KEY), and span the whole process. So
unlike command-line
arguments (which are public and need no grant), reading the
environment is a host-granted capability — the
env module doesn't exist unless you turn it on.
#Granting the capability
scua --allow-env=PORT,HOST,LOG_LEVEL script.scua # grant exactly those three names (the safe default)
scua --allow-env script.scua # grant every variable (trusted-host shorthand)
Without a grant, import env is a compile
error, before a line runs:
$ scua script.scua
script.scua:1: module `env` needs the `env` capability, which this run was not granted — grant it with `--allow-env` on the CLI, or the host's capability API when embedding
The per-name allowlist is the point: a variable you
didn't grant reads as nil even when it is set in the
environment, so a script can never see a secret it wasn't given.
Prefer --allow-env=NAME,...; reach for bare
--allow-env only when the script and the operator are the
same trusted party.
#Reading values
import env
let port = env.get("PORT") ?? "8080" -- missing/un-allowlisted → nil; ?? gives a default
let host = env.get("HOST") ?? "localhost"
-- required variable
if env.get("API_KEY") == nil then
error("API_KEY is required — set it in your environment")
end
env.get(name) returns the value as a string, or
nil if the variable was unset or not in the allowlist. A
missing variable is a normal nil, not an error. The module
is read-only — there is no env.set.
#Loading a .env file
The usual development setup is a .env file beside the
project. --env-file loads one and grants env
for exactly the names in it:
# .env
DB_HOST=localhost
DB_PORT=5432
TOKEN='s3cr3t'
$ scua --env-file=.env app.scua
Naming the file is the grant — you don't also need
--allow-env, and you shouldn't have to write the file's key
list twice. Note what that means: with --env-file alone
your script sees the file's names and nothing else. The
real environment stays hidden, so a stray
AWS_SECRET_ACCESS_KEY in your shell can't reach the script.
Add --allow-env=NAME,... if you want specific real
variables too.
A real environment variable wins over the file. That
is the rule every dotenv library follows, and it is what makes
.env a defaults file:
$ DB_HOST=prod.internal scua --env-file=.env app.scua # DB_HOST is prod.internal, DB_PORT is 5432
--env-file is repeatable and applied in order, and
nothing overwrites what is already set — so an earlier file wins over a
later one:
$ scua --env-file=.env.local --env-file=.env app.scua # .env.local overrides, .env fills the gaps
#The file format, and two things that catch people
KEY=value, # comments, an optional
export prefix, and both quotings — 'literal'
verbatim, "basic" processing \n,
\r, \t, \\ and \".
Either quote style may span lines. KEY= is the empty
string, which is set, not absent. A repeated key keeps its
last value, as other dotenv readers do — while across
several --env-files the first file wins, because
that is a different question: which line of a file owns a name, versus
which file does.
- A Windows path needs single quotes.
"C:\temp"contains\t, so it reads as a tab.'C:\temp'is verbatim and correct. (This is the same in Python's dotenv; it is a trap worth knowing once.) ${VAR}is not interpolated. It stays literal text. This is deliberate: a value that could name another variable would be a way around the allowlist you just set. Write the composed value out.
Anything the parser can't read — a line with no =, an
unterminated quote, junk after a closing quote — stops the run and says
so, rather than guessing.
#Reading a .env file as
data
If you want the contents of a .env file rather
than the authority — a deployment tool inspecting someone else's file,
say — dotenv.parse is a pure parser, no grant needed:
import dotenv
match dotenv.parse("PORT=8080\nTOKEN='s3cr3t'\n")
Ok(t) -> print(`{t.PORT} {t.TOKEN}`)
Error(why) -> print(`bad .env: {why}`)
end
$ scua parse_env.scua
8080 s3cr3t
It reads the same grammar as --env-file, so the two
can't disagree about what a file means. Reading one off disk needs the
fs capability, as
usual.
#What you can rely on
- Snapshot at launch. The environment is captured
once when the script starts, so
env.getis a pure lookup with no surprises mid-run — and a run's inputs are reproducible (recorded alongside the RNG seed). A change to the environment after launch is not seen. - Per-partition. Inside a spawned actor,
envis not granted — authority doesn't leak across partitions. Pass an actor what it needs in its starting message. - UTF-8. Values are UTF-8 strings. A non-UTF-8
allowlisted value fails the launch with a clear message (rather
than silently returning
nil); under bare--allow-env, a non-UTF-8 variable is skipped with a warning.
See the runnable examples/env.scua
(scua --allow-env=PORT,HOME examples/env.scua).