Configuration
Configure the env key in miso.json as an array of entries — one per app or .env file. Each entry defines its own path and validation rules, so variables are always checked against the file they belong to.
{
"env": [
{
"scope": "web",
"label": "web",
"path": "apps/web/.env.local",
"required": "all",
"variables": {
"PORT": "port",
"DATABASE_URL": "url",
"NODE_ENV": {
"type": "enum",
"values": ["development", "production", "test"],
"optional": true
}
}
},
{
"scope": "api",
"label": "api",
"path": "apps/api/.env",
"required": "all",
"variables": {
"REDIS_URL": {
"type": "url",
"schemes": ["redis", "rediss"]
}
}
}
]
}Miso validates every entry and reports all the failures together, each grouped under its label — you see the whole list in one pass rather than fixing one, rerunning, and hitting the next.
scope
Which target this entry applies to: a workspace member name, a configured task/script name, or the reserved value "global".
Required on root miso.json entries. miso env rejects any root entry with no scope:
env config invalid — every root entry needs a scope (a target name or "global")
.env.localMember miso.json files (nested at <member>/miso.json) don’t set scope at all — they scope by file location. Every entry inside a member’s own config applies only to that member.
One scope, one file
A scope belongs to the root config or to the member’s own config, never both. Declaring a root entry scoped to a member that also declares its own env fails:
env scope declared in two places — keep it in the root config or the member's, not both
web — root miso.json and apps/web/miso.jsonPick whichever fits: root when you want every schema in one place, the member’s config when the app owns its own. Splitting one scope across both is how the two copies drift apart — a variable added to one and not the other goes unvalidated.
Resolution order
When miso resolves the env for a run target T, it layers, later winning on key conflict:
- Root entries scoped to
"global" - Either root entries scoped to
T(or, for a member target, to the member’s directory basename), or — whenTis a member declaring its own env — that member’s<member>/miso.jsonentries
The merged result is gap-filled under the ambient shell environment — a variable already set in your shell is never overridden, at any layer.
"global" is reserved: it can’t be used as a workspace, task, or script name, so it never collides with a real target.
Validation vs. injection
scope is enforced differently depending on what’s using it:
- Validation (
miso env,--env) is strict — every root entry must declare ascope, or the command fails. - Injection (loading
.envvalues into a running script) is permissive — an entry whosescopedoesn’t match the current target is silently skipped, no error. This lets a rootmiso.jsoncarry entries for members that aren’t relevant to the command you’re currently running.
scope itself is a miso-mode requirement: a delegated repo (repo: "turbo" or "nx") resolves targets through the delegate, so root entries there don’t need one and "global" carries no reserved meaning. Validation still covers the whole repo in every mode — see Delegated repos.
Delegated repos
In a delegated repo, ownership splits by job:
- Validation always traverses the whole repo.
miso envand--envcheck every workspace member’smiso.jsonschema alongside the root’s, in every repo mode. A clean exit means the whole repo’s declared variables are present — that’s the point of a preflight gate. - Injection follows orchestration, not repo mode. When turbo runs the task, turbo owns its env and miso injects nothing. When a
repo.tasks.<name>override hands the task to miso, miso spawns the processes and injects the resolved env for each one — the same as miso mode.
label
An optional name for this entry, used in log output and error messages. If omitted, Miso falls back to the entry’s path value.
Using a label is recommended in any project with more than one env file — it makes errors immediately actionable. Failures print grouped under the label, one variable per line:
api
REDIS_URL: missing required variablepath
The path to the .env file for this entry, relative to the project root.
If omitted, Miso falls back to the same discovery order used when no env config is present at all:
.env.local.env.production.env.development.env
override
Path to this scope’s override file, relative to the project root (or the member’s directory for a member-local entry). Used by miso env -g -o: its values are layered on top of the baseline values (populated by -p when given), winning on key conflict. One override file per scope — at the root level for "global", or one per member entry.
See Env generation for the full --generate workflow.
required
Controls which variables in this entry must be present:
"all"
Default
Every variable must be present unless marked optional: true on that variable.
"none"
No variables are required — only type-check those that exist.
string[]
Only the listed keys are required; others may be absent.
variables
Defines the variables to validate for this entry. Accepts either an object (type validation) or an array (presence only).
Object form — each key is a variable name, each value is a type shorthand string or full VarConfig:
{
"variables": {
"PORT": "port",
"DATABASE_URL": "url",
"APP_NAME": {
"type": "string",
"optional": true
}
}
}Array form — only checks that the listed variables exist, no type validation:
{
"variables": ["DATABASE_URL", "API_KEY", "REDIS_URL"]
}Use the array form when you only care about presence (e.g. in CI) and don’t need format validation.
Single-app projects
If you only have one .env file, you still use an array — just with a single entry:
{
"env": [
{
"scope": "global",
"path": ".env.local",
"variables": {
"PORT": "port",
"DATABASE_URL": "url"
}
}
]
}