← All posts

Stop Using .env Files for Secrets in 2026: dotenv Alternatives for Every Language

The short answer

A .env file is the same idea in every language: a text file of KEY=value lines that a library loads into your process environment at startup. Node calls the loader dotenv (and now ships --env-file built in), Python has python-dotenv, Go has godotenv, and every other ecosystem has its own. They all do the same job, and they all load the same thing: your secrets, in plaintext, on disk.

In 2026 the loader itself is increasingly redundant. But that misses the real point. The file is the liability, in any language. For anything sensitive, stop using a .env file and inject secrets at runtime instead. SikkerKey does that with one CLI and SDKs for six languages.

Every language has a dotenv, and the same .env file

The package differs by ecosystem; the file does not.

LanguageCommon .env loader
Node.jsdotenv, or built-in --env-file since v20.6.0
Pythonpython-dotenv
Gogodotenv
.NETDotNetEnv
Kotlin / Javadotenv-java
PHPphpdotenv

Node is the clearest example of where this is heading. Since v20.6.0 (2023) you can run node --env-file=.env app.js with no package at all, and in 2026 the supported lines (the 22 and 24 LTS releases and the current 26) all read .env files out of the box. Node 20, the line that introduced the flag, reached end-of-life in April 2026. Whether you load the file with a package or a built-in flag, though, you still have a file full of plaintext secrets sitting next to your code.

A .env file is plaintext secrets on disk

The .env convention was built for convenience, and the failure modes are well known, regardless of language:

None of these are exotic. They are the ordinary ways credentials leak, and a loader, built-in or not, does nothing about any of them.

The fix: inject secrets at runtime

The alternative is to keep secrets out of files entirely. Your application asks a secrets manager for the values it needs when it starts, holds them in memory for the life of the process, and writes nothing to disk. The secret lives in one place, your SikkerKey vault, and your deployment reads it on demand instead of keeping its own copy.

That removes the whole class of problems above at once: there is no file to commit, bake in, print, or copy, and rotation, audit, and access control move to your SikkerKey vault where they belong. SikkerKey gives you two ways to do this, and both work the same in any stack.

One CLI for every stack

sikkerkey run injects secrets as environment variables and then runs your command, in any language. It is the drop-in replacement for dotenv, --env-file, python-dotenv, and the rest:

# Inject everything this machine can read, then run your app (any language)
sikkerkey run --all -- node app.js
sikkerkey run --all -- python app.py
sikkerkey run --all -- ./server

# Pick specific secrets
sikkerkey run --secret sk_db_prod --secret sk_stripe_prod -- ./worker

# Scope --all to a single project
sikkerkey run --all --project production -- node app.js

# Preview what would be injected, without running anything
sikkerkey run --all --dry-run

Migrating is a one-line change to your start command. For a Node app:

// before
{ "scripts": { "start": "node --env-file=.env app.js" } }

// after
{ "scripts": { "start": "sikkerkey run --all -- node app.js" } }

The secrets are passed to the process in memory, for that run only. Your code keeps reading them from the environment exactly as before, so nothing inside the app changes. The machine running this enrolled with SikkerKey once; after that it reads with no key or token in the command. (The first-secret walkthrough covers enrollment end to end.)

Or read secrets in code: six SDKs

If you would rather fetch specific secrets from inside the application, SikkerKey ships a native SDK for each major ecosystem. They share the same shape: create a client that auto-detects the machine's vault, then read a secret by id. Each reads at runtime over an Ed25519-signed request, with minimal or zero dependencies.

Python (3.10+)

# pip install sikkerkey
from sikkerkey import SikkerKey

sk = SikkerKey()                       # auto-detects this machine's vault
api_key = sk.get_secret("sk_stripe_prod")

Node.js / TypeScript (18+)

// npm install @sikkerkey/sdk
import { SikkerKey } from '@sikkerkey/sdk'

const sk = SikkerKey.create()          // auto-detects this machine's vault
const apiKey = await sk.getSecret('sk_stripe_prod')

Go (1.22+)

// go get github.com/SikkerKeyOfficial/sikkerkey-go@latest
import sikkerkey "github.com/SikkerKeyOfficial/sikkerkey-go"

sk, _ := sikkerkey.NewAutoDetect()     // auto-detects this machine's vault
apiKey, _ := sk.GetSecret("sk_stripe_prod")

.NET (8.0+)

// dotnet add package SikkerKey
using SikkerKey;

var sk = SikkerKeyClient.Create();     // auto-detects this machine's vault
var apiKey = await sk.GetSecretAsync("sk_stripe_prod");

Kotlin / JVM (17+)

// implementation("io.github.sikkerkeyofficial:sikkerkey-sdk:1.1.0")
import com.sikker.key.sdk.SikkerKey

val sk = SikkerKey()                    // auto-detects this machine's vault
val apiKey = sk.getSecret("sk_stripe_prod")

PHP (8.1+)

// composer require sikkerkey/sdk
use SikkerKey\SikkerKey;

$sk = SikkerKey::create();             // auto-detects this machine's vault
$apiKey = $sk->getSecret('sk_stripe_prod');

Full reference for each is in the SDK docs.

No bearer token to leak

Most secrets tools hand your app a long-lived API token to authenticate with, which just relocates the problem: now that token is the secret in your .env file or environment, and it can read everything.

SikkerKey works differently. When a machine is enrolled, a keypair is generated on that machine and only the public key is registered with SikkerKey. Every read is a request signed with the private key, which never leaves the host. There is no shared API key or bearer token to copy into a file, paste into CI, or leak in a log, and a request that is not signed by a known, approved machine is not served. This holds the same way whether you use the CLI or any of the SDKs.

Rotation, audit, and access control

Because the secret lives in your SikkerKey vault rather than a file, the operational pieces come with it:

When a .env file is still the right tool

This is not an argument against .env files for everything. For non-sensitive local configuration on your own machine, a port number, a log level, a feature flag, a .env file (loaded with node --env-file, python-dotenv, or whatever your stack uses) is perfectly fine.

The line is sensitivity. The moment a value would hurt if it leaked, a database URL, an API key, a token, a signing secret, it should not live in a file at all.

FAQ

Is dotenv deprecated in 2026? Not deprecated, but increasingly redundant. Node reads .env natively since v20.6.0, and the loader is a thin convenience in every language. The bigger question is whether secrets belong in a .env file at all.

What is the dotenv equivalent in Python, Go, or PHP? python-dotenv, godotenv, and phpdotenv respectively; .NET has DotNetEnv and the JVM has dotenv-java. They all load a plaintext .env file into the environment, with the same trade-offs.

How do I load secrets without a .env file? Inject them at runtime. SikkerKey's CLI (sikkerkey run --all -- <command>) does it for any language, or an SDK fetches them in code at startup.

Which languages does SikkerKey support? Six SDKs: Python, Node.js, Go, .NET, Kotlin/JVM, and PHP, plus a CLI that works with any language or runtime.

Should I commit .env to git? No. A committed .env is one of the most common ways secrets leak. Keep secrets out of files entirely and inject them at runtime.

Is a .env file ever fine? Yes, for non-sensitive local configuration (a port, a log level, a feature flag) on your own machine. The moment a value is a real secret, it should not be in a file.