EnvTruth: Env Var Boolean and Number Checker
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 set
if (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.
What you see
- 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 DEBUG with
"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 compose environment:, Kubernetes env:, workflow env: blocks)
and its value there. Only boolean-looking and short numeric values are shown. Everything
else (keys, URLs, tokens, long numbers) is masked as masked (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.
Rules
| 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.
What ET001 reports, and what it leaves alone
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 like VITE_, 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/1 in 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, PROD and SSR.
- 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" and process.env.PORT || 3000.
Settings
| 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.
Commands
- 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)
Languages
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.
Privacy and safety
- 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
.env and deployment files stay on your machine. The hover only shows values that look boolean or are short numbers.
Limitations
- The analysis is per expression and does not follow data flow:
DEBUG = os.getenv("DEBUG")
followed later by if DEBUG: is not linked (DEBUG = os.getenv("DEBUG", False) and
DEBUG = bool(os.getenv("DEBUG")) are reported). Destructured reads such as
const { DEBUG } = process.env are not tracked either.
- Only string-literal variable names are recognised (
os.getenv(name) with a variable is skipped),
and os imported under another name is not followed.
- ET003 needs a number literal on the other side of the operator (
process.env.PORT + offset is not checked).
- ET004 does not look for a surrounding
try/except or try/catch.
- The YAML reader understands the common compose and Kubernetes
env shapes (list, map, name/value,
valueFrom, one-line {name, value}), not the full YAML spec. Anchors and Helm templates are not expanded.
.env files 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.
License
MIT - included with the extension.
| |