Skip to content
grove
Esc
↑↓navigate↵open⌘Jpreview
On this page

Environment layering

The order grove builds a command's environment in, and why grove's own values win over your shell while .env.local wins over everything.

grove exec builds the environment in a deliberate order. Later tiers win.

  1. Trust variables. NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, and DENO_CERT, pointed at grove’s root or a bundle carrying it, for runtimes that ignore the OS store. A variable you already set is left alone.
  2. env_files. Read in order, later files winning. Every value here yields to anything already in your shell: an inline override is the most deliberate thing in the chain, and a checked-in file the least. A missing file is skipped, since .env is usually gitignored and its absence is normal.
  3. GROVE_CONTEXT, and GROVE_PORT, GROVE_HOST, GROVE_URL for the bound entry.
  4. Grove’s resolved values. Everything [env] and the active entry’s env block name, with templates filled in. These win over your shell, because a route naming a port it did not lease would be a lie, and a port hand-copied into a checked-in .env is the drift grove exists to remove.
  5. .env.local. The one tier above grove’s own, and it yields to nothing.

Why .env.local doesn’t yield to the shell

If .env.local yielded to the shell while grove’s values did not, exporting a name would hand it back to grove and the file’s line would silently stop applying. So it yields to nothing, and grove tells you on stderr every name it took over.

It cannot set GROVE_CONTEXT, GROVE_PORT, GROVE_HOST, or GROVE_URL. Grove fills those from the lease it holds, and an override would have grove name a route or a port it doesn’t serve, so it’s refused rather than believed.

A project that lists .env.local in its own env_files has already said where that file goes, and it stays in that lower tier. An upgrade must not change what a file already in use means.

.env.local isn’t a grove.toml key because Next, Vite, CRA and Rails dotenv all already treat it as the personal override, and their gitignore templates already carry it. grove doctor warns when it’s tracked by git, since a shared override hides the values grove leased from everyone.

Where a variable sits decides when it’s set

[env] is set for every command in the project. An env block on a route is only set while that route is bound, so a URL belongs in [env] even though it describes the route: a build needs it, and a build binds nothing. An env block on a detached entry is always set, exactly like [env].

Reporting rather than allocating

grove env prints this same environment from the leases that exist right now. A variable naming a port nobody holds is left out and named on stderr, since printing a number nothing routes to would be the lie the layering exists to prevent. grove exec --no-bind is the same for a command.

Last updated on September 22, 2026

Was this page helpful?