npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cooler-env

v4.0.0

Published

CLI + module utility to help you manage your environment variables better. Inspired by Ruby on Rails credentials.

Readme

Cooler-Env

CI npm version node license

A CLI and module for managing encrypted environment variables, inspired by Ruby on Rails credentials.

Instead of scattering plaintext .env files across machines and chat threads, Cooler-Env keeps your secrets in a single encrypted blob you can safely commit to version control, decrypted at runtime with a key that never leaves your .gitignore. Sensitive values spend as little time as possible in plaintext, and an intuitive CLI handles the create/edit/delete flow for you.


Contents

How it works

Every environment (development, production, …) is backed by a trio of files inside a config directory (default config/):

| File | Committed? | Purpose | | --------------- | ------------ | -------------------------------------------------------------------------------- | | <env>.key | ❌ Never | Hex secret key (crypto.randomBytes(32)). Auto-added to .gitignore on init. | | <env>.yml.enc | ✅ Yes | The encrypted secrets blob. Safe to commit. |

Encryption uses Node's built-in, authenticated aes-256-gcm. The on-disk blob is a single binary payload — a 5-byte versioned header ([magic "CENV"(4)][version(1)]) followed by [salt(16)][iv(12)][authTag(16)][ciphertext] — with a fresh random salt and IV per write and the AES key derived from your secret key via scrypt. Any tampering or a wrong key fails loudly rather than returning garbage. See Security model for details.

Installation

npm install cooler-env
# or
yarn add cooler-env

Requires Node.js 20+. Ships as a CommonJS build (importable from both require and ESM import) with TypeScript types included.

Quick start

# 1. Initialize an environment (creates config/development.key + config/development.yml.enc)
npx cooler-env init -e development

# 2. Add a secret through the interactive prompt
npx cooler-env add -e development
#   ? Key name:  API_KEY
#   ? Value:     sk_live_123...
// 3. Load them in your app
import { loadEnv } from "cooler-env";

const env = await loadEnv("development");
console.log(env.API_KEY); // "sk_live_123..."

CLI reference

All commands share the same two options:

| Option | Required | Description | | ----------- | -------- | ----------------------------------------------------------------------------------------------------------- | | -e <env> | ✅ | Environment name (e.g. development, production). Becomes a file name, so it cannot contain / or \. | | -p <path> | ❌ | Directory for the key/encrypted files, relative to the current directory or absolute. Defaults to config. |

init

Sets up a new environment, generating the .key and .yml.enc files and adding the key to the .gitignore in the current directory (run it from your repository root). If the key lives outside the current directory, init warns instead, and you must ignore it yourself.

cooler-env init -e development

⚠️ Never commit the .key file. Anyone with it can decrypt your secrets. init gitignores it for you, but double-check before pushing.

add

Opens an interactive prompt to add a new key/value pair. The value is hidden as you type — not echoed to the terminal, and its length isn't shown either. Pass --show to type it in the clear.

Key names must be valid environment-variable identifiers — start with a letter or underscore, then letters, digits, or underscores (e.g. API_KEY). Values may be anything, including an empty string.

cooler-env add -e development
cooler-env add -e development --show   # reveal the value while typing

For scripting and CI, pass the key with -k/--key and the value with -v/--value, which skips the prompts. Omit -v to read the value from stdin (a single trailing newline is trimmed) — preferable for secrets, since a value on the command line is visible in the process list (ps).

cooler-env add -e production -k API_KEY -v sk_live_123
printf %s "$SECRET" | cooler-env add -e production -k API_KEY   # value via stdin

edit

Opens an interactive prompt to pick an existing key and set a new value. The new value is hidden by default (no echo, no length shown), and the current value is not pre-filled — leave the prompt blank to keep the current value unchanged, so a stray Enter can't erase a secret. Pass --show to type in the clear, with the current value pre-filled as an editable default.

cooler-env edit -e development
cooler-env edit -e development --show   # reveal + pre-fill the current value

delete

Opens an interactive prompt to pick one or more keys to remove.

cooler-env delete -e development

list

Prints the key names for an environment, one per line, sorted (byte order). Pass --values to also print the values as KEY=value — a value that contains spaces, quotes, =, or a newline is quoted (KEY="...") so every key stays on its own line. This is a display format for reading, not for machine consumption; to generate a file to load, use export. This writes secrets to stdout, so use it deliberately.

cooler-env list -e development
cooler-env list -e development --values

export

Writes all of an environment's secrets to stdout as a .env file (default) or, with --shell, as export KEY=value lines you can source. Redirecting or eval-ing the output is how you get the secrets into a process without committing a plaintext file. This writes secrets to stdout, so use it deliberately — and create any file with a tight umask.

# Write a .env file (create it read/write for you only):
(umask 077; cooler-env export -e development > .env)

# Load straight into the current shell, no file on disk:
eval "$(cooler-env export -e development --shell)"

The two dialects encode awkward values differently:

  • Default (dotenv). Targets the npm dotenv parser and Node's util.parseEnv. Values are bare when safe, otherwise single-quoted — which those two parsers treat literally, round-tripping $, backticks, and backslashes. A value that contains a single quote, or a carriage return, prints a warning on stderr: its encoding may not survive every parser, and other ecosystems' loaders (python-dotenv, Ruby dotenv, Docker Compose) apply different quoting and interpolation rules. Use --shell when you need a byte-exact, loader-independent copy.
  • --shell. POSIX single-quoting that round-trips every value exactly in sh/bash/zsh. Keys must be valid shell identifiers; if any key isn't, the command aborts before writing a single line, so a failed eval is a clean no-op. Sourcing assigns real shell variables, so a key that shadows a special or read-only name (PATH, IFS, UID, …) takes effect as an assignment to it — export only environments whose keys you control.

An empty environment writes nothing to stdout (with a note on stderr), so export > .env yields an empty file rather than an error.

--help / --version

cooler-env --help      # usage, commands, and options
cooler-env --version   # the installed version

Unrecognized options and unexpected arguments are ignored with a warning on stderr rather than failing.

Programmatic API

loadEnv(env, options?)

Decrypts an environment's secrets and resolves to a Secrets object (Record<string, string>).

By default this is a pure read — it returns the secrets and touches nothing global. This is the recommended usage, since it keeps load order explicit and avoids clobbering real environment variables:

import { loadEnv } from "cooler-env";

const env = await loadEnv(process.env.NODE_ENV);
startServer({ apiKey: env.API_KEY, dbUrl: env.DATABASE_URL });

If you'd rather populate process.env globally (the classic behavior), opt in with inject:

await loadEnv(process.env.NODE_ENV, { inject: true });
// process.env.API_KEY is now set — unless it was already set in the shell/CI

Options

| Option | Type | Default | Description | | ------------ | --------- | ---------- | ----------------------------------------------------------------------------------------------------- | | configPath | string | "config" | Directory holding the encryption key and encrypted files. | | inject | boolean | false | Also write each secret into process.env. | | override | boolean | false | When injecting, overwrite variables already set in the environment. By default a shell/CI value wins. |

TypeScript

Types ship with the package:

import { loadEnv, type Secrets, type LoadEnvOptions } from "cooler-env";

const env: Secrets = await loadEnv("production", {
  inject: true,
} satisfies LoadEnvOptions);

Error handling

loadEnv rejects with a CoolerEnvError for expected failures — a missing key/encrypted file, a wrong key, or a tampered blob. It is exported so you can tell those apart from unexpected errors:

import { loadEnv, CoolerEnvError } from "cooler-env";

try {
  await loadEnv("production");
} catch (err) {
  if (err instanceof CoolerEnvError) {
    console.error(err.message); // e.g. "Encrypted file not found at /app/config/production.yml.enc."
  } else {
    throw err;
  }
}

Importing the package has no side effects. The CLI and library are kept separate, so import { loadEnv } from "cooler-env" never runs CLI code.

Security model

  • Cipher: aes-256-gcm (authenticated encryption) from Node's built-in crypto. No third-party crypto dependencies.
  • Key derivation: the AES key is derived from your secret key with scrypt against a per-write random salt.
  • On-disk format: a single binary blob — a 5-byte versioned header ([magic "CENV"(4)][version(1)]) followed by [salt(16)][iv(12)][authTag(16)][ciphertext]. A fresh salt + IV is generated on every write, so identical secrets never produce identical ciphertext. The header lets the KDF/cipher parameters evolve; headerless blobs written by older versions are still read.
  • Integrity: GCM's auth tag means a wrong key or any tampering throws instead of yielding corrupt output.
  • No plaintext on disk: writes produce ciphertext in memory and atomically rename a temp file over the encrypted file, so plaintext never lands on disk and an interrupted write can't leave a half-written file.
  • Prototype-pollution guard: decrypted payloads are sanitized to drop __proto__ / constructor / prototype keys before they reach your object or process.env.

The one rule that matters most: keep the .key file out of version control. init gitignores it for you.

Development

yarn install      # Yarn 4 (Berry), node-modules linker
yarn build        # tsc -> dist/
yarn test         # Jest
yarn lint         # ESLint

yarn test test/loadEnv.test.ts          # a single file
yarn test -t "returns secrets"          # a single test by name

CI runs lint + build + test across Node 20, 22, and 24 on every push and pull request.

License

MIT © Aaron Sood