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
.envfile 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
| Output |
|---|---|
|
|
|
|
|
|
| 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_URLinjection 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 |
|---|---|---|---|---|
|
| ✓ | — | The source YAML |
|
| ✗ |
| Prefix every emitted key (e.g. |
|
| ✗ |
| Delimiter for nested keys |
|
| ✗ |
| Value quoting style |
|
| ✗ |
| Flag credential-looking values |
|
| ✗ |
| Output format |
|
| ✗ |
| 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 | Native ( |
Anchors / references | None | Yes ( |
Type inference | None — values typed by structure | Aggressive ( |
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, |
|
Editor LSP support | Universal | Strong with |
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 |
|---|---|---|
|
| Provide a non-empty YAML string |
| YAML parser failed | Verify the YAML is valid (e.g. |
|
| Use |
| Multi-document file without | 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.