One evening, before pushing a preview build, I had the EAS dashboard open and I was counting environment variables by eye. Three environments — development, preview, production — with project scope and account scope layered on top. After a few round trips through the interface, I had lost track of which ones I had already checked.
So I tried to stop counting by hand, and typed eas env:list --json. It did not work.
The first thing I want to say is that this is not a typo on your side. eas env:list has no --json flag. If you have stopped at the same place, here is what to use instead, and how to handle the output without quietly breaking something.
eas env:list accepts five flags
These are the flags defined in the eas-cli v22.0.0 source, in packages/eas-cli/src/commands/env/list.ts.
| Flag | Default | What it does |
|---|---|---|
--environment | (prompted) | Target environment. Can be passed more than once |
--format | short | Output format. The only choices are long and short |
--scope | project | Project-wide or account-wide variables |
--include-sensitive | false | Print sensitive values instead of masking them |
--include-file-content | false | Print the contents of file variables |
You can also pass the environment as a plain argument, as in eas env:list production. The format choices are long and short, and JSON is not among them, so --json is simply treated as a flag that does not exist.
I took a longer route than I needed to here. Looking at the dashboard table, I assumed there had to be a JSON export, and I guessed at three option names before I checked the flag list. Reading static override flags in the source first would have taken me a minute.
Why --format short is not a .env file
--format short prints one NAME=value per line, which looks close enough to a .env file that reusing it feels safe. Three things get in the way.
- A heading line for the environment comes first, in the shape of
Environment: production - An environment with no variables returns the single sentence
No variables found for this environment. - Variable names are printed in bold, so depending on the terminal you may get styling control characters mixed in
The harder problem is that some values cannot be read at all. A variable whose visibility is sensitive stays masked unless you pass --include-sensitive, and a secret variable never leaves the EAS servers. Write a mask back as if it were a value, and the mask overwrites the real thing.
How the visibility levels relate to the EXPO_PUBLIC_ prefix is a separate axis, and I have written that up in EAS secrets are not a "keep it out of the app" setting. For this piece, one fact is enough: readable and unreadable values arrive in exactly the same line shape.
About thirty lines to turn it into JSON
Drop the heading lines, strip the control characters, and split on the first = only. Anything that is nothing but a mask gets readable: false, so the record keeps the fact that it is a marker rather than a value.
#!/usr/bin/env python3
# eas env:list --format short --environment production | python3 eas_env_to_json.py production
import json
import re
import sys
ANSI = re.compile(r"\x1b\[[0-9;]*m")
HEADER = re.compile(r"^(Environment|Variables for this project|Account-wide variables)\b")
MASKED = re.compile(r"^\*+(\s*\(.*\))?$")
def parse(lines):
out = []
for raw in lines:
line = ANSI.sub("", raw).rstrip("\n").strip()
if not line or HEADER.match(line):
continue
if line.startswith("No variables found"):
continue
if "=" not in line:
continue
name, value = line.split("=", 1) # keep values that contain "="
name = name.strip()
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
continue # drop styling leftovers and prose lines
out.append({
"name": name,
"value": value,
"readable": not MASKED.match(value.strip()),
})
return out
if __name__ == "__main__":
env = sys.argv[1] if len(sys.argv) > 1 else None
json.dump(
{"environment": env, "variables": parse(sys.stdin.readlines())},
sys.stdout, ensure_ascii=False, indent=2,
)
print()Here is the result of running it against input that reproduces the short format emitted by the v22.0.0 source.
{
"environment": "production",
"variables": [
{ "name": "APP_VARIANT", "value": "production", "readable": true },
{ "name": "EXPO_PUBLIC_API_URL", "value": "https://api.example.com", "readable": true },
{ "name": "SENTRY_AUTH_TOKEN", "value": "*****", "readable": false }
]
}An environment with no variables produces an empty variables array rather than an error. A Base64 value with = inside it survives, because only the first = is used as the separator.
Line up three environments and the gap shows in one row
Generate one JSON file per environment, then line them up.
#!/usr/bin/env python3
# python3 eas_env_matrix.py env-development.json env-preview.json env-production.json
import json
import sys
envs, table = [], {}
for path in sys.argv[1:]:
with open(path, encoding="utf-8") as f:
data = json.load(f)
env = data.get("environment") or path
envs.append(env)
for v in data["variables"]:
table.setdefault(v["name"], {})[env] = "secret" if not v["readable"] else "ok"
missing = 0
print("NAME".ljust(28) + "".join(e.ljust(14) for e in envs))
for name in sorted(table):
row = [table[name].get(e, "-") for e in envs]
missing += row.count("-")
print(name.ljust(28) + "".join(c.ljust(14) for c in row))
print(f"\nmissing cells: {missing}")
sys.exit(1 if missing else 0) # fail the job when something is missingRunning it locally gives this.
NAME development preview production
APP_VARIANT ok ok ok
EXPO_PUBLIC_API_URL ok ok ok
SENTRY_AUTH_TOKEN - secret secret
missing cells: 1The - is the gap. Since I moved to this shape, I no longer bounce between dashboard tabs. It exits with 1, so dropping it into a pre-build job also stops a preview from going out with a variable that was never added.
What I had confused was "cannot read" with "not there"
For a while I counted masked rows as variables I had not created yet. I went to add one that I thought was missing, and nearly overwrote the production value with a fresh one. The mechanism was not at fault. My reading of it was.
A mask means the value cannot be read, not that the value is absent. That one distinction I check first, even on days when I am in a hurry.
The same care applies to eas env:pull --environment production. It writes a .env.local file, but secret variables come out as commented lines rather than values. Judge by the shape of the line alone and you may recreate a value that is sitting safely on the server already.
This kind of inventory also pays off before an SDK bump. I traced where native configuration quietly disappears in Before upgrading to Expo SDK 57, find the native changes prebuild will erase, if that is where you are heading next.
For now, dump production to JSON once, stamp it with a date, and keep it. Diff it against the same output next week, and you will have your own record of what was added and when. That is where I started.