env-trace never reads .env files. It derives which environment variables a TypeScript/JavaScript codebase actually uses from static analysis of the source AST, so it works even in legacy repositories where a .env never existed and nobody remembers what the app needs to boot.
The source of truth is the code, not a populated env file. env-trace does not sync or generate .env.example by reading secrets out of .env.
env-trace never reads .env, .env.local, or any file that may contain real secret values. It only reads source code and .env.example (or .env.sample / .env.template). There is no network activity, no telemetry, and no backend.
This is enforced by an allowlist in packages/core/src/exclusions.ts — not a denylist. Any basename outside {.env.example, .env.sample, .env.template} is rejected before readFile.
One-off via npx:
npx env-trace scanGlobal:
npm install -g env-traceProject dependency:
npm install --save-dev env-trace
# pnpm add -D env-trace
# yarn add -D env-traceCursor — .cursor/mcp.json:
{
"mcpServers": {
"env-trace": {
"command": "npx",
"args": ["-y", "env-trace-mcp"]
}
}
}Claude Code (stdio servers):
claude mcp add --transport stdio env-trace -- npx -y env-trace-mcpClaude Desktop — claude_desktop_config.json:
{
"mcpServers": {
"env-trace": {
"command": "npx",
"args": ["-y", "env-trace-mcp"]
}
}
}Programmatic:
import { scanProject } from "env-trace-core";
const result = await scanProject({ cwd: process.cwd() });
console.log(result.variables.map((v) => v.name));From this repo before the first npm publish:
pnpm install
pnpm build
node packages/cli/dist/cli.js scan
node packages/cli/dist/mcp.jsnpx env-trace diffLists names used in source but missing from .env.example, and names documented but not found in code. Exit code is non-zero when anything is missing from the example file.
Global flags (all commands): --cwd <path>, --json, --config <path>.
The console examples below were captured from this repo after pnpm build:
node packages/cli/dist/cli.js <command> --cwd packages/core/test/fixtures/appUse packages/core/test/fixtures/synced for the in-sync diff case. After publish, replace the node …/cli.js prefix with npx env-trace or a global env-trace.
List every detected usage (name, file, line, default, kind). Detects process.env.X, process.env["X"], destructuring from process.env, import.meta.env.VITE_* (Vite/Astro; kind meta), Zod z.object({...}).parse(process.env) (best-effort), and Nest ConfigService.get / getOrThrow with a literal key (best-effort). Vite built-ins on import.meta.env (MODE, BASE_URL, DEV, PROD, SSR) are ignored.
npx env-trace scan┌────────────────┬────────────────────┬──────┬───────────┬─────────────┐
│ Name │ File │ Line │ Default │ Kind │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ API_KEY │ src/config.ts │ 1 │ │ element │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ AWS_REGION │ src/config.ts │ 2 │ us-east-1 │ element │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ DATABASE_URL │ src/db.ts │ 1 │ │ property │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ LOG_LEVEL │ src/destructure.ts │ 1 │ info │ destructure │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ REDIS_URL │ src/destructure.ts │ 1 │ │ destructure │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ SESSION_SECRET │ src/destructure.ts │ 1 │ │ destructure │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ ZOD_REQUIRED │ src/env.ts │ 4 │ │ zod │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ ZOD_OPTIONAL │ src/env.ts │ 5 │ from-zod │ zod │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ ZOD_CHAINED │ src/env.ts │ 12 │ │ zod │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ PORT │ src/server.ts │ 1 │ 3000 │ property │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ HOST │ src/server.ts │ 2 │ localhost │ property │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ FLAG │ src/server.ts │ 3 │ │ property │
├────────────────┼────────────────────┼──────┼───────────┼─────────────┤
│ OTHER_FLAG │ src/server.ts │ 3 │ │ property │
└────────────────┴────────────────────┴──────┴───────────┴─────────────┘
13 variable(s), 13 usage(s)
--json prints the ScanResult object (filePath values are absolute):
npx env-trace scan --json{
"usages": [
{
"name": "DATABASE_URL",
"filePath": "/repo/src/index.ts",
"line": 1,
"hasDefault": false,
"kind": "property"
},
{
"name": "PORT",
"filePath": "/repo/src/index.ts",
"line": 2,
"hasDefault": true,
"defaultValue": "3000",
"kind": "property"
}
],
"variables": [
{
"name": "DATABASE_URL",
"usages": [
{
"name": "DATABASE_URL",
"filePath": "/repo/src/index.ts",
"line": 1,
"hasDefault": false,
"kind": "property"
}
],
"hasDefaultOnAllUsages": false,
"firstSeenFile": "/repo/src/index.ts"
},
{
"name": "PORT",
"usages": [
{
"name": "PORT",
"filePath": "/repo/src/index.ts",
"line": 2,
"hasDefault": true,
"defaultValue": "3000",
"kind": "property"
}
],
"hasDefaultOnAllUsages": true,
"firstSeenFile": "/repo/src/index.ts"
}
]
}Compare source usage to .env.example / .env.sample / .env.template.
| Flag | Meaning |
|---|---|
--ci |
One-line output (ok or missing N, unused M) |
--json |
DiffResult object |
Exit code 1 if missingInExample.length > 0. Unused names in the example file are reported but do not fail the process.
npx env-trace diffExample file: .env.example
missingInExample (11)
API_KEY src/config.ts:1
AWS_REGION src/config.ts:2
FLAG src/server.ts:3
HOST src/server.ts:2
LOG_LEVEL src/destructure.ts:1
OTHER_FLAG src/server.ts:3
REDIS_URL src/destructure.ts:1
SESSION_SECRET src/destructure.ts:1
ZOD_CHAINED src/env.ts:12
ZOD_OPTIONAL src/env.ts:5
ZOD_REQUIRED src/env.ts:4
unusedInExample (1)
DEAD_VAR
When source and the example file match:
Example file: .env.example
In sync: source usages match the example file.
--ci is meant for pipelines:
npx env-trace diff --cimissing 11, unused 1
ok
--json prints DiffResult:
{
"missingInExample": [
{
"name": "API_KEY",
"references": [{ "filePath": "/repo/src/config.ts", "line": 1 }]
}
],
"unusedInExample": ["DEAD_VAR"],
"exampleFile": "/repo/.env.example"
}Write .env.example from source. Refuses to overwrite unless --force. --json prints { content, fileName } (same shape as the MCP tool); the write is a CLI side effect.
npx env-trace generateWrote .env.example
If the file already exists:
Refusing to overwrite .env.example. Pass --force to replace it.
npx env-trace generate --forceWrote .env.example
Generated content groups names by the first file that referenced them, uses a literal default when one was found, and <REQUIRED> otherwise:
# src/index.ts
DATABASE_URL=<REQUIRED>
PORT=3000
Write env.d.ts. process.env usages become declare namespace NodeJS { interface ProcessEnv { ... } }. import.meta.env usages become Vite-style ImportMetaEnv / ImportMeta (with /// <reference types="vite/client" />). A mixed project gets both. A variable is optional (?:) only if every usage in source had a literal default.
npx env-trace typesWrote env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
PORT?: string;
}
}Vite / import.meta.env:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL?: string;
readonly VITE_APP_NAME: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}The README image links above expect PNG captures in docs/images/. Do not commit mock-ups — capture a real terminal (dark theme, full command + output visible).
| File | Capture |
|---|---|
docs/images/scan.png |
scan table with cyan headers and the N variable(s), M usage(s) footer. The screenshot already taken against school-trips-backend is the right one. |
docs/images/diff.png |
diff on packages/core/test/fixtures/app — red missingInExample list and yellow unusedInExample. |
docs/images/generate.png |
Wrote .env.example (run generate in a directory that has no example file yet). |
docs/images/types.png |
Wrote env.d.ts. |
| Tool | Description | Agent sees |
|---|---|---|
scan_env_usage |
Full list of detected usages |
ScanResult JSON (usages, variables) |
diff_env_example |
Source vs example-file drift |
DiffResult JSON (missingInExample with file+line, unusedInExample, exampleFile) |
generate_env_example |
Generated example file contents |
{ content, fileName } JSON. Does not write to disk. |
--json on the CLI is the same payload as the corresponding tool. Both call toJson in env-trace-core.
Input: { "cwd": "<optional project root>" }. Defaults to CLAUDE_PROJECT_DIR, then process.cwd().
name: env-trace
on: [push, pull_request]
jobs:
diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "24"
- run: npx env-trace diff --ciThe job fails when source references a variable that is not in .env.example.
CI on main and pull requests runs typecheck, ESLint, Prettier, tests, and build (see .github/workflows/ci.yml).
Publishing to npm is manual and gated:
- Bump
"version"inpackages/core/package.jsonandpackages/cli/package.jsonto the same value (for example0.1.0). - Commit, then tag and push that version:
git tag v0.1.0 && git push origin v0.1.0. -
.github/workflows/publish.ymlre-runs CI, then waits on the GitHub EnvironmentProd. - Approve the pending deployment in the Actions UI. Only then are
env-trace-coreandenv-tracepublished via OIDC (@env-trace/mcp-serverstays private). NoNPM_TOKENis used.
One-time GitHub / npm setup:
- Repo Settings → Environments →
Prod. Required reviewers: your user. Uncheck Allow administrators to bypass configured protection rules (otherwise admins skip the review). Optionally restrict deployments to tagsv*. - On npmjs.com, add a Trusted Publisher (GitHub Actions) for each of
env-trace-coreandenv-trace. If the package does not exist yet, add the trusted publisher for a new package name. Fields must match exactly:- Organization or user:
mytheondev(the GitHub owner of this repo, not the npm username) - Repository:
env-trace - Workflow filename:
publish.yml - Environment name:
Prod - Allowed actions:
npm publish
- Organization or user:
- npm does not validate these fields when you save them. A mismatch only shows up at publish time. After a successful publish you can set Publishing access to Require 2FA and disallow tokens and delete the
NPM_TOKENsecret fromProdif it is still there.
Optional env-trace.config.json in the project root:
{
"include": ["**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}"],
"exclude": [],
"exampleFile": ".env.example"
}| Field | Default | Notes |
|---|---|---|
include |
**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts} |
Source globs, relative to --cwd
|
exclude |
[] |
Additional globs. Cannot re-enable node_modules, dist, build, .next, .turbo, *.d.ts, or other hardcoded exclusions. |
exampleFile |
.env.example |
Must be .env.example, .env.sample, or .env.template. Any other name is rejected. |
Hardcoded scan exclusions (not configurable): node_modules/, dist/, build/, .next/, .turbo/, out/, coverage/, .output/, .nuxt/, .svelte-kit/, .vercel/, .cache/, .git/, and *.d.ts. Implemented in the scanner itself — tsconfig include/exclude is ignored.
- Does not read
.env,.env.local,.env.development,.env.production, or.env.*.local. - Does not scrub secrets or detect provider key patterns. It will not turn a populated
.envinto a scrubbed.env.example. - Does not support languages other than TypeScript/JavaScript.
- Does not watch files, call the network, or phone home.
- VS Code extension with inline diagnostics
- Multi-repo / org-level scan via GitHub API
- Opt-in comparison of names only (never values) against real
.env.*files — needs its own security design; not built speculatively



