npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

env-xray

v0.1.0

Published

Find ghost variables, orphaned config, wrong defaults, and port conflicts in your .env files. Zero dependencies.

Readme

env-xray

CI

license zero dependencies

                                             
   ___ _ ____   __     __  ___ __ __ _ _   _ 
  / _ \ '_ \ \ / /____\ \/ / '__/ _` | | | |
 |  __/ | | \ V /______>  <| | | (_| | |_| |
  \___|_| |_|\_/      /_/\_\_|  \__,_|\__, |
                                      |___/ 

Map every environment variable from definition to consumption. Find what's broken before production does.

Zero dependencies. Works with .env, .envrc, docker-compose.yml, GitHub Actions, Makefiles, K8s manifests, and source code in 6 languages.

npx env-xray

Why This Exists

Every project accumulates environment variables like sediment (and to continue a metaphor, sometimes they contain fossils as well as like, broken pieces of glass that can cut your ass). Layers of .env, .env.defaults, .envrc, .env.local, docker-compose.yml, K8s manifests—each defining some variables, consuming others, and hoping they all agree.

They often don't.

I kept running into this at my day job—variables not matching across layers, services hitting dead endpoints because of port mismatches, cron endpoints unprotected because nobody defined the auth secret. I looked for a tool that could map everything end-to-end and tell me what's broken. Couldn't find one. So I built it.

I ran it against my own production monorepo and it found a service port conflict we'd missed for months—a fallback of http://localhost:8092 in the frontend, but the service ran on 9091. Three features silently hitting a dead endpoint. A one-line fix, but we didn't know it was broken.


What It Finds

| Check | What It Means | |-------|--------------| | Ghost (hard) | Variable consumed in code with no fallback and no definition. Will crash or use undefined. | | Ghost (soft) | Variable consumed with a fallback (\|\| "default") but never formally defined. Won't crash but isn't configured. | | Orphan | Variable defined in env files but never consumed by code. Dead config. | | Conflict | Variable defined in multiple files with different values. Which one wins? | | Stale default | Placeholder value like your-api-key-here that would cause silent failures. | | Duplicate | Same variable defined twice in the same file. Copy-paste error. | | Empty | Variable defined with empty value and consumed in code. Will be an empty string at runtime. |


Does It Actually Work?

I ran env-xray against 23 popular open-source projects across 8 languages. Every single one had findings.

| Project | Stack | Vars | Findings | Hard Ghosts | Soft Ghosts | Orphans | |---------|-------|------|----------|-------------|-------------|---------| | supabase | TS/Mixed | 591 | 469 | 258 | 51 | 119 | | posthog | Python/Django | 567 | 535 | 209 | 298 | 23 | | cal.com | TS/Next.js | 348 | 109 | 58 | 25 | 20 | | n8n | TS/Node | 252 | 244 | 128 | 75 | 0 | | mastodon | Ruby/Rails | 246 | 229 | 112 | 96 | 14 | | nocodb | TS/Node | 193 | 189 | 87 | 86 | 15 | | BookStack | PHP/Laravel | 173 | 15 | 0 | 0 | 15 | | twenty | TS/React | 171 | 122 | 43 | 35 | 33 | | medusa | TS/Node | 153 | 139 | 72 | 36 | 5 | | chatwoot | Ruby/Rails | 137 | 82 | 26 | 47 | 5 | | saleor | Python/Django | 118 | 111 | 31 | 80 | 0 | | loki | Go | 110 | 76 | 43 | 0 | 9 | | documenso | TS/Next.js | 106 | 55 | 12 | 1 | 42 | | immich | TS/Svelte | 94 | 82 | 60 | 11 | 11 | | paperless-ngx | Python/Django | 83 | 80 | 44 | 35 | 1 | | strapi | TS/Node | 80 | 64 | 35 | 29 | 0 | | appwrite | PHP/Mixed | 68 | 65 | 10 | 7 | 48 | | monica | PHP/Laravel | 63 | 43 | 0 | 0 | 32 | | maybe | TS/Next.js | 59 | 41 | 26 | 15 | 0 | | hoppscotch | TS/Vue | 55 | 44 | 33 | 4 | 4 | | traefik | Go | 17 | 14 | 7 | 5 | 1 | | minio | Go | 9 | 9 | 8 | 0 | 1 | | typst | Rust | 1 | 1 | 1 | 0 | 0 |

"Hard" ghosts have no fallback—they'll crash or silently use undefined. "Soft" ghosts have fallbacks like process.env.PORT || 3000—they won't crash but the variable is still never formally defined.

BookStack and Monica (both PHP/Laravel) had zero ghosts—all their env vars are properly defined. That's what clean looks like.

What it found

cal.comCRON_SECRET used in 6 files for auth but never defined in any env file. Cron endpoints silently accept any request. POSTGRES_PASSWORD defined with 3 different values across config files.

twentyTWENTY_APP_ACCESS_TOKEN referenced 16 times across 7 files, defined nowhere. 11 placeholder values like your_twenty_api_key_here and xxx.

documensoNEXTAUTH_SECRET defined as "secret" in two places and "${NEXTAUTH_SECRET:?err}" in a third. 42 orphaned variables.


Install

# Run without installing
npx env-xray

# Install globally
npm install -g env-xray

# Add to your project
npm install --save-dev env-xray

Usage

CLI

npx env-xray                           # Grouped summary (default)
npx env-xray --verbose                 # Full details for every finding
npx env-xray --md > report.md          # Markdown report
npx env-xray --json                    # JSON output
npx env-xray --ci --severity=error     # Fail CI on critical issues

| Flag | What it does | |------|-------------| | --verbose, -v | Show full details for every finding | | --json | JSON output (values masked) | | --markdown, --md | Markdown report | | --severity=error | Only show errors (hard ghosts, conflicts) | | --severity=warning | Show errors and warnings | | --ci | Exit code 1 if errors found (default: always exit 0) | | --init-ignore | Generate .env-xray-ignore from current findings | | --local-only | Skip findings from deploy/, .github/, k8s/ directories | | --exclude=<pattern> | Skip files/directories matching pattern (repeatable) | | --max-file-size=<bytes> | Skip files larger than this (default: 1MB) |

By default, env-xray shows a grouped summary—type breakdown, top findings by severity, then results organized by directory. Use --verbose for the full list.

CI / GitHub Action

Run env-xray on every PR:

name: Env Check
on: [pull_request]
jobs:
  env-xray:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: andymboyle/env-xray@main
        with:
          severity: error

Or as a one-liner in an existing workflow:

- name: Check env vars
  run: npx env-xray --ci --severity=error

Programmatic API

import { scan, formatTerminal, formatJSON } from 'env-xray';

const result = scan({
  rootDir: '/path/to/project',
  maxFileSize: 2_000_000,       // skip files over 2MB (default: 1MB)
  exclude: ['generated/'],      // skip directories/files
});

// result.findings      — array of Finding objects
// result.variables     — Map<string, Variable> of all discovered variables
// result.filesScanned  — { envFiles: string[], sourceFiles: string[] }
// result.skippedFiles  — files skipped due to size, symlinks, or --exclude

console.log(formatTerminal(result));  // colored terminal output
console.log(formatJSON(result));      // JSON (values masked)

How It Works

What it reads

env-xray scans your project in a single pass, categorizing every file it finds:

Variable references (consumption)—source code in 6 languages:

| Language | Patterns detected | |----------|------------------| | JavaScript/TypeScript | process.env.VAR, process.env["VAR"], import.meta.env.VAR, const { VAR } = process.env, Zod schemas, envalid | | Python | os.environ["VAR"], os.environ.get("VAR"), os.getenv("VAR") | | Ruby | ENV["VAR"], ENV.fetch("VAR") | | Go | os.Getenv("VAR"), struct tags (envconfig:"VAR", env:"VAR") | | Rust | env::var("VAR") | | PHP | getenv("VAR"), $_ENV["VAR"], $_SERVER["VAR"] |

Variable definitions (where values are set):

| File type | What's parsed | |-----------|--------------| | .env* files | KEY=value, export KEY=value, quoted values, inline comments | | Custom .env files | dev-services.env, celery-queues.env, etc. | | docker-compose.yml | environment: sections, env_file: references (followed and parsed) | | GitHub Actions | env: blocks at workflow, job, and step level | | Makefiles | export VAR=value, VAR ?= value, VAR := value | | Config files | config.yaml, config.json with ${VAR} interpolation | | K8s manifests | Deployment YAMLs in deploy/, k8s/, kubernetes/ directories |

What it filters

env-xray isn't just grep. It understands context:

  • .example and .template files are excluded from orphan, empty, stale, and conflict detection—they're documentation, not active config
  • ${VAR} interpolation in docker-compose is recognized as a reference, not a conflicting literal value
  • Platform variables (CI, NODE_ENV, VERCEL_URL, HOSTNAME, etc.) are excluded from ghost detection—they're set by the runtime, not .env files
  • Expected conflicts like PORT and DATABASE_URL are skipped—they're supposed to differ across services
  • Tooling directories (tools/, examples/, test/, fixtures/, sandbox/) are excluded from orphan detection
  • Fallback detection distinguishes process.env.PORT (hard ghost) from process.env.PORT || 3000 (soft ghost)
  • --local-only filters out deploy/, .github/, k8s/ findings when you only care about local dev
  • --exclude=<pattern> skips specific files or directories entirely

What it protects

  • Symlinks are skipped—prevents reading files outside your project root
  • Large files are skipped—files over 1MB (adjustable with --max-file-size) are ignored to prevent OOM on binary files or data dumps
  • Secret values are masked—all output shows "http***" instead of raw values, including --json

Ignore File

Create .env-xray-ignore in your project root to skip known false positives:

# These are set by the deployment platform, not in .env files
VERCEL_URL
RAILWAY_ENVIRONMENT
AWS_REGION

Or generate one automatically from your current findings:

npx env-xray --init-ignore

This creates the ignore file with all current variables, so future runs only catch new issues. Remove lines as you fix them to track progress.


Contributing

Found a false positive? A pattern env-xray misses? Open an issue with the example and we'll fix it.


License

MIT