~/env-editor ☀ LIGHT ☕ Support me apps ← about me
🔒 100% client-side · Your secrets NEVER leave this browser · No server · No logging · No storage
███████╗███╗   ██╗██╗   ██╗    ███████╗██████╗ ██╗████████╗ ██████╗ ██████╗ 
██╔════╝████╗  ██║██║   ██║    ██╔════╝██╔══██╗██║╚══██╔══╝██╔═══██╗██╔══██╗
█████╗  ██╔██╗ ██║██║   ██║    █████╗  ██║  ██║██║   ██║   ██║   ██║██████╔╝
██╔══╝  ██║╚██╗██║╚██╗ ██╔╝    ██╔══╝  ██║  ██║██║   ██║   ██║   ██║██╔══██╗
███████╗██║ ╚████║ ╚████╔╝     ███████╗██████╔╝██║   ██║   ╚██████╔╝██║  ██║
╚══════╝╚═╝  ╚═══╝  ╚═══╝      ╚══════╝╚═════╝ ╚═╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝

Env File Editor — edit, validate & convert .env files online

Several files at once · 39 lint rules with one-click fixes · 68 secret formats · Node / Python / Docker / Bash compatibility · 40 export formats
By Jasper Bernaers · Updated 2 October 2026 · no upload, no storage, no account
double-click a tab to rename · drop files anywhere
—
Secret & hygiene audit
Everything below is computed in this tab. Nothing is uploaded, and no value is ever sent anywhere.
◀ Environment A (e.g. .env.example)
▶ Environment B (e.g. .env.production)
Effective environment
Loader compatibility
Format
.env

Guide — from a messy .env to a clean, safe one

Five steps. Every edit is undoable, and nothing you paste leaves this tab.
1Open your files

Load .env file, drop one or several files anywhere on the page, paste, or start from a template. Each file gets its own tab above the editor. Try Load messy sample to see what gets caught.

2Fix the syntax

The sidebar lists every issue with its line. Click an issue to jump to it, press its fix button to repair just that line, or Fix N safe issues to clean up duplicates, stray spaces, missing quotes, BOM and CRLF in one go.

3Run the audit

The Audit tab grades the file from A to F and lists live-looking credentials, placeholder secrets and unsafe switches. Placeholders get a generate random value button; debug, TLS and http:// findings get a one-click fix.

4Check across files and loaders

Layers shows which value wins across .env, .env.local and friends. Compare diffs two environments. Compat shows how Node, Python, Docker and Bash each read the same file.

5Tidy up and export

Sort or Group by prefix, then download, copy, encrypt with a passphrase, or convert to one of 40 formats: Compose, Kubernetes, GitHub Actions, Azure, Zod and more.

ONE-CLICK FIXESWhat the fix buttons do

Only the lines involved are rewritten; the rest of the file stays exactly as it was.

keep last
deletes earlier copies of a duplicate key
remove space
around “=” and at the end of a line
add quotes
for values with spaces
close quote
ends an unterminated value
comment out
disarms a line without “=”
→ KEY
renames, updating ${KEY} references
AUDIT FIXESMake the file safer

Rotate anything real that was ever committed; these fixes only change the file.

placeholder
256 random bits from crypto.getRandomValues
debug on
switched off in production
TLS off
NODE_TLS_REJECT_UNAUTHORIZED=1
http://
becomes https://
in a comment
delete the commented-out credential
WHICH TABWhich tab answers which question

The .env text is the single source of truth for all of them.

Table
edit values in cells, see inferred types
Compare
what is missing in production?
Layers
which file's value wins?
Compat
why does Docker read it differently?
Export
I need it as YAML, JSON or a schema
KEYBOARDShortcuts

All of them work in the editor.

Ctrl/⌘ F
find and replace (keys only, if you like)
Ctrl/⌘ S
download the current file
Ctrl/⌘ /
comment or uncomment lines
Ctrl/⌘ Z
undo typing, clean-up and fixes
Ctrl/⌘ ⇧ M
mask values for screen sharing
F2
rename the file tab

Free online .env file editor, validator and secret scanner

Create, edit, lint, audit and convert .env (dotenv) files directly in your browser. Open one file or a whole set — .env, .env.local, .env.production — and you immediately get syntax highlighting with line numbers, a live validator with 39 rules, a secret scan that recognises 68 provider token formats, an editable table with inferred types, an environment diff with merge, the effective values across files, a side-by-side view of how five different loaders read the same file, and one-click conversion into 40 formats — from docker-compose, Kubernetes and Azure App Service to a Zod or Pydantic schema.

Everything runs 100% client-side. There is no back end, no upload, no account, and nothing you type is written to browser storage — close the tab and the content is gone. You can verify it yourself: open the network tab and watch it stay empty while you work.

Real .env parser

Handles export prefixes, single and double quotes, multi-line values, escape sequences and inline comments — not just a naive split on “=”.

39 lint rules

Duplicate keys, unterminated quotes, spaces around “=”, smart quotes and zero-width characters, invalid ports and URLs, BOM and CRLF, unresolved ${VAR} references.

Secret scan

68 token formats — AWS, GitHub, Stripe, OpenAI, Anthropic, Slack, Azure, Supabase and more — plus secrets in comments, placeholder and low-entropy secrets and JWT checks.

Browser exposure

Flags a secret behind NEXT_PUBLIC_, VITE_, REACT_APP_ or PUBLIC_ — values every visitor can read once the app is built.

Layers

Open several files and see which value wins, in Next.js, Vite or Docker Compose order, with the values it overrides.

Loader compatibility

The same file through npm dotenv, python-dotenv, Docker Compose, docker run --env-file and Bash — every disagreement highlighted.

40 export formats

Compose, Kubernetes, Helm, GitHub Actions, Azure, AWS, Vercel, Fly.io, shell, PowerShell, Zod, t3-env, envalid, Pydantic, JSON Schema, Go and more.

18 templates

Next.js, Vite, Laravel, Django, Rails, Supabase, Prisma, Strapi and more — with APP_KEY, SECRET_KEY and friends generated locally.

Encryption

Wrap a .env file in AES-256-GCM with a passphrase, so you can send it to a teammate without pasting secrets into chat in the clear.

Environment diff

Compare two files, see what is missing, added or different, copy the gaps across or build a merged file with the strategy you choose.

Why a .env file needs linting at all

The dotenv format looks trivial and is full of traps. A duplicate key silently wins over the earlier one. A space before the = makes docker run --env-file reject the whole file. An unquoted value with a # in it is cut short by Node's dotenv but not by Python's. A value copied from a web page carries typographic quotes or a zero-width space that nobody can see. A CRLF file sourced by an entrypoint script leaves a stray carriage return at the end of every value — the kind of bug that costs an afternoon. Every one of those is caught here the moment you paste the file, with a clickable line number.

How five loaders read the same .env file

There is no single .env standard, so the same line can mean different things depending on what loads it. This is what the Compat tab checks for your own file, modelled on the parsers' source code (try it with a messy sample):

In the fileNode dotenv 17python-dotenvDocker Composedocker run --env-fileBash source
KEY=a #noteaaaa #notea
KEY=a#baa#ba#ba#ba#b
Surrounding quotes✓ removed✓ removed✓ removed✗ kept in the value✓ removed
"\n" in double quotes✓ newline✓ newline✓ newline✗ literal, quotes kept~ literal \n
${VAR}✗ needs dotenv-expand✓ even in single quotes✓ not in single quotes✗ literal✓ not in single quotes
$VAR without braces✗ literal✗ literal✓ expanded✗ literal✓ expanded
Multi-line quoted value✓✓✓✗ breaks into lines✓
export KEY=…✓✓✓✗ rejects the file✓
KEY = value✓ trimmed✓ trimmed✓ trimmed✗ rejects the file✗ command not found
KEY=hello world✓ kept✓ kept✓ kept✓ kept✗ runs world
KEY: value✓✗ skipped✓✗ rejects the file✗ command not found
Line without =~ ignored silently~ key with no value~ unset~ taken from host env✗ command not found
CRLF line endings✓ stripped✓ stripped✓ stripped✓ stripped✗ \r kept

Which variables reach the browser

Front-end frameworks only expose variables with a public prefix, and they copy those values into the JavaScript that every visitor downloads. A secret behind one of these prefixes is a published secret — the audit marks it critical.

FrameworkPublic prefixWhat happens
Next.jsNEXT_PUBLIC_inlined into the bundle at next build
ViteVITE_exposed on import.meta.env; change it with envPrefix
Create React AppREACT_APP_embedded at build time
NuxtNUXT_PUBLIC_overrides runtimeConfig.public
SvelteKit / AstroPUBLIC_$env/static/public / import.meta.env
ExpoEXPO_PUBLIC_inlined into the JavaScript bundle
GatsbyGATSBY_available in browser code
Vue CLIVUE_APP_embedded at build time

Load order of .env, .env.local and .env.production

When several files define the same key, the framework decides which one wins. Open your files as tabs and the Layers tab applies these rules for you (see a four-file Next.js example):

ToolHighest priority → lowest
Next.jsprocess.env → .env.$(NODE_ENV).local → .env.local (not in test) → .env.$(NODE_ENV) → .env
Viteshell variables → .env.[mode].local → .env.[mode] → .env.local → .env
Docker Composeenvironment: → last env_file entry → … → first env_file entry

.env templates for popular frameworks

Each template uses the variable names the framework expects and generates values such as APP_KEY, SECRET_KEY or AUTH_SECRET in your browser. Open one directly:

What the audit looks for

Findings are graded from critical to low and rolled into a score, and the report can be copied as Markdown for a ticket or a pull request — with the key names but never the values.

Convert .env to anything

The same file becomes a docker-compose environment block, a Dockerfile, docker run -e flags, a Kubernetes ConfigMap, Secret or container env: list, Helm values, a GitHub Actions or GitLab CI block, gh secret set commands, an Azure App Service settings file, Azure Key Vault commands with the matching Key Vault references, an AWS ECS task definition or SSM Parameter Store commands, Vercel, Netlify, Fly.io and Heroku CLI scripts, a Cloudflare wrangler.toml, Terraform tfvars, shell, PowerShell, fish and .bat scripts, a systemd EnvironmentFile, a VS Code launch.json, a Markdown table for your README, CSV, JSON, YAML, Java .properties, a committable .env.example — or typed code: env.d.ts, a Zod schema, a t3-env createEnv, envalid, a Pydantic BaseSettings class, JSON Schema or a Go struct, so a missing variable fails at startup instead of at 3 a.m.

All 39 lint rules

Your secrets stay in the tab

Pasting production credentials into a random website is normally a bad idea, so it is worth being precise about what happens here: files are parsed by JavaScript already loaded in the page, held in memory, and rendered back to the screen. They are never sent anywhere, never written to localStorage, and never included in the reports you copy. Template links carry key names only, in the part of the URL a browser never sends to a server. The one thing this tool cannot protect you from is a secret that has already been committed — if git log --all -- .env returns anything, rotation is the only real fix.

One-click fixes

Can it fix the problems it finds?

Yes. Most issues in the sidebar have a small fix button, and so do the audit findings that a file edit can solve. Only the lines involved are rewritten — comments, blank lines and the formatting of every other line stay exactly as they were — and every fix can be undone with Ctrl/⌘+Z in the editor.

Fix N safe issues at the top of the list applies every fix that cannot change what a loader reads: duplicates, stray spaces, missing quotes, smart quotes, invisible characters, the BOM and CRLF line endings. Renames, closing quotes and commenting out are left to you, because they change meaning.

Which audit findings can be fixed with one click?

A placeholder, short, low-entropy or default secret gets generate random value: 256 bits from crypto.getRandomValues, written as 64 hex characters. Debug mode in production and other safety switches are flipped off, NODE_TLS_REJECT_UNAUTHORIZED=0 becomes 1, a plaintext http:// endpoint becomes https://, a reused secret gets fresh values for the other keys, and a credential in a comment can be deleted. A live API key is never “fixed” by editing the file: revoke and rotate it at the provider.

Using the editor

What does this tool do?

It is a full workbench for .env files: a syntax-highlighted editor that holds several files at once, a live validator with 39 rules, a secret audit that recognises 68 token formats, an editable table with inferred types, an environment diff with merge, a Layers view that shows the effective values across .env, .env.local and friends, a Compat view that shows how five different loaders read your file, 18 framework templates, password encryption, and a converter that turns the same file into 40 other formats. Everything happens in your browser — there is no server involved at any point.

How do I get my file in?

Several ways: Load .env file opens a file picker (you can pick several files at once), you can drag files anywhere onto the page, Paste from clipboard reads your clipboard if the browser allows it, you can type or paste into the editor, New from template starts from a framework preset, and Import JSON / YAML converts an existing config — including a Docker Compose environment: block, a Kubernetes env: list or an Azure App Service settings export. Files are read locally and never uploaded.

Can I work on several .env files at once?

Yes. The strip above the tabs holds as many files as you like — .env, .env.local, .env.production and so on. Press + for a new one, double-click a tab (or press F2) to rename it, and drop several files onto the page to open them together. Every tab — Table, Audit, Export — works on the file that is selected, while Layers and Compare work across files. They all live in memory only.

What is the “Load messy sample” button?

It loads a deliberately broken example — a duplicate key, a space before the =, an unterminated line, a live-looking Stripe key, a placeholder JWT secret, TLS verification switched off and an unresolved ${VAR} — and jumps to the Audit tab. It is the fastest way to see what the linter and the audit actually catch.

What are the templates?

New from template offers 18 starting points: Next.js, Vite, Node/Express, Laravel, Django, Rails, Symfony, Spring Boot, Supabase, Prisma, Auth.js, Stripe, Docker Postgres, WordPress, Strapi, n8n, an AI app and an Azure app. Each uses the variable names that framework expects, with comments on what is safe to expose. Values such as APP_KEY, SECRET_KEY, SECRET_KEY_BASE or AUTH_SECRET are generated in your browser with crypto.getRandomValues. You can also link straight to one, for example ?template=laravel.

What do the seven tabs do?

Are the tabs kept in sync?

Yes, in both directions. Editing a cell in the Table tab rewrites the .env text, and editing the text updates the table, the stats, the lint list and the audit. The .env text is the single source of truth — comments, blank lines and quote style survive a round trip through the table.

What do the numbers in the sidebar mean?

Live counts of variables, comment lines, duplicate keys, empty values, flagged secrets (keys with a critical or high audit finding) and total lines. Underneath, every syntax issue is listed: the first six, with show all for the rest, and the style notes folded away. Click an issue to jump to its line, or press its fix button.

Which keyboard shortcuts exist?

Ctrl/⌘+F opens find and replace, Ctrl/⌘+S downloads the current file, Ctrl/⌘+/ comments or uncomments the selected lines, Ctrl/⌘+Z undoes clean-up actions as well as typing, Ctrl/⌘+Shift+M toggles value masking, Tab inserts two spaces, F2 renames the focused file tab, and Esc closes the find bar or any dialog.

What does “Mask values” do?

It replaces every value with dots in the editor, the table, the compare results and the status bar, while leaving the real text untouched underneath. It is meant for screen sharing, pair programming and screenshots. The Export tab has its own separate redact values checkbox for when you want to hand the file to someone.

Can I search and replace across keys only?

Yes. Open Find, turn on Keys only, and replace runs against key names rather than the whole file — the safe way to rename a prefix such as OLD_APP_ to NEW_APP_ without touching any values that happen to contain the same text. Aa toggles case sensitivity.

How do the clean-up tools work?

What does “Generate secret” produce?

Three values from crypto.getRandomValues: a 256-bit hex string, the same 256 bits as base64, and a UUID. They are appended to the end of the file for you to rename and use. They are generated in your browser from the operating system's secure random source — not from a seeded pseudo-random generator, and not from a server.

Is my work saved if I reload?

No, deliberately. Nothing you type is written to localStorage, cookies or IndexedDB, because everything you paste here is by definition a secret. All open files live only in the memory of this tab. Reloading loses them — download, copy or encrypt before you leave.

Does it work offline?

Yes, once the page has loaded. The parser, the linter, the loader models, the secret patterns, the templates, encryption and every export format are part of the page. You can disconnect and keep working, which is a reasonable habit when handling production credentials.

Is there a size limit?

No hard limit. Above roughly 3,000 lines the syntax highlighting switches off automatically to keep typing responsive; everything else — linting, auditing, exports — keeps working normally.

.env syntax, quoting and the classic gotchas

What is a .env file?

A plain text file of KEY=VALUE lines that holds configuration outside your source code — database URLs, API keys, feature flags, ports. Loaders such as Node's dotenv, Python's python-dotenv, Ruby's dotenv, Laravel, Docker Compose and Vite read it at startup and put the values into the process environment.

Is there an official .env specification?

No, and that is the root of most confusion. Every loader implements its own dialect. They agree on KEY=VALUE, # comments and quoting, and disagree about interpolation, multi-line values, escape sequences, inline comments and whitespace. The Docker Compose env_file format is the most precisely documented one. The Compat tab runs your file through five loaders side by side, so you can see exactly where they disagree.

Why does the same file behave differently in Node, Python and Docker?

Because each loader has its own parser. Some examples the Compat tab will show you: KEY=alpha#beta is alpha in Node dotenv but alpha#beta everywhere else; ${VAR} is expanded by python-dotenv, Compose and Bash but not by plain Node dotenv; docker run --env-file keeps quotes as part of the value and rejects the whole file if a key has a space; KEY=hello world is fine for dotenv but runs a command called world when a shell sources the file; and python-dotenv expands ${VAR} even inside single quotes.

Do I need quotes around values?

Only when the value contains something ambiguous: a leading or trailing space, a #, a quote character or a line break. Everything else is fine unquoted. Fix quoting applies exactly that rule across the file.

What is the difference between single and double quotes?

In most loaders, double quotes expand escape sequences — "line1\nline2" becomes two lines — and often expand ${VAR} references too. Single quotes are literal: 'a\nb' stays as backslash-n. The editor decodes \n, \t, \r, \\ and \" inside double quotes and leaves single-quoted values alone.

Can a value span several lines?

Yes, if it is quoted — which is how people paste RSA private keys and certificates. The parser here follows the value across lines until the closing quote, keeps it as one variable, and marks the continuation lines in the gutter. If the closing quote is missing you get an unterminated quote error, because a real loader would swallow the rest of your file into that one value.

Why does my value get cut off at the #?

Because an unquoted # preceded by whitespace starts an inline comment. PASSWORD=abc#123 keeps the hash (no space in front), but PASSWORD=abc #123 gives you abc. Quote the value and the problem disappears.

Are spaces around the = allowed?

Shell syntax says no, and several loaders agree. KEY = value can end up with a key literally named "KEY " and a value of " value". The linter flags both sides separately, and Trim & normalise fixes them.

Does export KEY=value work?

Yes — the prefix exists so the file can also be sourced by a shell. The parser recognises and preserves it, colours it separately, and drops it in formats where it makes no sense, such as JSON or a Kubernetes manifest.

What characters can a key contain?

To be safe: a letter or underscore first, then letters, digits and underscores — the POSIX identifier rule. Anything else may load in Node but will break export, shell interpolation and several parsers. The linter warns on invalid identifiers and, more gently, on lower-case keys, since the universal convention is UPPER_SNAKE_CASE.

What does ${VAR} do inside a .env file?

Some loaders substitute another variable's value there. Support is inconsistent: dotenv alone does not expand, dotenv-expand, Docker Compose and Laravel do. This tool shows the resolved value under the field in the Table tab, warns when a reference points at a key that does not exist, catches self-references, and can bake the references into literal values with Resolve ${VAR} refs.

Does ${VAR:-default} work?

The editor understands the shell-style default syntax — ${PORT:-3000} resolves to 3000 when PORT is absent, and the missing key is not reported as an error. Whether your loader supports it is another matter; Docker Compose does, plain dotenv does not.

What happens if the same key appears twice?

Practically every loader keeps the last occurrence, so the earlier line is dead config that still looks alive during code review. Duplicates are counted in the sidebar, marked red in the table, listed with all their line numbers, and Remove duplicates deletes the earlier ones.

Why does my first variable read as undefined?

Often it is a UTF-8 BOM at the start of the file — usually added by a Windows editor. Node dotenv, python-dotenv, Docker and Compose cope with it, but a shell that sources the file sees \uFEFFDATABASE_URL=…, which is not a valid assignment at all. The linter flags a BOM, and any clean-up action or export writes the file without it.

Why do my values have a stray character at the end in Docker?

CRLF line endings. A file saved on Windows and sourced by an entrypoint script (set -a; . ./.env) leaves \r at the end of every value, so PORT becomes "3000\r" and the port parse fails. Docker's own --env-file and Compose strip the carriage return, which is why it only bites some setups. The linter reports CRLF endings, the Compat tab shows the ␍ where it survives, and anything you export from here is written with plain LF.

Can a .env file contain arrays or nested objects?

No — the format is flat strings only. The usual workaround is a delimiter (ALLOWED_HOSTS=a.com,b.com) or embedded JSON in a quoted value. When you import a nested JSON or YAML config here, the keys are flattened with underscores: {"db":{"host":"x"}} becomes DB_HOST=x.

Are values always strings?

Always. DEBUG=false arrives in your code as the string "false", which is truthy in JavaScript and Python alike — a bug that has shipped to production more times than anyone will admit. Parse and validate explicitly, or use a schema library such as zod or envalid.

Should I commit my .env file?

No. Add it to .gitignore and commit .env.example instead — same keys, no values — so a new developer knows what to fill in. The Export tab generates that file for you, keeping your comments and section headers intact.

Which variables end up in the browser?

Only the ones with the framework's public prefix, and those are copied into the JavaScript bundle at build time: NEXT_PUBLIC_ (Next.js), VITE_ (Vite, configurable with envPrefix), REACT_APP_ (Create React App), NUXT_PUBLIC_ (Nuxt), PUBLIC_ (SvelteKit and Astro), EXPO_PUBLIC_ (Expo), GATSBY_ and VUE_APP_. Anyone can read those values. The audit raises a critical finding when a secret sits behind one of these prefixes, and the Layers tab marks them.

In which order are .env, .env.local and .env.production loaded?

It depends on the framework. Next.js checks process.env, then .env.$(NODE_ENV).local, .env.local (skipped when NODE_ENV is test), .env.$(NODE_ENV) and finally .env, stopping at the first hit. Vite loads .env, .env.local, .env.[mode] and .env.[mode].local, with later files winning and shell variables beating all of them. Docker Compose reads a list of env_file entries top-down and the last one wins. Open the files as tabs and the Layers tab computes the result for you.

Validation and lint rules

What does the validator check?

39 rules across three severities. Errors: duplicate keys, invalid lines, unterminated quotes. Warnings: spaces around the =, invalid identifiers, unquoted values containing spaces, text after a closing quote, unresolved or self-referencing ${VAR}, a BOM, invisible zero-width characters, typographic quotes and non-breaking spaces pasted from the web, backtick quotes, Windows paths in double quotes, a trailing backslash, invalid port numbers and URLs, an unusual NODE_ENV, overriding system variables such as PATH or NODE_OPTIONS, keys that differ only in case, an unquoted multi-line key and YAML-style lines. Notes: lower-case keys, empty values, long values or keys, CRLF endings, URLs without a scheme, e-mail shape, mixed boolean styles, $VAR without braces, tabs, spaces inside quotes, trailing whitespace and the export prefix. The full list is under the FAQ.

How do I jump to a problem line?

Click the issue in the sidebar. The editor scrolls to that line and selects it. Problem lines are also marked in the gutter — red for errors and any critical or high audit finding, yellow for warnings.

Why is an empty value only a note and not an error?

Because it is often intentional — OPTIONAL_FEATURE_URL= is a legitimate way to say “not configured”. It is worth seeing at a glance, though, because an empty value and a missing key behave differently: the first gives your code an empty string, the second gives undefined.

What is an “invalid line”?

A non-empty, non-comment line with no =, or one that starts with =. Most loaders skip these silently, so a typo like DATABASE_URL postgres://… simply produces no variable at all and you find out at runtime.

Why warn about unquoted values with spaces?

Because behaviour diverges. Some parsers keep the whole rest of the line, some stop at the first space, and a shell sourcing the file will try to run the second word as a command. Quoting removes the ambiguity entirely.

What counts as an unresolved reference?

A ${VAR} or $VAR pointing at a key that is not defined anywhere in the same file and has no :-default. It may still resolve at runtime from the real environment, so it is a warning rather than an error — but it is the most common cause of a value that comes out as the literal text ${DB_HOST}.

Does it check whether values are valid?

Where the key name makes the intent clear, yes: a *_PORT must be a number from 1 to 65535, a *_URL must parse as a URL, an *_EMAIL must look like an address, and NODE_ENV should be development, production or test. It cannot know whether your database URL points at a database that exists. The Table tab also shows the type it infers for every key, which is what the Zod, envalid, Pydantic and JSON Schema exports are built from.

Can it fix problems automatically?

The mechanical ones, yes: remove duplicates, trim whitespace, fix quoting, uppercase key names, resolve references, sort or group. Anything that requires a decision — which of two duplicate values is right, whether a secret should be rotated — is reported and left to you.

What is the difference between the linter and the audit?

The linter is about syntax and hygiene: will this file load correctly. The audit is about security: is there a live credential in it, is a secret a placeholder, is a protection switched off. A file can be perfectly valid and still fail the audit badly.

What is the Compat tab?

It runs the file through faithful ports of five loaders — npm dotenv 17, python-dotenv, Docker Compose's env_file parser, the Docker CLI's --env-file parser, and a model of set -a; source .env in Bash — and shows every variable where they disagree, which loaders reject the file outright, and which lines each one skips. Invisible differences are made visible: · for a leading or trailing space, ⏎ for a newline, ␍ for a carriage return. The host environment is treated as empty.

The secret audit

What does the Audit tab actually do?

It runs every value against more than forty provider token formats, a placeholder-secret list, an entropy calculation and a set of configuration rules, then grades what it finds from critical to low and turns that into a score out of 100 with a letter grade. All of it in your browser — no value is ever sent anywhere, and the values are not even included in the report you copy.

Which credential formats does it recognise?

68 formats, matched by shape: AWS access keys, GitHub tokens (ghp_, gho_, github_pat_), GitLab PATs, Slack tokens, app tokens and webhooks, Stripe live and test keys and webhook secrets, OpenAI, Anthropic, OpenRouter, Groq, xAI, Perplexity, Replicate and Hugging Face keys, Google API keys and OAuth client secrets, Microsoft Entra ID client secrets, Azure storage keys and SAS tokens, Supabase secret keys and service-role JWTs, SendGrid, Mailgun, Mailchimp, Resend, Twilio, npm, PyPI, DigitalOcean, Shopify, Square, Discord tokens and webhooks, Telegram, Linear, Notion, Postman, Atlassian, Figma, Grafana, New Relic, Sentry DSNs and auth tokens, Doppler, Databricks, PlanetScale, Tailscale, Terraform Cloud, Pulumi, Mapbox secret tokens, Dropbox, Fly.io, Meta, Heroku, Cloudflare, Cloudinary, Algolia, Datadog, age secret keys, JSON Web Tokens, SSH keys, PEM and PuTTY private keys, and credentials embedded in a connection URL.

Why does it distinguish Stripe live from test keys?

Because the consequences are completely different. sk_test_… in a repository is untidy; sk_live_… is a payment incident. The same logic runs throughout: a Twilio account SID is medium, a Twilio API key is critical.

Why is a NEXT_PUBLIC_ or VITE_ variable flagged as critical?

Because it is not a secret any more once the app is built. Those prefixes tell the framework to inline the value into client-side JavaScript, so a NEXT_PUBLIC_STRIPE_SECRET_KEY or a VITE_SUPABASE_SERVICE_ROLE_KEY is readable by every visitor. Keys that are public by design — publishable keys, Supabase anon keys, Firebase web config, analytics IDs — are not reported as exposed.

What is a “placeholder secret”?

A value like changeme, secret, password, admin or abc123 sitting in a key named *_SECRET, *_TOKEN or *_PASSWORD. It is flagged high because these survive from the first day of a project into staging and sometimes into production, where they are effectively public knowledge.

What does the entropy check do?

It measures the Shannon entropy of a value in bits per character. A secret-named key holding a 16-character value with under 2.6 bits per character is repetitive or dictionary-like rather than random, and is flagged. The reverse check also runs: a very high-entropy random string in a key that is not named like a secret is flagged low, because keys named CLIENT_CONFIG sometimes hold real credentials.

What does it check inside JSON Web Tokens?

It decodes the payload (never the signature, and nothing is sent anywhere) and reports tokens whose role is service_role — a Supabase key that bypasses Row Level Security — tokens that have already expired, and tokens without an exp claim, which stay valid until the signing key is rotated.

Why is it warning about a value I know is fine?

Detection is by shape, so a 32-character hex build hash can look exactly like an API key. Findings are signals to check, not verdicts — that is why every finding names the rule that produced it and links to the line. A short low-entropy value in a key called APP_SECRET deserves the flag even if you know the app is a toy.

What configuration problems does it detect?

NODE_TLS_REJECT_UNAUTHORIZED=0, other disabled TLS, certificate, CSRF or signature checks, DISABLE_* / SKIP_* safety switches, debug mode in production, wildcard CORS origins and allowed hosts, plaintext http:// endpoints, well-known default database passwords, secrets behind a browser-exposed prefix such as NEXT_PUBLIC_ or VITE_, LD_PRELOAD and NODE_OPTIONS=--require code-loading tricks, and JSON Web Tokens that have expired or never expire.

How is the score calculated?

It starts at 100 and subtracts a weight per finding — 34 for critical, 16 for high, 7 for medium, 2 for low — with the result mapped to a letter: A from 90, B from 75, C from 55, D from 35, F below that. It is a rough prioritisation aid, not a compliance measure: a single critical finding drops you to a D on purpose.

Can I share the audit result?

Copy report puts a Markdown table on your clipboard with the severity, key name, line number and finding for each issue. Values are never included, so the report is safe to paste into a ticket or a pull request.

Does a clean audit mean my secrets are safe?

No. It means nothing in this file matched a known pattern or an unsafe setting. It cannot tell you whether the file is in .gitignore, whether it was committed six months ago, whether the same key is pasted in a Slack thread, or whether the token has the right scopes. Those checks are on you.

I found a real key in my file. What now?

Rotate it first, before cleaning anything up — a key that has been exposed stays exposed, and deleting the line does not un-expose it. Then check git history with git log --all --full-history -- .env; if it appears there, rewriting history is not enough on its own either. Rotate, then clean.

Does it detect secrets in comments?

Yes. A commented-out #OLD_API_KEY=sk_live_… line is exactly as exposed as an active one, so comment lines are run through the same token patterns and reported as Credential in a comment. The template link leaves such comment lines out automatically.

Converting and exporting

Which formats can I export to?

40, in five groups. Files: .env, .env.example, JSON, YAML, CSV, a Markdown variables table and Java .properties. Containers & CI: Docker Compose, Dockerfile ENV, docker run -e, Kubernetes ConfigMap, Secret and container env:, Helm values, GitHub Actions, gh secret set and GitLab CI. Cloud & hosting: Azure App Service, Azure Key Vault, AWS ECS, AWS SSM Parameter Store, Vercel, Netlify, Fly.io, Heroku, Cloudflare Workers and Terraform tfvars. Shell & runtime: bash, PowerShell, fish, a Windows .bat, systemd and a VS Code launch.json. Code & schema: TypeScript env.d.ts, Zod, t3-env, envalid, Pydantic Settings, JSON Schema and a Go struct.

How does the Export tab work?

Pick a format on the left and the preview on the right updates immediately from whatever is in the editor. Copy puts it on your clipboard, Download saves it with a sensible filename and extension. Switching format never re-reads or changes your source file.

What is the redact checkbox for?

It replaces every value with ******** in the exported output while keeping keys, comments and structure intact. Use it when you need to show someone the shape of your configuration — in a ticket, a document or a code review — without handing over the contents.

How is .env.example generated?

Every key is kept with an empty value, and all comments, section headers and blank lines are preserved exactly. That gives you a committable template that documents which variables the project needs, in the order and grouping you already use.

Does the Kubernetes Secret export encrypt anything?

No, and it says so in the output. A Kubernetes Secret stores base64, which is an encoding, not encryption — anyone who can read the manifest can decode it with one command. The export gives you both the readable stringData form and the base64 data form; for real protection you still need encryption at rest, RBAC, or a tool like Sealed Secrets or an external secret store.

What does the GitHub Actions export produce?

Two blocks. The first maps each key to ${{ secrets.KEY }}, which is what you actually want in a workflow. The second contains the literal values, for the non-sensitive variables where that is appropriate. There is also a separate gh secret set format that pushes every variable to your repository secrets in one script.

How do I move my .env to Azure, AWS, Vercel or Fly.io?

Pick the platform in the Cloud & hosting group. Azure App Service gets a JSON file for az webapp config appsettings set --settings @appsettings.json; Azure Key Vault gets az keyvault secret set commands with dash-case names plus the matching @Microsoft.KeyVault(...) references; AWS gets an ECS task-definition block and aws ssm put-parameter commands with SecureString for secrets; Vercel, Netlify, Fly.io and Heroku get their CLI commands; Cloudflare gets a wrangler.toml [vars] section. Review the output before running it — the commands contain your values unless redact values is ticked.

Why would I want a TypeScript env.d.ts?

Because it turns a missing environment variable into a compile error instead of a runtime crash. The generated declaration extends NodeJS.ProcessEnv with your keys, so your editor autocompletes them and TypeScript objects when you reference a variable that does not exist.

Can it generate a Zod or Pydantic schema from my .env?

Yes. The Code & schema group turns the file into a Zod object, a t3-env createEnv block for Next.js (with NEXT_PUBLIC_* in client), an envalid cleanEnv call, a pydantic-settings BaseSettings class, a JSON Schema and a Go struct for caarlos0/env. Types are inferred from each key and value: ports and integers, booleans, URLs, e-mail addresses, NODE_ENV as an enum, and secrets as SecretStr in Python.

What is the difference between the shell and systemd exports?

The shell script uses export KEY='value' with single quotes escaped properly, so you can source env.sh in any POSIX shell. The systemd EnvironmentFile format has no export, no inline comments and its own quoting rules — systemd will not parse a shell script, which is a surprisingly common cause of a service that starts with empty configuration.

How does JSON export handle types?

Everything is exported as a string, because that is what an environment variable is. PORT=3000 becomes "3000", not 3000. If you want real types, use the Zod, envalid, Pydantic or JSON Schema exports: they validate the strings at startup and convert them — PORT to an integer, DEBUG=true to a boolean.

Can I import from JSON or YAML?

Yes — Import JSON / YAML takes a pasted object and converts it into .env lines. Nested JSON is flattened with underscores and upper-cased, so {"redis":{"host":"x"}} becomes REDIS_HOST=x. It also understands a Docker Compose environment: block (map or list form), a Kubernetes - name: / value: list, an Azure App Service or ECS [{"name","value"}] array and a docker inspect Env array. You can replace the whole file or merge into it.

Does exporting change my file?

No. The editor content is the source of truth and exports are generated from it on the fly. The only buttons that rewrite your file are the clean-up tools in the sidebar, and each one tells you exactly what it changed.

Comparing environments

What does the Compare tab do?

It diffs two .env files by key: what is missing from B, what only exists in B, which shared keys have different values, and how many are identical. It is the fastest way to answer “why does this work locally and not in staging”.

What does the Layers tab show?

Choose Next.js, Vite, Docker Compose or a simple left-to-right order, and a mode such as production. The tab lists the load chain, marks which of those files you have open, and shows for every key the value that wins, the file it comes from, and the values it overrides. Browser-exposed keys are tagged. Open as file writes the effective environment into a new tab, with a comment naming the source of each value. The Try an example button loads a four-file Next.js setup.

How do I compare my current file against another?

Use Editor into A or Editor into B to pull the file you are working on into either side, then paste the other one opposite it. Swap flips the two panes when you realise you have them the wrong way round.

Can I check my .env against .env.example?

That is the most useful case. Put .env.example in A and your real .env in B: everything listed as missing from B is a variable the project documents but your environment does not set. Turn off compare values so you only see the key-level differences.

What do the merge buttons do?

Add these to B appends every key that exists only in A to the B pane, with A's value, and vice versa. Build merged file keeps A's layout and comments, takes values from B according to the strategy you pick — B always wins, A always wins, or B wins only when it is not empty — appends B-only keys at the end, and opens the result as a new file tab.

What is the “ignore key case” option?

It compares Database_Url and DATABASE_URL as the same key. Useful when two environments were maintained by different people with different habits, and you want the real differences rather than the casing noise.

Can I export the comparison?

Copy diff as Markdown produces a summary and lists of key names for each category, ready to paste into a pull request or a deployment checklist. Only key names are included — no values ever leave the tab.

Does compare respect masking?

Yes. With masking on, the diff shows which keys differ without showing what the values are — the right mode for going through an environment mismatch with someone else on a call.

Can I compare more than two files?

Yes, in two ways. Open all of them as file tabs and use the Layers tab, which shows the value each key ends up with and which file it came from. For drift checks, keep .env.example in pane A of Compare and pull each environment into pane B with the file picker.

Privacy, security and good practice

Is it safe to paste production secrets here?

The tool is built so that it is: there is no back end, no upload, no analytics on your content, and nothing is written to browser storage. Your file is parsed by JavaScript already in the page and rendered back to the screen. You can verify it — open the network tab and it stays empty while you edit. If your policy forbids pasting production credentials into any web page, that policy still applies, and you can load the page once and work with the network disconnected.

Does the page store anything?

No localStorage, no cookies, no IndexedDB for your content — only the light/dark theme choice is remembered. That is a deliberate trade-off: a reload loses your work, and nothing is left behind on a shared machine.

What about the analytics on the site?

The site uses privacy-first page-view analytics with no cookies and no personal data. It records that the page was viewed. It has no access to what you type — that never leaves the JavaScript running in your tab.

Where should secrets actually live?

A local .env is fine for development. For anything shared or deployed, use the platform's secret store — AWS Secrets Manager or Parameter Store, Google Secret Manager, Azure Key Vault, HashiCorp Vault, Doppler, Infisical, or your CI provider's encrypted secrets. The advantages are audit logs, rotation and access control, none of which a file on disk can give you.

How do I share a .env file with a teammate?

Not as plain text over Slack or email — both keep searchable copies forever. Use a secret manager, or Encrypt / decrypt here: it wraps the file with AES-256-GCM and a passphrase, so you can send the encrypted block by chat and the passphrase by a different channel. If you only need to show the structure, use Copy template link (key names only) or export .env.example.

How does the encryption work?

Encrypt / decrypt derives a 256-bit key from your passphrase with PBKDF2-SHA-256 (600,000 iterations and a random 16-byte salt) and encrypts the file with AES-256-GCM and a random 12-byte nonce, all through the browser's Web Crypto API. The result is a text block between BEGIN/END ENV-EDITOR ENCRYPTED FILE lines — safe to paste into a chat or save as .env.enc. Only the file name is readable. Anyone with the block and the passphrase can open it here; a changed byte or a wrong passphrase fails the authentication check instead of producing garbage. The passphrase generator gives about 150 bits; send the passphrase through a different channel than the file.

What does the template link contain?

Only key names, comments and blank lines — the same thing as .env.example — compressed into the part of the URL after #, which browsers never send to a server. Values are stripped before the link is built and stripped again when it is opened, and comment lines that contain a credential are left out. It is a quick way to tell a teammate which variables a project needs.

I accidentally committed my .env. What do I do?

In this order: rotate every credential in it, then remove the file from history with git filter-repo or BFG, then force-push and tell anyone with a clone to re-clone. Rotation comes first because the moment a secret hits a remote — especially a public one — assume it has been scraped. Bots scan new GitHub commits for token patterns within seconds.

How do I stop it happening again?

Should different environments have different secrets?

Always. Sharing one API key between development, staging and production means a laptop compromise is a production compromise, and it makes rotation an all-or-nothing event. The reused secret finding in the audit exists for the same reason at the file level.

How often should secrets be rotated?

The honest answer is: whenever exposure is plausible, and on a schedule you can actually keep. Immediately after any suspected leak, when someone with access leaves, and periodically for high-value credentials. A rotation process that is used quarterly beats a monthly policy that nobody follows.

Is base64 a way to protect a value?

No. Base64 is an encoding — echo … | base64 -d reverses it in a second. It appears in Kubernetes Secrets and Docker configs for transport reasons, never for secrecy. The Kubernetes export in this tool says so directly in the generated file.

Can this replace a secrets manager?

No, and it is not trying to. It is an editor, a linter and a converter for the files you already have. A secrets manager gives you storage, access control, rotation and audit trails. This tool helps you get the file right before it goes into one.

Is it free? Is there a catch?

Free, no account, no limits, no telemetry on your content. It is one of a set of client-side browser tools at jasperbernaers.com/apps — no server means no running cost, and no server also means nothing that could leak.

Which browsers are supported?

Any current version of Chrome, Edge, Firefox, Safari, Brave, Opera or Vivaldi, on desktop or mobile. There are no external libraries or frameworks — encryption uses the browser's built-in Web Crypto API — so the page is self-contained and keeps working offline.