Guard · Env config
Find the env var nobody declared before you deploy
RepoOps compares the environment variables your code reads with the ones your env files declare, and reports the gaps with the file, the line and the fix. It reads names, never values, and it changes nothing.
For: the person who deploys a repository an agent helped write, and the owner who keeps its env var registry
What it does, and why it helps
The Env config tab has two parts. The tab itself renders the repository's own registry, the file .claude/brain/config.md, where each variable lists what reads it and what happens when it is missing. The Doctor subtab runs the scan. It lists the repository's tracked files and the untracked files git does not ignore, reads every text file under the size bound, finds each env var read (a dotted or bracketed read of the process env object or the import.meta env object, or a Deno env get), parses the KEY= lines of every .env family file, and runs five checks over the two sets.
An env var your code reads is never declared (High): read without an inline default, in no env file, and not a runtime variable a platform sets. A var in your .env isn't in .env.example (High): declared in a real env file and read in code, but absent from the example. No .env.example, but your code reads env vars (Heads-up): one finding in place of the per-variable one. A hard-coded localhost URL will break in prod (Heads-up): a quoted loopback base URL in application code. A var in .env.example is never used (Heads-up). Each finding carries up to 25 evidence rows as path:line and a fix you can paste. A clean result says none of these checks fired in the files scanned, and the banner says so in those words. The scan does not check the values set on your deploy host.
The pain. A variable the code reads was set once on one machine. Nothing declares it. A fresh clone boots with it undefined, and production finds out at runtime.
The point of view. Read the declaration gap, not the deploy log. What the code reads and what the env files declare are both in the repository, so the mismatch can be found before the deploy, by name, without ever reading a value.
What gets easier. Knowing what to add to .env.example before you push. Each finding names the variable, the first file and line that reads it without a default, and the exact lines to add.
When it helps. Before a deploy of any tracked repository that is a git working tree, and after an agent session that added a variable read or a hard-coded URL.
Its limits. It reads what git can list: tracked files plus untracked files that git does not ignore, so a gitignored .env is outside the scan, whatever the module comment says about it. Names in docs, HTML, JSON, tests, examples, git hooks, CI workflows and the docs directory are skipped on purpose, and so are flag names ending in _OFF, _DISABLE, _SKIP, _ENABLE or _DEBUG. It stops at 4,000 files or 24 MB and says the scan was truncated. It never checks a value.
Understand it in 30 seconds
Read the narration
- 0:00 It works on your machine.
- 0:02 Prod boots with a variable nobody declared.
- 0:06 Compare what the code reads with what the env files declare.
- 0:10 Names, never values.
- 0:13 Each finding names the file and line, the variable, and the fix.
- 0:17 A clean result means these checks did not fire.
- 0:23 Fix the declaration before the deploy.
- 0:25 The guide covers the five checks.
Synthetic example. Read the guide
Where to find it
Where to find it
- Desktop:
localhost:4000, then Local settings in the sidebar, then Env config under Workspace utilities. - Hosted: desktop only.
- Keyboard: ⌘ K, then type “Env config”.
When to use it
A fresh clone that boots half-configured
Situation. An agent session added a webhook handler that reads a new variable. The value is in the developer's local .env. Nothing else knows the variable exists.
What you do. Open Env config, then Doctor. Read the High finding. Run the three lines under How to fix: add the blank placeholder to .env.example, keep the real value in the local .env, set it on the deploy host. Press Re-scan.
What you see. The finding An env var your code reads is never declared, with the variable name in brackets and the first path:line that reads it without a default. After the re-scan the high pill reads 0 high.
What it establishes. The variable is declared where a fresh clone and a deploy can see it. The scan does not know whether the host has the value; only the host does.
A localhost URL an agent hard-coded
Situation. A generated client calls the API at a quoted http://localhost URL. It works in every local run.
What you do. Read the Heads-up finding A hard-coded localhost URL will break in prod. Move the base URL behind an env var with the loopback address as the dev default, as the fix shows, and add the new variable to .env.example.
What you see. The evidence row shows the file, the line and the clipped line of code. After the change the finding is gone and, if you forgot the example line, a new High finding names the variable you introduced.
What it establishes. The deploy host can point the client at the real host. The scan established that no quoted loopback URL remains in the files it read, and nothing more.
Before you start
- Supported versions
- RepoOps desktop v0.3.1, the release this guide was read against. The scan shells out to git, so git must be on the machine.
- Where it runs
- Local only. Local settings lists Env config and Doctor under Workspace utilities; the standalone page stays reachable at /env-doctor.html. The focused Today page no longer draws a Config / env doctor panel, and the focused sidebar has no Doctor badge. No hosted page renders these findings.
- Permissions
- None beyond running the desktop app. The route has no per-request check; the localhost bind is the trust boundary, the same posture as every local read.
- Connections
- A tracked repository whose path is a git working tree. A path that is not one returns the message not a git repository (sync the repo first). No account, no token, no network.
- Plan
- No plan gate. The pricing tier map (website/lib/pricing-tiers.ts) has no row for this feature.
Configure it
- Keep a .env.example at the repository root.
It is the shape the checks compare against: one KEY= line per variable the code reads, blank values. Without one the scan emits a single Heads-up, No .env.example, but your code reads env vars, and skips the per-variable missing-from-example check.
- Open Env config, then Doctor, on the repository.
The tab paints the last stored result at once and refreshes in place; the line under the buttons reads scanned with the time, and cached when the server served its stored copy. Nothing to set up: the scan runs on open.
- Read the pills, then the findings in order.
The three pills count critical, high and heads-up. Findings sort High before Heads-up; the scan emits no critical. Each card carries the title, a plain explanation, the evidence rows and How to fix.
- Press Re-scan after you edit code or an env file.
The server keeps the last result per repository until you ask for a fresh one or the app restarts. The button reads Scanning… while it runs and sends ?fresh=1 to the route.
- Document the variable in the registry.
The Env config tab renders the repository's .claude/brain/config.md. A row per variable, naming its reader and its missing behaviour, is what the tab shows; the scan finds the gap, the registry records the answer.
| Setting | Where | A sensible choice | Why it matters |
|---|---|---|---|
.env.example | the repository root | one KEY= line per variable the code reads, values blank | The example is the declared shape. A variable read in code and absent here is a High finding; one declared here and read nowhere is a Heads-up. |
Re-scan (?fresh=1) | Env config, Doctor, the Re-scan button | press it after every env file or code edit | The server stores the last result per repository (64 repositories, least recently used evicted) with no expiry; only a fresh request or a restart replaces it. |
REPOOPS_PREWARM | the data directory's .env | unset (on), or 0 to skip the boot warm | At boot the server fetches /api/env-doctor for each tracked repository so the first open is warm. With 0 the first open runs the scan itself. |
.claude/brain/config.md | the tracked repository's brain | a row per variable: who reads it, what happens when it is missing | The Env config tab renders this file. When it is absent the tab shows its zero state instead of a registry. |
CURSOR_ADMIN_KEY | the RepoOps data directory's .env | set only if you pull Cursor usage | The Aggregator vendor keys panel under the findings reads set or missing for it; the value is never returned or shown. |
COPILOT_METRICS_TOKEN + COPILOT_METRICS_ORG | the RepoOps data directory's .env | set both only if you pull Copilot metrics | Both must be present to read as set. The panel also shows the last vendor pull time when one has run. |
What you should see
A clean repository
Configuration. Every variable the code reads without a default is in .env.example, no quoted loopback URL outside tests, docs and launcher scripts, no stale example line.
Expect. The pills read 0 critical, 0 high, 0 heads-up and the green banner says none of the five config checks fired in the files scanned, and that the values on your deploy host were not checked.
Verify. The Today tab's Config / env doctor panel leads with No config/env footguns and the Doctor badge in the sidebar reads clean.
A High finding
Configuration. A variable read without a default, in no env file, not a platform variable, not an optional flag name.
Expect. One card titled An env var your code reads is never declared, with the variable in brackets after the first path:line that reads it, and a fix of three lines plus the inline-default alternative.
Verify. The high pill counts it on the Doctor page.
Not a git repository, or a large one
Configuration. A tracked path that is not a git working tree, or a repository over 4,000 readable files or 24 MB.
Expect. The first shows Couldn't scan: not a git repository (sync the repo first), with no findings. The second shows its findings and adds scan truncated (large repo) to the scanned line; files past the bound were not read.
Verify. The Today panel reads Env scan unavailable with the same error for the first. For the second the response carries truncated true and scanned with the count of files read.
Data and cost
- What is captured
- Per scan: the variable names read and declared, the path and line of the first undefaulted read per variable, up to 25 evidence rows per finding, and for a loopback URL the line of code clipped to 160 characters. Files are read into memory on this machine to find the names and are not stored.
- Who can see it
- Local only. Nothing about a finding is published to a team or a hosted page; the Env config leaf is desktop-only in the spine.
- How long it is kept
- No retention knob. The last result per repository lives in the server's memory until a fresh scan or a restart, in the app store as a last-known-good row (lib/api-cache-disk.mjs, keyed apilkg:, oldest rows evicted at the cap) so the tab paints after a restart, and in this browser's IndexedDB under env-doctor:<repo>.
- What leaves the machine
- None. The scan runs git and reads files on this machine; no model is called and no request leaves it. The vendor keys panel reads booleans from the local server.
- What it costs
- No model call and no metered spend. The scan is bounded at 4,000 files, 512 KB per file and 24 MB in total, and the route's response is cached for two minutes so a tab revisit does not re-run it.
When the result differs
| Symptom | Likely cause | Next action |
|---|---|---|
| Couldn't scan: not a git repository (sync the repo first). | The tracked path is not a git working tree, or the mirror has not synced yet. | Sync the repository, then press Re-scan. |
| A variable you set on the host reads as never declared. | The scan reads env files git can list. A gitignored .env is outside it, and the deploy host is never read. | Add the KEY= line to .env.example. The host value stays where it is. |
| A variable you know is unset is not reported. | Its name is a platform variable (NODE_ENV, PORT, CI, a VERCEL or GITHUB_ prefix), an optional flag name (_OFF, _DISABLE, _SKIP, _ENABLE, _DEBUG), or its read has an inline default. | Nothing to fix in the scan; declare it by hand if the registry should list it. |
| The finding is still there after you fixed it. | The tab painted the stored result; the server keeps the last scan until asked for a fresh one. | Press Re-scan. The line under the button drops cached. |
| scan truncated (large repo) on the scanned line. | The scan reached 4,000 files or 24 MB and stopped reading. | Read the findings for the files scanned; the response says how many. There is no setting that raises the bound. |
| The Env config tab shows a zero state instead of a registry. | The repository has no .claude/brain/config.md. | Add the file with a row per variable, its reader and its missing behaviour. The Doctor subtab works either way. |
| A vendor key row reads missing. | CURSOR_ADMIN_KEY, or one of COPILOT_METRICS_TOKEN and COPILOT_METRICS_ORG, is not in the RepoOps data directory's .env. | Set it there and restart the app. The panel reads set or missing only. |
- Disable
- Not provided as a switch. REPOOPS_PREWARM=0 stops the boot warm; the scan still runs when the Doctor subtab or Today opens. Untrack the repository to stop it for that repository.
- Roll back
- Nothing to roll back. The scan changes no file; the edits you make from a fix are ordinary edits in your own checkout, reverted in git.
- Revoke access
- No credential is involved in the scan. A vendor key is removed by deleting its line from the data directory's .env; RepoOps does not revoke it at the vendor.
- Delete
- Not provided. No route clears a stored result. A restart empties the in-memory copy; the disk copy is an apilkg: row in the app store (lib/api-cache-disk.mjs) that the cap evicts oldest first; the browser copy is the env-doctor:<repo> entry in this browser's IndexedDB.
Related tasks
Maintenance evidence
- Feature id
config(spine leafconfig)- Owner
- Vibe-coder mission control program (Config & env doctor), folded under Env config by the nav redesign N7 (PR #190). Guide: LDG-0717.
- Supported product version
- RepoOps v0.3.1
- Last verified
- 2026-09-15, read against origin/main at 52366bb6d; labels read from the served tab source (public/env-doctor.html), the finding titles and fix strings from lib/env-doctor.mjs, the Today panel text from lib/today.mjs, and the git listing flags from scanEnvDoctor. Not checked on a running instance.
- Example fixtures
- No fixture file; the inputs are inline in lib/env-doctor.test.mjs (twelve cases, including the no-self-flag scan of the scanner's own source), lib/today.test.mjs (the config-doctor panel) and lib/next-best-action.test.mjs (the env-high action).
- Source references
lib/env-doctor.mjs,lib/routes/env-doctor.mjs,lib/routes/mirror-files.mjs,lib/canonical-tabs.mjs,lib/zero-states.mjs,lib/security-guardrails.mjs,lib/api-cache.mjs,lib/api-cache-disk.mjs,lib/prewarm.mjs,lib/today.mjs,lib/next-best-action.mjs,lib/routes/vendor-usage.mjs,lib/vendor-usage/index.mjs,lib/cc-usage.mjs,public/env-doctor.html,public/dashboard.html- Documentation review
- Independent review requested on the slice pull request; not yet recorded.
- Video review
- Narrated story rendered and published 2026-09-26 (render 29c0894f9ec6, LDG-1018) with the breadcrumb Local settings, checked against main at 0f24215b5. One frame, the captions and the transcript were reviewed by the authoring agent, not an independent reviewer; the audio was not listened to by a person. Narration is the provisional Windows voice until LDG-0721.
Last updated