obfus.link
Converters

YAML to .env with secret-scanning and platform formatters

Convert any YAML configuration into a flattened .env file with secret-credential scanning and platform-specific formatters for Vercel, Railway, Fly.io, and Docker Compose. Generic .env output also supported.

YAML-to-env flattens any YAML configuration into env-var format with configurable delimiter for nested keys and quoting style. The secret scanner flags credential-looking values (KEY, SECRET, TOKEN, PASSWORD, known cloud provider prefixes) before they ship. Platform formatters emit Vercel, Railway, Fly.io, or Docker Compose-specific shapes.

1. Insight

Insight

The problem this article addresses and why it matters.

Two formats, one platform-specific quirk per environment

Configuration management has converged on YAML for human-edited config (Kubernetes manifests, docker-compose, GitHub Actions, Helm charts) and .env-style key-value pairs for runtime injection into a process (twelve-factor apps, container env vars, Vercel / Railway / Fly.io project settings). Most teams keep both: a YAML source of truth for review and version control, an exported .env for the runtime.

The export is a flat-vs-nested translation problem dressed up as a string replacement. YAML supports nesting (database.host is database.host); env vars don't (DATABASE__HOST or DATABASE_HOST — the delimiter varies). YAML supports booleans, numbers, dates, lists; env vars are strings. YAML supports references and anchors; env vars don't.

The naive script that exports YAML to env handles maybe 70% of real configs. The remaining 30% is where every team writes their own variant — and where the security incidents live (the variable named DATABASE_PASSWORD accidentally committed to git because the developer didn't realise the export pulled secrets from a parent file).

Why a converter with platform formatters and a secret scanner

The tool in this article handles the flattening + type coercion deterministically, then adds two features that turn the export into something safe to run unsupervised. First, a secret scanner: every output key whose name or value matches credential patterns (*KEY, *SECRET, *TOKEN, *PASSWORD, *PRIVATE, AWS access keys, Stripe sk_*, JWT-shaped values) is flagged with a severity rating and an explanation. Second, platform formatters: instead of generic .env output, the tool emits the exact format Vercel CLI, Railway, Fly.io, or docker-compose expects. The developer pastes the output straight into the platform's import mechanism.

What this article delivers

End-to-end walkthroughs of a real Kubernetes-style YAML exported to four different platform formats, the secret-scanner output that catches accidental credential exposure, and the conventions for naming the delimiter (__ vs _) when nesting is involved. We cover the YAML constructs that don't translate cleanly (anchors, multi-document files, multiline blocks) and the recovery paths for each.

2. Intent

Intent

What you will be able to do after reading.

By the end of this article you will be able to:

  • Convert any YAML configuration into a flattened .env file with a configurable delimiter for nested keys
  • Apply the secret scanner to flag credential-looking values before they ship to a logging system or commit history
  • Emit platform-specific output for Vercel, Railway, Fly.io, or docker-compose instead of generic .env
  • Choose the right quoting style for values containing spaces, equals signs, or shell metacharacters
  • Identify YAML constructs the converter can't translate (anchors, references, multi-document) and the recovery paths

The Examples section walks through a real Kubernetes ConfigMap export, the same input exported to a Vercel-import JSON, and the secret scanner finding three credentials the developer forgot were in the source file.

3. Examples

Examples

Annotated code and worked scenarios.

Before / after: a Kubernetes-style config

A YAML config from a real microservice deployment:

Before:

service:
  name: orders-api
  port: 8080
database:
  host: orders-db.internal
  port: 5432
  ssl: true
  pool:
    max: 20
    idle_timeout: 30000
features:
  rate_limiting: true
  audit_log:     false
secrets:
  jwt_secret:   "sk_live_abc123def456ghi789"
  db_password:  "p@ssw0rd!2026"

After:

yamlToEnv({
  yaml,
  prefix:     '',
  delimiter:  '__',
  quote:      'double',
  secretScan: true,
  targetPlatform: 'generic',
});

Output:

SERVICE__NAME="orders-api"
SERVICE__PORT="8080"
DATABASE__HOST="orders-db.internal"
DATABASE__PORT="5432"
DATABASE__SSL="true"
DATABASE__POOL__MAX="20"
DATABASE__POOL__IDLE_TIMEOUT="30000"
FEATURES__RATE_LIMITING="true"
FEATURES__AUDIT_LOG="false"
SECRETS__JWT_SECRET="sk_live_abc123def456ghi789"
SECRETS__DB_PASSWORD="p@ssw0rd!2026"

Plus the secret-scan output:

// secrets: [
//   {
//     key:      'SECRETS__JWT_SECRET',
//     reason:   'value matches Stripe live key pattern (sk_live_*)',
//     severity: 'high',
//   },
//   {
//     key:      'SECRETS__DB_PASSWORD',
//     reason:   'key matches credential pattern (*PASSWORD)',
//     severity: 'high',
//   },
// ]

The scanner caught both the Stripe key (detected by value-pattern) and the password (detected by key-name pattern). The developer sees these before pasting the .env into a CI variables list — which catches the case where the source YAML accidentally has secrets that should be in a vault, not in env-vars.

Before / after: Vercel platform format

Same input, targetPlatform: 'vercel':

{
  "platform": "vercel",
  "command":  "vercel env add",
  "entries": [
    { "key": "SERVICE__NAME",           "value": "orders-api",         "target": ["production", "preview", "development"] },
    { "key": "SERVICE__PORT",           "value": "8080",               "target": ["production", "preview", "development"] },
    { "key": "DATABASE__HOST",          "value": "orders-db.internal", "target": ["production", "preview", "development"] },
    ...
  ]
}

The output is the exact JSON shape vercel env pull reads in. Paste into .vercel/env.json or feed to vercel env add --file -. Skip the manual entry of 12 variables one at a time in the Vercel dashboard.

Before / after: docker-compose env_file

# targetPlatform: 'docker-compose' produces a fragment for service definition
services:
  orders-api:
    env_file:
      - ./orders.env

# Plus the generated orders.env file:
SERVICE__NAME=orders-api
SERVICE__PORT=8080
DATABASE__HOST=orders-db.internal
# ... (no quotes — docker-compose env_file format doesn't accept them)

docker-compose's env_file format is similar to .env but rejects quoted values — the tool detects the target and emits the appropriate variant. Quoting differences between platforms are the kind of bug that surfaces six hours after deploy when an env var contains a space.

Before / after: nested delimiter choice

The same YAML with different delimiter settings:

database:
  pool:
    max: 20

delimiter

Output

__ (default)

DATABASE__POOL__MAX="20"

_

DATABASE_POOL_MAX="20"

.

DATABASE.POOL.MAX="20" (unusual; some tools accept this)

-

rejected — env vars can't contain hyphens on POSIX

Most teams use __ because it's unambiguous: RATE_LIMIT_MAX could be rate_limit.max or rate.limit_max in the source, but RATE_LIMIT__MAX is unambiguously rate_limit.max.

When humans use this

A developer exporting a new service's config for the first time runs the YAML through the tool, reviews the secret-scan output (and moves any flagged values to a secrets manager), picks the platform format for their deployment target, and pastes. The whole flow takes under two minutes versus the 15-30 minute manual conversion that ends with a typo in DATABASE_HOST.

When agents use this

Three patterns where the tool adds value in agent pipelines:

  • Infrastructure scaffolding. An agent generating a new service writes the YAML config (because YAML is what reviewers will edit), then immediately converts to env-vars for the platform's deployment system. The agent doesn't re-derive the flattening rules each time.
  • Migration assistant. An agent migrating a service from one platform to another (Heroku → Fly.io, Vercel → Railway) reads the source platform's env-var export, builds the equivalent YAML, then re-exports in the target platform's format. The roundtrip surfaces incompatibilities (Heroku's automatic DATABASE_URL injection has no Fly.io equivalent).
  • Secret-hygiene auditing. A scheduled agent runs the secret scanner on every YAML file in the repo on every PR. New high-severity findings open a security advisory before the PR can merge.

Edge cases

YAML anchors and references

The converter resolves anchors (&anchor) and references (*reference) inline before exporting. The output has no anchor concept — every reference materialises as a full key-value. Comment on each output key indicates whether it came from an anchor; the comment is stripped from the platform formats but visible in the generic .env output.

Multi-document YAML

Files with --- separators are treated as multiple configs. The converter emits one document per call by default; pass documentIndex: N to select a specific document, or mode: 'all' to receive an array of outputs.

Multiline string blocks

| (literal) and > (folded) multiline blocks are preserved as literal multiline values in the output. The platform formatters handle these differently — vercel accepts JSON-escaped newlines, docker-compose env_file rejects them (use environment: block instead). Warnings surface the platform-specific limitation.

Empty maps and nulls

key: {} and key: null produce KEY="". Same goes for empty arrays. If your downstream interprets empty strings as missing values, post-process the output before pasting.

4. Documentation

Documentation

Reference signatures, edge cases, and lookup tables.

Input parameters

Field

Type

Required

Default

Description

yaml

string

The source YAML

prefix

string

''

Prefix every emitted key (e.g. APP_)

delimiter

string

'__'

Delimiter for nested keys

quote

'single' | 'double' | 'none'

'double'

Value quoting style

secretScan

boolean

false

Flag credential-looking values

targetPlatform

'generic' | 'vercel' | 'railway' | 'fly' | 'docker-compose'

'generic'

Output format

documentIndex

number

0

Multi-document YAML — which document to convert

Output shape

{
  env:      string;     // .env-formatted output for generic; the platform's expected format otherwise
  keys:     string[];   // every emitted key, in source order
  warnings: string[];   // YAML constructs that didn't translate cleanly
  secrets?: Array<{
    key:      string;
    reason:   string;
    severity: 'high' | 'medium';
  }>;
  platform?: string;    // platform-specific output structure (vercel JSON, fly.toml, etc.)
}

Secret-scan patterns

Severity = high when ANY of the following match:

  • Key name matches *KEY, *SECRET, *TOKEN, *PASSWORD, *PRIVATE (case-insensitive)
  • Value matches a known cloud-provider key prefix: sk_live_, sk_test_, pk_live_, AKIA* (AWS), xoxb-, xoxp- (Slack), ghp_, gho_ (GitHub PAT)
  • Value matches JWT shape (three base64 segments separated by .)

Severity = medium when:

  • Key contains auth, cred, signing (case-insensitive) and value is non-empty
  • Value is a URL containing credentials (https://user:pass@host/...)

JSON vs YAML for configuration: which format to use

Both YAML and JSON appear in config files everywhere; the choice has real consequences for tooling, parser safety, and human ergonomics. This tool exists because the source of config is often YAML (hand-edited) but the destination at runtime is usually flat env vars or JSON.

Property

JSON

YAML

Spec stability

RFC 8259 — single spec

YAML 1.1 vs 1.2 — parsers disagree

Comments

Not supported

Supported (#)

Trailing commas

Disallowed

Allowed

Multiline strings

Escaped \n only

Native (| literal, > folded)

Anchors / references

None

Yes (&anchor, *ref, <<: merge)

Type inference

None — values typed by structure

Aggressive (yes, no, on, off → booleans)

Parsing ambiguity

None

Several footguns (Norway problem, octal numbers, sexagesimals)

Indentation-sensitive

No

Yes

Schema validation

JSON Schema (mature)

JSON Schema (via YAML→JSON conversion)

Formatters

Prettier, jq

yamllint, prettier (limited)

Editor LSP support

Universal

Strong with yaml-language-server

Use JSON when the file is machine-generated, exchanged between services, or stored in a database/cache. The lack of comments hurts hand-editing but the unambiguous spec is worth it — every parser in every language agrees on what the file means.

Use YAML when the file is hand-edited by developers and the structure has repetition that benefits from anchors. Kubernetes manifests, GitHub Actions workflows, Ansible playbooks, and Docker Compose files are all YAML for this reason — comments and multiline strings make complex configs readable in a way JSON never will.

Avoid YAML when the file is consumed by a parser whose YAML version you don't control (older Python yaml.safe_load, embedded systems, legacy Ruby). The 1.1 → 1.2 changes around yes / no / on / off produce silent type changes that don't surface until production. Use strictyaml or YAML 1.2-only parsers to eliminate the implicit-typing class of bugs.

Modern alternatives worth considering:

  • TOML — INI-shaped, less ambiguous than YAML, no indentation traps. Standard for Rust Cargo, Python pyproject.toml, Go build configs.
  • JSON5 / JSONC — JSON with comments + trailing commas. Used by VS Code settings, TypeScript tsconfig.json. Drop-in upgrade for editor-friendly JSON.
  • HCL — Terraform's config language. Designed for declarative infra with first-class interpolation. Niche outside the HashiCorp ecosystem.

For the env-var pipeline this tool implements, YAML is the right source: ops engineers can comment, group, and use anchors for repeated values, then the converter produces the deterministic .env the runtime expects.

Error codes

Code

When it fires

Recovery

INPUT_EMPTY

yaml is empty

Provide a non-empty YAML string

INPUT_MALFORMED

YAML parser failed

Verify the YAML is valid (e.g. yamllint)

INPUT_INVALID_TYPE

delimiter contains a character invalid for env-var names (-, . for strict POSIX)

Use __ (default) or _

UNSUPPORTED_FORMAT

Multi-document file without documentIndex and the documents have different shapes

Specify which document to convert

When NOT to use this tool

For runtime secrets, don't put them in YAML in the first place — use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) and reference the secret-manager identifier in the YAML. The export then carries only the identifier, not the secret value. This tool helps you spot the case where you accidentally violated that rule.

For configuration that has runtime conditionals (different values per environment), use a multi-document YAML and call the converter per environment, OR use a config library that handles environments natively (config for Node, dynaconf for Python). Flat env-vars don't compose conditionals well.

For binary or multiline values, the env-var format is the wrong destination — use a config file mounted into the container or a config-fetching call at runtime.

Performance notes

Typical execution: under 5ms for inputs below 50KB. The secret-scan adds 2-5ms — it's a regex sweep across the output. The tool is deterministic — same input + same parameters always produce byte-identical output — so REST responses are Edge-Cache eligible. The YAML parser handles up to 1MB inputs comfortably; larger inputs return INPUT_TOO_LARGE rather than risk slow conversion.

The platform formatters track their respective platform's documented format as of mid-2026. Platform changes (rare but they happen — Vercel migrated its env-var CLI in 2024) require a tool release. Pin the version in CI if your build needs exact reproducibility.

Try it now

YAML to .env

Convert YAML config to .env format with secret scanning

FAQ

Frequently asked questions

Why use __ as the default delimiter?

It's unambiguous. RATE_LIMIT_MAX is ambiguous (could be rate_limit.max or rate.limit_max); RATE_LIMIT__MAX is unambiguous (rate_limit.max). The double-underscore convention is widely adopted by env-var heavy frameworks (Symfony, Laravel, ASP.NET Core).

What if my YAML has secrets I want to keep?

The secret scanner flags them but doesn't strip them. The output still contains the values; the warnings tell you which keys should move to a secrets manager (Vault, AWS Secrets Manager, Doppler) and out of YAML / env-vars entirely.

How does the Vercel formatter differ from generic .env?

Vercel's import format is a JSON array of { key, value, target } objects where target is the list of environments. The formatter emits that shape directly — paste into .vercel/env.json or pipe to vercel env add --file.

What about YAML lists?

Lists are flattened by index — items: [a, b, c] becomes ITEMS__0=a, ITEMS__1=b, ITEMS__2=c. Most env-var consumers don't parse this idiom; if your downstream expects a comma-separated string, pre-join the list in YAML or post-process the output.