The runtime throws plain Errors with the exact messages below. There are no custom error classes to catch by name.
Several fixes on this page are envless … commands. That binary is not part of the runtime. It ships in its own package, @goenvless/cli, which is the terminal command and nothing else, and @goenvless/env neither depends on it nor installs it. If envless is not on your PATH, install it with npm i -g @goenvless/cli@latest, or add it as a dev dependency and run it through npx envless. See Installation and CLI, runtime and SDK.
Envless env is not loaded yet. Import '@goenvless/env/register' or await load() first.
You read env.X before the loader finished. Importing @goenvless/env does not load anything on its own, and loading is asynchronous. The message only fires while nothing has been loaded at all, so it is always a startup ordering problem. In a browser bundle it means the adapter inlined nothing, see the client bundle section below.
Fix: Preload it with node --import @goenvless/env/register app.js (or Bun preload), or await load() before touching env. require('@goenvless/env/register') is refused outright, because it cannot block long enough to finish the load. A framework adapter (/next, /vite) awaits the load for you when your config is evaluated.
Missing ENVLESS_PROJECT. / Missing ENVLESS_ENV.
The full messages are Missing ENVLESS_PROJECT. Set it to your project slug, or add a .envless file. and Missing ENVLESS_ENV. Set it to your environment slug, or add a .envless file. The loader reads ENVLESS_PROJECT / ENVLESS_ENV first, then falls back to the .envless file in the process working directory. It does not walk up the tree, so a package that starts in its own directory never sees a .envless at the repo root. ENVLESS_PRODUCT is optional and follows the same order.
Fix: Run envless link in the directory the process starts from (writes .envless), or set ENVLESS_PROJECT and ENVLESS_ENV to the slugs.
Missing ENVLESS_TOKEN. Set it to an Envless machine key (ev_sk_…) to load variables.
No key was found. The runtime reads ENVLESS_TOKEN first, then the token stored in ~/.envless/config.json, and it authenticates with a machine key, not a browser session. A session token from envless login gets you the neighbouring message instead: ENVLESS_TOKEN must be a machine key (ev_sk_…). Create one in the Envless dashboard.
Fix: Set ENVLESS_TOKEN=ev_sk_... from the dashboard Keys page.
Set ENVLESS_KEY (a workspace key) or ENVLESS_PASSPHRASE to decrypt in machine mode.
The token authenticates the fetch, but the server only returns ciphertext. The loader has no key to decrypt with. The Node loader takes both from the environment and nowhere else: the workspace key the CLI caches in ~/.envless/config.json is never used by the runtime. @goenvless/env/edge is the exception, it reads no environment at all and takes { key } or { passphrase } as arguments to its load().
Fix: Set ENVLESS_KEY (base64url raw 256-bit key from Settings → /settings/ci-access) or ENVLESS_PASSPHRASE alongside the token. ENVLESS_KEY wins when both are set. See Passphrase.
Could not decrypt <NAME>. Check ENVLESS_KEY or ENVLESS_PASSPHRASE.
Ciphertext arrived but the decryption credential is wrong, usually because the workspace passphrase was rotated after the key/passphrase was captured. The name in the message is the first variable that failed, and the load throws rather than handing back a half decrypted environment.
Fix: Refresh ENVLESS_KEY / ENVLESS_PASSPHRASE to the current value (on a dev machine, envless passphrase set).
Missing env var: <name>
You accessed env.<name> and the loaded environment has no variable under that name. The proxy throws instead of returning undefined, so a read that came back undefined did not come from here.
Fix:
- Check the variable name for typos, and confirm it exists in the environment the process is actually bound to.
- If the read is genuinely optional, import
optionalinstead ofenv:import { env, optional } from '@goenvless/env/server', and the same export exists on@goenvless/env/client.optional.NAMEreturnsundefinedfor a name that isn't loaded rather than throwing, and it is typedPartial<EnvlessServerEnv>so TypeScript still makes you handle the empty case.'NAME' in envalso answersfalsefor such a name if you prefer to guard. - A
defaulton the variable will not fill it in. The loader keeps only the name, value, type and visibility of each variable, so--defaultand--requirednever reach the runtime and nothing is checked for you at boot.
<name> is server only. Import it from '@goenvless/env/server'
You read a server-visibility variable through @goenvless/env/client. The client entry only exposes variables marked client, and it hides the rest completely: they are absent from in, from Object.keys(env) and from JSON.stringify(env).
Fix: Import it from @goenvless/env/server, which is Node only, because it reaches node:fs for the .envless file and the on-disk cache. On an edge runtime import @goenvless/env/edge instead and pass { token, project, environment } plus { key } or { passphrase } to its load(). Or flip the variable's visibility to client in the dashboard if it's genuinely safe to ship.
Refusing to serialise the Envless env, because it holds decrypted secrets…
The full message is Refusing to serialise the Envless env, because it holds decrypted secrets and this would copy every one of them into the output. Read the names you need, or call load() for a plain record you own. Something reached for toJSON on the env proxy, which is what JSON.stringify does first. Response.json(env), res.send(env), a structured logger writing the object, and a snapshot test serialising it all land here.
Only the unfiltered proxies refuse: the env from @goenvless/env/server and the one from the root @goenvless/env. The env from @goenvless/env/client still serialises normally, and gives back its client-visibility variables alone, so a client bundle that stringifies env is unaffected.
Fix: Serialise the values, not the proxy.
- Read the names you need and build the object yourself,
{ region: env.AWS_REGION }. - Or use the record
load()resolves with. It is a plainRecord<string, string>that you own, with no guard on it, soJSON.stringifyworks.@goenvless/env/edgeonly ever hands you that record, so nothing there refuses either. - Spreading and enumerating still work, so
{ ...env }andObject.keys(env)are not blocked. Both copy every decrypted secret out, which is the thing the refusal exists to stop, so treat them the same way.
Pass { project } and { environment }. and the other @goenvless/env/edge messages
@goenvless/env/edge reads no files and no environment variables, so its own errors name a missing argument to load(options), never a missing env var:
Pass a machine key as { token }. An edge runtime has no .envless file and no config to read one from.Pass { project } and { environment }. An edge runtime has no .envless file to resolve them from.Pass { passphrase } or { key }. Without one the values cannot be decrypted.The workspace could not be resolved, so the passphrase cannot be turned into a key. Pass { workspaceId } explicitly.Only{ passphrase }needs the workspace, since the salt is derived from the workspace id.{ key }is imported raw and resolves no workspace at all.Could not decrypt <NAME>, with no trailing hint, because the credential came in as an argument.
Fix: Pass the missing option. The full shape is { token, project, environment, product?, passphrase? | key?, workspaceId?, endpoint?, fetch? }. Its load() resolves with a plain record: it writes nothing into process.env, exports no env proxy and keeps no cache, so nothing on this page about process.env, env.X or the disk cache applies to it.
A bundle fails on node:fs
Five of the nine entrypoints reach node:fs, so none of them can be bundled for an edge runtime or a browser: ., /server, /next, /vite and /register. The root entry is the trap, because it re-exports encrypt, decrypt and passphraseStrength alongside the loader, so importing a crypto helper from @goenvless/env drags the filesystem in with it.
Fix: Take crypto from @goenvless/env/crypto, which is pure Web Crypto with no Node built-ins and no Buffer, and exports encrypt, decrypt, encryptWithKey, decryptWithKey, workspaceKey, importWorkspaceKey, passphraseStrength and ENCRYPTION_PARAMETERS. Load variables through @goenvless/env/edge, and read them in browser code through @goenvless/env/client. Those three, plus /eslint, are the four entries that pull in nothing from Node.
TypeScript can't find env.X
envless types writes envless-env.d.ts into the directory you run it from. That file augments NodeJS.ProcessEnv, where every name is typed string, and two module names: @goenvless/env/server (EnvlessServerEnv) and @goenvless/env/client (EnvlessClientEnv, which gets the client-visibility names alone, and is left out of the file entirely when no variable is marked client). So a missing env.X can only come from those two entries, and it means the file is missing, stale, or outside your tsconfig includes. The env exported from the root @goenvless/env carries a string index signature, is never generated into, and accepts any name you type at it.
Fix: Import env from @goenvless/env/server or @goenvless/env/client, then run envless types from your package root and commit the result. Re-run it whenever a variable is added or renamed.
env.X is not the value you stored
env coerces on read, using the variable's type. A number runs the raw string through Number(), so abc reads as NaN and an empty value reads as 0. A boolean is true only for exactly true or 1, and false for everything else, casing and padding included. A string, and any type the runtime does not recognise, is handed back untouched. The record load() resolves with is never coerced.
Fix: Set the type that matches the value, envless var set PORT=3000 --type number, or --type string to get the raw text back.
process.env.X doesn't match env.X
Two rules cover every case:
- The loader never overwrites a name that already had a value in
process.envbefore it ran. The ambient value stays, the store still holds the Envless value, and the two disagree.load({ revalidate: true })still refuses to touch that name. - A variable whose name would change how the process or its children execute code is decrypted and returned, but deliberately never written into
process.env:NODE_OPTIONS,NODE_PATH,NODE_REPL_EXTERNAL_MODULE,ELECTRON_RUN_AS_NODE,LD_PRELOAD,LD_LIBRARY_PATH,LD_AUDIT,DYLD_INSERT_LIBRARIES,DYLD_LIBRARY_PATH,DYLD_FRAMEWORK_PATH,PATH,BUN_INSPECT,BUN_INSPECT_CONNECT_TO,RUBYOPT,RUBYLIB,BUNDLE_GEMFILE,GEM_HOME,GEM_PATH,PYTHONPATH,PYTHONHOME,PYTHONSTARTUP,PYTHONUSERBASE,PHPRC,PHP_INI_SCAN_DIR,ENVLESS_CLIENT_ENV,ENVLESS_INJECTED. The loader warns on the console and names them.
Fix: Read those through env, or from the record load() resolves with. There is no option that forces them into process.env.
"I'm getting server values in my client bundle"
The bundler plugin isn't running. Check that:
- Next: you wrapped your config with
withEnvless. - Vite: the
envless()plugin is in thepluginsarray. - You're importing from
@goenvless/env/client(not/server) in client code.
Only client-visibility variables are inlined either way. A server variable whose name starts with NEXT_PUBLIC_ or VITE_ would be inlined by the bundler itself whatever Envless marks it, so the adapter deletes it from process.env before the build and warns.
"Hot reload doesn't pick up dashboard changes"
Nothing watches the dashboard. The adapters call the loader once, when Next evaluates your config or when Vite runs the plugin's config hook, and load() memoises its promise for the life of the process. HMR re-runs your modules, not the loader.
Fix: Restart the dev server. Within 15 minutes of the last load the on-disk cache still counts as fresh, so a plain restart can hand back the same values. Run envless cache clear first, from @goenvless/cli. That cache is the runtime's: @goenvless/env writes a file under ~/.envless/cache after every successful fetch and reads it back in place of the next one, and the CLI command only deletes those files.
"Build fails in CI but works locally"
ENVLESS_KEY and ENVLESS_PASSPHRASE reach the Node loader through the environment and nowhere else, and @goenvless/env/edge takes them as { key } or { passphrase } arguments instead. Locally the CLI can fall back to the workspace key cached in ~/.envless/config.json, which is why envless sync and envless run keep working there. The runtime has no such fallback, in CI or anywhere else, and the ciphertext cache under ~/.envless/cache cannot stand in for a credential.
Fix: In the CI environment, set ENVLESS_TOKEN (an ev_sk_ key) and a decryption credential, either ENVLESS_KEY or ENVLESS_PASSPHRASE. Set ENVLESS_ENV if .envless binds more than one environment, since the loader takes the first one listed.
.env.<slug> is not ignored by git, so nothing was written.
This one comes from the envless binary in @goenvless/cli, not from the runtime. You passed --require-gitignore to envless sync and git reports that at least one file sync was about to write is tracked.
The refusal is all or nothing, and it happens before a single variable is fetched or decrypted. Every target environment is checked first, one unignored file stops the whole run, the message names each offending .env.<slug>, no file is written for any environment, and the command exits 1. It does not skip that environment and sync the rest.
Fix: Add every named file to .gitignore and run it again. If one is already committed, git rm --cached .env.<slug> before you do. Only a definite "not ignored" from git trips the refusal, so an inconclusive answer (no git binary, or a directory that is not a repository) lets the run proceed.
"I rotated a variable but my running app still has the old value"
load() memoises its promise per process, so every later call gets the values the first one resolved with. Underneath, the fetch is skipped entirely while the cached copy in ~/.envless/cache is under 15 minutes old. That copy is written by @goenvless/env itself, on every successful fetch. No envless command fills it, envless cache clear only empties it. The load() on @goenvless/env/edge is a different function with neither a memo nor a disk cache, so it refetches on every call.
Fix: Restart the process, or call load({ revalidate: true }) from a signal handler. That skips both the memo and the disk cache, refetches, and rewrites the store along with the process.env names the loader itself put there. A variable deleted server side disappears from both. envless cache clear, from @goenvless/cli, is the outside-the-process half of this: it empties the disk cache so the next process fetches fresh, but it cannot reach into a running process to clear the memo, so it is a companion to the restart rather than a substitute for it.
Still stuck?
Open an issue at the Envless repo or reach support from the dashboard.