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

envsitter

v0.0.4

Published

Safely inspect and match .env secrets without exposing values

Readme

envsitter

Safely inspect and match .env secrets without ever printing values.

envsitter is designed for LLM/agent workflows where you want to:

  • List keys present in an env source (.env file or external provider)
  • Compare a key’s value to a candidate value you provide at runtime ("outside-in")
  • Do bulk matching (one candidate against many keys, or candidates-by-key)
  • Produce deterministic fingerprints for comparisons/auditing
  • Ask boolean questions about values (empty/prefix/suffix/regex/type-ish checks) without ever returning the value

Related: https://github.com/boxpositron/envsitter-guard — an OpenCode plugin that blocks agents/tools from reading or editing sensitive .env* files (preventing accidental secret leaks), while still allowing safe inspection via EnvSitter-style tools (keys + deterministic fingerprints; never values).

Security model (what this tool does and does not do)

  • Values are read in-process for comparisons, but never returned by the library API and never printed by the CLI.
  • Deterministic matching uses HMAC-SHA-256 with a local pepper.
    • This avoids publishing raw SHA-256 hashes that are easy to dictionary-guess.
  • Candidate secrets should be passed via stdin (--candidate-stdin) to avoid shell history.

Non-goals:

  • This tool is not a secret manager.
  • This tool does not encrypt or relocate .env values; it operates on sources in-place.

Install

npm install envsitter

Or run the CLI without installing globally:

npx envsitter keys --file .env

Pepper (required for deterministic fingerprints)

envsitter uses a local "pepper" as the HMAC key.

Resolution order:

  1. process.env.ENVSITTER_PEPPER (or ENV_SITTER_PEPPER)
  2. Pepper file at .envsitter/pepper (auto-created if missing)

The pepper file is created with mode 0600 when possible, and .envsitter/ is gitignored.

CLI usage

Quick reference

| Command | Description | |---------|-------------| | keys | List all keys in an env file | | fingerprint | Get deterministic fingerprint for a key | | match | Match candidate value(s) against key(s) | | match-by-key | Bulk match candidates by key | | scan | Detect value shapes (JWT, URL, base64) | | validate | Check dotenv syntax | | copy | Copy keys between env files | | format / reorder | Sort and organize env files | | annotate | Add comments to keys | | add | Add new key (fails if exists) | | set | Create or update key | | unset | Set key to empty value | | delete | Remove key(s) from file |

Commands

  • keys --file <path> [--filter-regex <re>]
  • fingerprint --file <path> --key <KEY>
  • match --file <path> (--key <KEY> | --keys <K1,K2> | --all-keys) [--op <op>] [--candidate <value> | --candidate-stdin]
  • match-by-key --file <path> (--candidates-json <json> | --candidates-stdin)
  • scan --file <path> [--keys-regex <re>] [--detect jwt,url,base64]
  • validate --file <path>
  • copy --from <path> --to <path> [--keys <K1,K2>] [--include-regex <re>] [--exclude-regex <re>] [--rename <A=B,C=D>] [--on-conflict error|skip|overwrite] [--write]
  • format --file <path> [--mode sections|global] [--sort alpha|none] [--write]
  • reorder --file <path> [--mode sections|global] [--sort alpha|none] [--write]
  • annotate --file <path> --key <KEY> --comment <text> [--line <n>] [--write]
  • add --file <path> --key <KEY> [--value <v> | --value-stdin] [--write]
  • set --file <path> --key <KEY> [--value <v> | --value-stdin] [--write]
  • unset --file <path> --key <KEY> [--write]
  • delete --file <path> (--key <KEY> | --keys <K1,K2>) [--write]

Notes for file operations:

  • Commands that modify files (copy, format/reorder, annotate, add, set, unset, delete) are dry-run unless --write is provided.
  • These commands never print secret values; output includes keys, booleans, and line numbers only.
  • When targeting example files (.env.example, .env.sample, .env.template, .env.dist, .env.default), a warning is emitted. Use --no-example-warning to suppress.
  • Non-standard env file names are fully supported (e.g., api.env, database.env, config.env.local).
  • Values with special characters (spaces, #, quotes, newlines) are automatically double-quoted with proper escaping.

List keys

envsitter keys --file .env

Filter by key name (regex):

envsitter keys --file .env --filter-regex "/(KEY|TOKEN|SECRET)/i"

Fingerprint a single key

envsitter fingerprint --file .env --key OPENAI_API_KEY

Outputs JSON containing the key’s fingerprint and metadata (never the value).

Match a candidate against a single key (recommended via stdin)

node -e "process.stdout.write('candidate-secret')" \
  | envsitter match --file .env --key OPENAI_API_KEY --candidate-stdin --json

Exit codes:

  • 0 match found
  • 1 no match
  • 2 error/usage

Match operators (for humans)

envsitter match supports an --op flag.

  • Default: --op is_equal
  • When --op is_equal is used, EnvSitter hashes both the candidate and stored value with the local pepper (HMAC-SHA-256) and compares digests using constant-time equality.
  • Other operators evaluate against the raw value in-process, but still only return booleans/match results (no values are returned or printed).

Operators:

  • exists: key is present in the source (no candidate required)
  • is_empty: value is exactly empty string (no candidate required)
  • is_equal: deterministic match against a candidate value (candidate required)
  • partial_match_prefix: value.startsWith(candidate) (candidate required)
  • partial_match_suffix: value.endsWith(candidate) (candidate required)
  • partial_match_regex: regex test against value (candidate required; candidate is a regex like "/^sk-/" or a raw regex body)
  • is_number: value parses as a finite number (no candidate required)
  • is_boolean: value is true/false (case-insensitive, whitespace-trimmed) (no candidate required)
  • is_string: value is neither is_number nor is_boolean (no candidate required)

Examples:

# Prefix match
node -e "process.stdout.write('sk-')" \
  | envsitter match --file .env --key OPENAI_API_KEY --op partial_match_prefix --candidate-stdin

# Regex match (regex literal syntax)
node -e "process.stdout.write('/^sk-[a-z]+-/i')" \
  | envsitter match --file .env --key OPENAI_API_KEY --op partial_match_regex --candidate-stdin

# Exists (no candidate)
envsitter match --file .env --key OPENAI_API_KEY --op exists --json

Match one candidate against multiple keys

node -e "process.stdout.write('candidate-secret')" \
  | envsitter match --file .env --keys OPENAI_API_KEY,ANTHROPIC_API_KEY --candidate-stdin --json

Match one candidate against all keys

node -e "process.stdout.write('candidate-secret')" \
  | envsitter match --file .env --all-keys --candidate-stdin --json

Match candidates-by-key (bulk assignment)

Provide a JSON object mapping key -> candidate value.

envsitter match-by-key --file .env \
  --candidates-json '{"OPENAI_API_KEY":"sk-...","ANTHROPIC_API_KEY":"sk-..."}'

For safer input, pass the JSON via stdin:

cat candidates.json | envsitter match-by-key --file .env --candidates-stdin

Scan for value shapes (no values returned)

envsitter scan --file .env --detect jwt,url,base64

Optionally restrict which keys to scan:

envsitter scan --file .env --keys-regex "/(JWT|URL)/" --detect jwt,url

Validate dotenv syntax

envsitter validate --file .env
envsitter validate --file .env --json

Copy keys between env files (production → staging)

Dry-run (no file is modified):

envsitter copy --from .env.production --to .env.staging --keys API_URL,REDIS_URL --json

Apply changes:

envsitter copy --from .env.production --to .env.staging --keys API_URL,REDIS_URL --on-conflict overwrite --write --json

Rename while copying:

envsitter copy --from .env.production --to .env.staging --keys DATABASE_URL --rename DATABASE_URL=STAGING_DATABASE_URL --write

Annotate keys with comments

envsitter annotate --file .env --key DATABASE_URL --comment "prod only" --write

Reorder/format env files

envsitter format --file .env --mode sections --sort alpha --write
# alias:
envsitter reorder --file .env --mode sections --sort alpha --write

Add a new key (fails if key exists)

envsitter add --file .env --key NEW_API_KEY --value "sk-xxx" --write
# or via stdin (recommended to avoid shell history):
node -e "process.stdout.write('sk-xxx')" | envsitter add --file .env --key NEW_API_KEY --value-stdin --write

Set a key (creates or updates)

envsitter set --file .env --key API_KEY --value "new-value" --write
# or via stdin:
node -e "process.stdout.write('new-value')" | envsitter set --file .env --key API_KEY --value-stdin --write

Unset a key (set to empty value)

envsitter unset --file .env --key OLD_KEY --write

Delete keys

# Single key:
envsitter delete --file .env --key DEPRECATED_KEY --write

# Multiple keys:
envsitter delete --file .env --keys OLD_KEY,UNUSED_KEY,LEGACY_KEY --write

Output contract (for LLMs)

General rules:

  • Never output secret values; treat all values as sensitive.
  • Prefer --candidate-stdin over --candidate to avoid shell history.
  • Exit codes: 0 match found, 1 no match, 2 error/usage.

JSON outputs:

  • keys --json -> { "keys": string[] }
  • fingerprint -> { "key": string, "algorithm": "hmac-sha256", "fingerprint": string, "length": number, "pepperSource": "env"|"file", "pepperFilePath"?: string }
  • match --json (single key) ->
    • default op (not provided): { "key": string, "match": boolean }
    • with --op: { "key": string, "op": string, "match": boolean }
  • match --json (bulk keys / all keys) ->
    • default op (not provided): { "matches": Array<{ "key": string, "match": boolean }> }
    • with --op: { "op": string, "matches": Array<{ "key": string, "match": boolean }> }
  • match-by-key --json -> { "matches": Array<{ "key": string, "match": boolean }> }
  • scan --json -> { "findings": Array<{ "key": string, "detections": Array<"jwt"|"url"|"base64"> }> }
  • validate --json -> { "ok": boolean, "issues": Array<{ "line": number, "column": number, "message": string }> }
  • copy --json -> { "from": string, "to": string, "onConflict": string, "willWrite": boolean, "wrote": boolean, "hasChanges": boolean, "issues": Array<...>, "plan": Array<...> }
  • format --json / reorder --json -> { "file": string, "mode": string, "sort": string, "willWrite": boolean, "wrote": boolean, "hasChanges": boolean, "issues": Array<...> }
  • annotate --json -> { "file": string, "willWrite": boolean, "wrote": boolean, "hasChanges": boolean, "issues": Array<...>, "plan": { ... } }
  • add --json / set --json / unset --json -> { "file": string, "key": string, "willWrite": boolean, "wrote": boolean, "hasChanges": boolean, "issues": Array<...>, "plan": { "key": string, "action": "added"|"updated"|"unset"|"key_exists"|"not_found"|"no_change", "line"?: number } }
  • delete --json -> { "file": string, "keys": string[], "willWrite": boolean, "wrote": boolean, "hasChanges": boolean, "issues": Array<...>, "plan": Array<{ "key": string, "action": "deleted"|"not_found", "line"?: number }> }

Library API

Basic usage

import { EnvSitter } from 'envsitter';

const es = EnvSitter.fromDotenvFile('.env');

const keys = await es.listKeys();
const fp = await es.fingerprintKey('OPENAI_API_KEY');
const match = await es.matchCandidate('OPENAI_API_KEY', 'candidate-secret');

File operations via the library

import {
  addEnvFileKey,
  annotateEnvFile,
  copyEnvFileKeys,
  deleteEnvFileKeys,
  formatEnvFile,
  setEnvFileKey,
  unsetEnvFileKey,
  validateEnvFile
} from 'envsitter';

await validateEnvFile('.env');

await copyEnvFileKeys({
  from: '.env.production',
  to: '.env.staging',
  keys: ['API_URL', 'REDIS_URL'],
  onConflict: 'overwrite',
  write: true
});

await annotateEnvFile({ file: '.env', key: 'DATABASE_URL', comment: 'prod only', write: true });
await formatEnvFile({ file: '.env', mode: 'sections', sort: 'alpha', write: true });

// Add a new key (fails if exists)
await addEnvFileKey({ file: '.env', key: 'NEW_KEY', value: 'new_value', write: true });

// Set a key (creates or updates)
await setEnvFileKey({ file: '.env', key: 'API_KEY', value: 'updated_value', write: true });

// Unset a key (set to empty)
await unsetEnvFileKey({ file: '.env', key: 'OLD_KEY', write: true });

// Delete keys
await deleteEnvFileKeys({ file: '.env', keys: ['DEPRECATED', 'UNUSED'], write: true });

Utility functions

import { isExampleEnvFile } from 'envsitter';

// Detect example/template env files
isExampleEnvFile('.env.example');      // true
isExampleEnvFile('api.env.sample');    // true
isExampleEnvFile('.env');              // false
isExampleEnvFile('api.env');           // false

Match operators via the library

import { EnvSitter } from 'envsitter';
import type { EnvSitterMatcher } from 'envsitter';

const es = EnvSitter.fromDotenvFile('.env');

const matcher: EnvSitterMatcher = { op: 'partial_match_prefix', prefix: 'sk-' };
const ok = await es.matchKey('OPENAI_API_KEY', matcher);

Bulk matching

import { EnvSitter } from 'envsitter';

const es = EnvSitter.fromDotenvFile('.env');

// One candidate tested against a set of keys
const matches = await es.matchCandidateBulk(['OPENAI_API_KEY', 'ANTHROPIC_API_KEY'], 'candidate-secret');

// One matcher tested against a set of keys
const prefixMatches = await es.matchKeyBulk(['OPENAI_API_KEY', 'ANTHROPIC_API_KEY'], { op: 'partial_match_prefix', prefix: 'sk-' });

// Candidates-by-key
const byKey = await es.matchCandidatesByKey({
  OPENAI_API_KEY: 'sk-...',
  ANTHROPIC_API_KEY: 'sk-...'
});

External sources (hooks)

You can load dotenv-formatted output from another tool/secret provider:

import { EnvSitter } from 'envsitter';

const es = EnvSitter.fromExternalCommand('my-secret-provider', ['export', '--format=dotenv']);
const keys = await es.listKeys();

Development

npm install
npm run typecheck
npm test

Run a single test file:

npm run build
node --test dist/test/envsitter.test.js

Run a single test by name:

npm run build
node --test --test-name-pattern "outside-in" dist/test/envsitter.test.js

License

MIT. See LICENSE.