Environment variables are always strings. That one fact causes a whole family of quiet bugs:
DEBUG = os.environ.get("DEBUG", False) # "false" in .env -> DEBUG is the string "false"
if DEBUG: # ... which is truthy, so debug mode is ON
...
workers = int(os.getenv("WORKERS")) # TypeError the day WORKERS is not setif (process.env.FEATURE_BETA) { ... } // FEATURE_BETA=false still enables the feature
if (process.env.FLAG === true) { ... } // never true: FLAG is a string or undefined
const next = process.env.PORT + 1; // "3000" + 1 is "30001"People hit this constantly (Django forum: "env variables can never seem to be false", python-dotenv issue #291), and the usual linters do not flag it. EnvTruth does, in Python and JavaScript/TypeScript, and a Quick Fix rewrites each one into a correct parse.
- Problems and squiggles on the exact expression, with a message that explains what really
happens (
"false", "0" and "no" are all truthy,"3000" + 1 is "30001",raises TypeError when WORKERS is unset). - Quick Fixes (lightbulb) with a safe rewrite for each finding.
- Activity bar: an EnvTruth icon opens the Env checks view. Findings are grouped by
file, and each row names the rule and the variable (for example
ET001 DEBUGwith "string used as boolean"), so rows are easy to tell apart. Click a row to jump to it, or use the inline lightbulb button to apply its Quick Fix. A badge on the icon shows the number of findings. - Hover on any env read to see where that variable is defined in the workspace (
.env,.env.*,*.env, docker composeenvironment:, Kubernetesenv:, workflowenv:blocks) and its value there. Only boolean-looking and short numeric values are shown. Everything else (keys, URLs, tokens, long numbers) is masked asmasked (N chars). - Status bar:
EnvTruth: N. Click it to open the view.
Open files are checked as you type. The rest of the workspace is scanned in the background.
| ID | Rule | Default | Example | Quick Fix |
|---|---|---|---|---|
| ET001 | Env string used as a boolean by truthiness | Warning | if os.getenv("DEBUG"):, bool(os.environ.get("FEATURE_X")), not os.getenv("VERBOSE"), if (process.env.DEBUG), !!process.env.FLAG, Boolean(process.env.FLAG), process.env.X ? a : b, process.env.DEBUG && log(), import.meta.env.VITE_DEBUG |
os.getenv("DEBUG", "").strip().lower() in ("1", "true", "yes", "on") / ["1", "true", "yes", "on"].includes((process.env.DEBUG ?? "").trim().toLowerCase()) |
| ET002 | Env value compared to a boolean literal | Error | process.env.FLAG === true (always false), os.getenv("DEBUG") == True, is False |
The same boolean parse (negated for == False, !== true) |
| ET003 | Env value used as a number without parsing | Warning | process.env.PORT + 1, process.env.TIMEOUT > 5000, process.env.PORT === 8080, os.getenv("PORT") + 1, os.getenv("WORKERS") > 4 |
Number(process.env.PORT ?? "0") / int(os.getenv("PORT", "0")) (float for float literals; an existing default is kept) |
| ET004 | Conversion that crashes when unset | Warning | int(os.getenv("WORKERS")), float(os.environ.get("RATIO")), json.loads(os.getenv("CFG")), JSON.parse(process.env.CFG) |
Add a default: int(os.getenv("WORKERS", "0")), JSON.parse(process.env.CFG ?? "null") |
| ET005 | Non-string default | Warning | os.getenv("DEBUG", False), os.environ.get("RETRIES", 3): a str when set, a bool/int when unset |
int(os.environ.get("RETRIES", "3")), or a boolean parse for True/False defaults |
When one expression triggers ET005 together with another rule (if os.getenv("DEBUG", False):), only the more specific
finding is shown, and its fix handles the default too.
A truthiness check is only a bug when the variable is meant to be a flag. if not os.getenv("API_KEY"): raise
is an intended presence check and is fine. EnvTruth reports a truthiness check when:
- the name looks like a flag:
DEBUG,VERBOSE,DRY_RUN,ENABLE*,DISABLE*,FEATURE*,USE_*,IS_*,HAS_*,ALLOW_*,SKIP_*,NO_*,FORCE_*,*_ENABLED,*_DISABLED,*_FLAG,*_ON,*_OFF,*_DEBUG(framework prefixes likeVITE_,NEXT_PUBLIC_,REACT_APP_are ignored), or - the code passes a boolean default (
os.getenv("X", False)), or - the variable is set to
true/false/yes/no/on/off/0/1in any.env, compose or Kubernetes file in the workspace.
Names built from the words in envTruth.presenceNames are treated as presence checks and never
reported: by default KEY, TOKEN, SECRET, PASSWORD, PASSWD, URL, URI, HOST,
DSN, PATH, DIR, FILE, ENDPOINT and NO_COLOR. A word matches a whole name
(NO_COLOR) or the start or end of any _-separated part (API_KEY, APIKEY,
REDIS_HOSTNAME). A name that also looks like a flag (USE_API_KEY, KEYCLOAK_ENABLED) is still
checked, unless it equals a listed word exactly.
Also never reported:
- Vite's built-in booleans
import.meta.env.DEV,PRODandSSR. - Anything inside comments and strings (including template literals, f-strings and docstrings).
- Reads that are already parsed or compared as strings:
os.getenv("DEBUG", "").lower() == "true",process.env.DEBUG === "true",os.getenv("X", "").strip().lower() in (...),Number(process.env.PORT). - Value defaults such as
os.getenv("REGION") or "us-east-1"andprocess.env.PORT || 3000.
| Setting | Default | Description |
|---|---|---|
envTruth.enabled |
true |
Turn all checks on or off. |
envTruth.rules |
{} |
Severity per rule (error, warning, information, hint) or off, e.g. { "ET002": "error", "ET005": "off" }. |
envTruth.presenceNames |
see above | Words that mark a variable as a presence check (ET001 only). Setting this replaces the default list. |
envTruth.maxFiles |
2000 |
Maximum number of source files scanned in the workspace. Files open in an editor are always checked. Files over 1 MB are skipped. |
envTruth.exclude |
node_modules, .git, dist, build, out, vendor, venv, .venv, __pycache__, .next, coverage, site-packages |
Glob patterns that are never scanned. node_modules, .git, dist, build, vendor, venv and .venv are always excluded. |
Settings apply immediately, without a reload.
- EnvTruth: Rescan Workspace (also the refresh button in the view title)
- EnvTruth: Show Env Checks
- EnvTruth: Open Settings (gear button in the view title)
- Apply Quick Fix (inline button on each fixable row)
Python (.py, .pyw) and JavaScript / TypeScript (.js, .jsx, .mjs, .cjs, .ts, .tsx,
.mts, .cts). Recognised reads: os.getenv(...), os.environ.get(...), os.environ[...]
(plus getenv / environ imported with from os import ...), process.env.X,
process.env["X"], import.meta.env.X.
- No network, no telemetry.
- EnvTruth only reads files inside the workspace, through VS Code's file APIs. Symlinks that lead outside the workspace are ignored, and files over 1 MB are skipped.
- It never runs processes, so it works fully in untrusted workspaces.
- Values from
.envand deployment files stay on your machine. The hover only shows values that look boolean or are short numbers.
- The analysis is per expression and does not follow data flow:
DEBUG = os.getenv("DEBUG")followed later byif DEBUG:is not linked (DEBUG = os.getenv("DEBUG", False)andDEBUG = bool(os.getenv("DEBUG"))are reported). Destructured reads such asconst { DEBUG } = process.envare not tracked either. - Only string-literal variable names are recognised (
os.getenv(name)with a variable is skipped), andosimported under another name is not followed. - ET003 needs a number literal on the other side of the operator (
process.env.PORT + offsetis not checked). - ET004 does not look for a surrounding
try/exceptortry/catch. - The YAML reader understands the common compose and Kubernetes
envshapes (list, map,name/value,valueFrom, one-line{name, value}), not the full YAML spec. Anchors and Helm templates are not expanded. .envfiles and YAML files are re-read when they are saved in the editor or change on disk. If your system's file watcher limit is exhausted, use Rescan Workspace after changing files outside the editor.
MIT - included with the extension.