.env to TOML Converter
Turn a .env file into a structured TOML configuration. DATABASE__HOST=localhost becomes [database] host = "localhost". Booleans and numbers are coerced; quoted values are unwrapped.
What this tool does
It reads a .env file line by line, pulls out the KEY=value pairs, splits each key on a nesting separator (__ by default) into a path, and emits the result as TOML 1.0. DATABASE__HOST=localhost becomes a [database] table with host = "localhost". Type inference turns true/false into booleans and bare numbers into integers or floats, so the output is typed rather than all-strings.
The intent it closes: "I've been running a service off a flat .env file and now I want a structured, typed config I can commit to the repo." The usual destination is a config.toml read by a Rust, Go, or Python service at startup, or a settings file you hand-edit and add comments to once the shape is right.
One thing to know up front: keys are lowercased when they're split into the nested path. DATABASE__HOST and database__host both land at [database] host. The widget hint says this; the rest of the page assumes it.
When you'd reach for it
- Promote a flat
.envto a structured config. Name your keysDATABASE__HOST,DATABASE__PORTand leave the separator on__— they collapse into one[database]table. - Get a typed config instead of all-strings. Leave Infer types on Yes so
DEBUG=falsebecomes a real boolean andWORKERS=4becomes an integer, not"false"and"4". - Drop a vendor prefix while you convert. Set Strip prefix to
MYAPP_soMYAPP_DATABASE__HOSTlands at[database] hostwithout the noise. - Unwrap quoted secrets cleanly.
PASSWORD="s3cret!"comes through with the quotes removed and re-emitted as a proper TOML string. - Keep values verbatim when inference would mangle them. Switch Infer types to Keep as strings for things like zero-padded codes or version strings that should not become numbers.
- Convert a file full of credentials safely. Parsing and emission run in your tab, so a
.envfull of API keys never leaves the machine.
How the conversion works
One click of Convert runs three stages.
1. Parse the .env lines
Each line is read independently. A leading export is stripped and the line is trimmed; blank lines and lines starting with # are skipped. What remains must match KEY=value where the key is [A-Za-z_][A-Za-z0-9_]* — anything that doesn't match (a line with no =, a key that starts with a digit) is silently skipped rather than erroring. There is no multi-line value support; every pair lives on one line.
2. Unwrap and infer the value
A double-quoted value ("…") is unwrapped and its backslash escapes are collapsed (\n becomes a literal n, \" becomes "). A single-quoted value ('…') is taken literally with no escape processing. A bare, unquoted value has any trailing # comment stripped first, then — when Infer types is on — it is coerced: true/false (any case) to boolean, -?\d+ to integer, -?\d+.\d+ to float, and a value shaped like […] or {…} is run through JSON.parse, falling back to the original string if that throws.
3. Build the tree and emit TOML
Each key is lowercased and split on the separator into a path; the parts before the last become nested tables and the last holds the value. If a prefix was set, it's removed from the front of the key first (along with any underscores left behind). The assembled tree is handed to the same canonical TOML writer the formatter uses, so the output has one space around each =, a blank line between sections, and basic double-quoted strings. If nothing parsed into a key, the status bar shows No KEY=VALUE lines found. and no output is written.
Options reference
Separator
The substring a key is split on to build nested tables. The default is __ (double underscore), the de-facto convention used by Symfony, Spring, and many twelve-factor apps, because a single underscore is too common inside ordinary variable names. DATABASE__HOST with __ becomes [database] host; the same key with the single _ separator splits on every underscore and nests as [database] [database.host]-style depth, which is rarely what you want. The . (dot) option is there for keys that already use dotted names.
Infer types
Yes (numbers/bools/JSON) coerces unquoted values: true/false to booleans, whole numbers to integers, decimals to floats, and bracket/brace-shaped values through JSON.parse so TAGS=["a","b"] becomes a real TOML array. Keep as strings turns all of that off and writes every bare value as a quoted string — the safer choice when a value only looks numeric (a zip code, a build number) but must stay textual. Quoted values are never inferred either way; quoting in the source is your signal that you meant a string.
Strip prefix
A leading prefix to remove from every key before the tree is built. With MYAPP_, the key MYAPP_DATABASE__HOST becomes DATABASE__HOST (and any underscores left dangling at the front are trimmed too) before it's lowercased and split. Only keys that actually start with the prefix are touched; the rest pass through unchanged. Leave it blank to keep keys as-is.
Output and mapping rules
- Keys split on the separator → nested tables.
DB__HOSTwith the__separator becomes[db]withhost = …. - Keys are lowercased as the path is built, so the TOML identifiers come out lowercase regardless of how the
.envshouted them. - Comment lines and blank lines are dropped, and so are any inline
# commentsafter a bare value — they're parsed only to be skipped, never carried into the TOML. exportis stripped from the front of anyexport KEY=valueline.- Booleans, integers, and floats are emitted bare (when inference is on); strings are emitted with basic double quotes.
- Encoding and layout: UTF-8, LF line endings, one space around each
=, a blank line between sections. - Download:
config.toml, content typetext/plain;charset=utf-8.
Example
Input:
# app
APP_NAME=MyApp
DEBUG=false
# database
DATABASE__HOST=localhost
DATABASE__PORT=5432
DATABASE__PASSWORD="s3cret!"
Output (defaults — separator __, infer types on, no prefix):
app_name = "MyApp"
debug = false
[database]
host = "localhost"
port = 5432
password = "s3cret!"
Note that APP_NAME has only a single underscore, so it stays a flat key app_name rather than nesting — only the __ separator nests. DEBUG became a real boolean and PORT a real integer, while the quoted password kept its ! and came through as a string.
Recipes by intent
Group flat keys into sections
Rename related keys to share a __ stem — DATABASE__HOST, DATABASE__PORT, DATABASE__USER — and leave the separator on __. They collapse into one [database] table instead of three top-level keys.
Get a typed config, not a wall of strings
Leave Infer types on Yes. DEBUG=false, WORKERS=4, and RATE=0.5 come out as a boolean, an integer, and a float — exactly what a TOML reader expects, with no manual retyping.
Strip an app prefix from every key
Put your prefix (for example MYAPP_) in Strip prefix. Every MYAPP_* key sheds the prefix before nesting, so a flat namespaced .env turns into a clean tree without the repeated noise.
Keep a value that only looks numeric
Switch Infer types to Keep as strings, or quote the value in the source. A zero-padded code like ID=007 stays the string "007" instead of becoming the integer 7.
Limits and performance
- In-memory, single pass. The whole file is split into lines and the tree is built before anything is emitted, so input, tree, and output all sit in memory at once. A real
.envis tiny and converts instantly; this isn't a tool that needs a streaming path. - One value per line. There's no multi-line or heredoc value support — a value that spans lines won't parse the way a shell would read it.
- The textarea is the only slow part, and only if you paste something enormous; for ordinary config-sized files it's imperceptible. Use Download .toml rather than Copy if a giant output ever lags the right pane.
- Comments are not preserved, so plan to re-add the documentation you want in the committed TOML by hand.
Errors and how to fix them
No KEY=VALUE lines found.
Nothing in the input matched a KEY=value pair after comments and blanks were skipped. Common causes: the file is all comments, the keys start with a digit (the key must match [A-Za-z_][A-Za-z0-9_]*), or you pasted something that isn't a .env at all. Make sure at least one line has a valid key, an =, and a value.
A line was silently skipped
Lines that don't match the KEY=value shape are dropped without an error — a line with no =, a key beginning with a number, or a stray fragment. Check the line has a valid key on the left of the first =.
My key didn't nest into a table
Nesting only happens on the chosen Separator. A key like APP_NAME has a single underscore, so with the default __ separator it stays a flat app_name key. Rename it with a __ stem (APP__NAME) or change the separator to single _ if you really want every underscore to nest.
A value I wanted as text came out as a number or boolean
Type inference coerced it. Quote the value in the source (ID="007") — quoted values are never inferred — or switch Infer types to Keep as strings to turn coercion off entirely.
My comments disappeared
Expected. # lines and trailing # comments after bare values are parsed only so they can be skipped; they never enter the TOML tree. Add the comments you want back into the output by hand.
FAQ
Why are my keys lowercased?
The key is lowercased as it's split into the nested path, so the TOML identifiers come out lowercase no matter how the .env capitalised them. This is by design — .env keys are conventionally SHOUTED, but TOML config keys read better lowercase, and it keeps DATABASE__HOST and database__host from producing two different tables.
Are array values supported?
Partly. .env has no native array syntax, but with Infer types on, a value shaped like TAGS=["a","b"] is run through JSON.parse and becomes a real TOML array. A plain comma list like TAGS=a,b,c stays a single string — wrap it in brackets in the source, or edit the output by hand.
What's the difference between the single and double underscore separator?
The default __ splits only on double underscores, so ordinary names like APP_NAME stay flat. The single _ option splits on every underscore, which nests aggressively and is rarely what you want for a typical .env. Pick __ unless your keys were deliberately namespaced with single underscores.
Does it handle export lines?
Yes. A leading export is stripped before the line is parsed, so a sourced shell file with export DATABASE__HOST=localhost converts the same as a plain .env.
Is my data uploaded?
Never. Parsing and TOML emission run entirely in your browser. You can go offline after the page loads and the converter still works — safe for a .env full of API keys, passwords, and tokens.