This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
EnvX (envx-cli on npm) is a CLI tool for secure environment file encryption and management using GPG. It manages .env.<stage> files across environments (local, development, staging, production) with passphrases stored in .envrc (direnv convention) and project-level config in .envxrc (JSON).
# Build
npm run build # TypeScript compilation to dist/
# Development
npm run dev # Run via ts-node (src/index.ts)
# Test
npm test # Run all tests (Jest, ts-jest)
npm run test:core # Core unit tests only (__tests__/core/)
npm run test:integration # Integration tests only — REQUIRES `npm run build` first
npm run test:watch # Watch mode
npm run test:coverage # With coverage report
# Run a single test file
npx jest __tests__/core/file.test.ts
# Run a single test by name
npx jest -t "should encrypt environment file"
# Lint & Format
npm run lint # ESLint check (src + __tests__)
npm run lint:fix # ESLint auto-fix
npm run format # Prettier format
npm run format:check # Prettier check
# Release (uses release-it with conventional changelog)
npm run release:patch # Patch version bump
npm run release:minor # Minor version bump
npm run release:dry # Dry runIntegration tests in __tests__/integration/cli.test.ts shell out to the compiled dist/index.js, so always run npm run build before test:integration (or full npm test) when source has changed.
src/index.ts — Creates the Commander.js program, registers subcommands, and defines the inline list (alias ls), status, init, and version commands. Exported createProgram() allows module usage. The init command orchestrates the full first-run flow: detect environments, discover and offer to ignore non-secret ones, write .envxrc, update .gitignore, run interactive secret setup, and optionally encrypt selected environments.
Each command file in src/commands/ exports a createXxxCommand() function returning a Commander Command, plus an executeXxx() async function with the core logic. Commands available:
encrypt/decrypt— GPG operations on.env.<stage>files. Support--all(batch all environments),--overwrite,--dry-run.create— Create new.env.<stage>files (optionally from a template).copy— Copy a stage file to plain.env. Note:--allhere means all directories containing that environment, so-e/--environmentis required when--allis used (different semantics from encrypt/decrypt).interactive— Inquirer-driven setup for secrets in.envrc.run— Decrypt an env file in memory and spawn a sub-process with variables injected viaprocess.env. Supports-e <stage>, repeatable-f/--env-file, repeatable--env KEY=VAL,--overload, and--dry-run. dotenvx-style precedence:process.envwins over files by default. Never writes plaintext to disk. Seedocs/superpowers/specs/2026-04-07-envx-run-design.mdfor the full design.config— Manage.envxrc. Subcommands:show,ignore list/add/remove,exclude list/add/remove,reset.
All utilities in src/utils/ use static class methods, not instances:
ExecUtils(utils/exec.ts) — GPG operations (encryptFile,decryptFile,testGpgOperation), shell execution, GPG availability check,getCurrentDir().CliUtils(utils/exec.ts) — Formatted terminal output via chalk (success,error,warning,info,header,subheader,printTable,formatEnvironment,formatPath).FileUtils(utils/file.ts) — File discovery viafast-glob,.envrcparsing/writing,.envxrcparsing/writing/merging,.gitignoremanagement, ignore-pattern and exclude-dir resolution, hashing, backup helpers, path manipulation. HoldsDEFAULT_IGNORE_PATTERNS(example,sample,template) andDEFAULT_EXCLUDE_DIRS(node_modules,.git,dist,.next,.turbo,.output,.nuxt,.cache,build,coverage,.svelte-kit).InteractiveUtils(utils/interactive.ts) — User prompts viainquirer(confirmations, passphrase input, environment selection — single and multi).
Two distinct config files live alongside each project:
-
.envrc— direnv-style shell file (export KEY="val"). Holds the GPG passphrase(s). Git-ignored. Discovered by walking upward fromcwdviaFileUtils.readEnvrcNearest— commands run from a monorepo subdirectory find the.envrcat the repo root. -
.envxrc— JSON project config. Committable and meant to be shared. Discovered by walking upward viaFileUtils.findEnvxrcUpward. Optional fields:ignore: string[]— environment names to skip during discovery (case-insensitive exact match). When unset, falls back toFileUtils.DEFAULT_IGNORE_PATTERNS. An empty array[]is the explicit "no filtering" escape hatch.excludeDirs: string[]— directory names excluded fromfast-globwalks. When unset, falls back toFileUtils.DEFAULT_EXCLUDE_DIRS.environments: string[]— environments the user opted into managing duringenvx init.
Read/write through
FileUtils.readEnvxrc,writeEnvxrc, andmergeEnvxrc. Validated byenvxrcFileConfigSchemainsrc/schemas/index.ts(named to avoid collision with the olderenvrcConfigSchema).The upward walk stops at the first ancestor containing any of
.envrc,.envxrc, or.git— that ancestor is treated as the project root. If the project root doesn't contain the specific file being looked up, the walk returnsnulland callers fall back to their existing no-config behavior (e.g., passphrase prompt). Writes viaFileUtils.mergeEnvxrctarget the nearest existing.envxrc, soenvx config ignore addfrom a subdirectory edits the root config.
FileUtils.findAllEnvironments(cwd, ignorePatterns?) globs **/.env.* with the resolved excludeDirs ignored, then filters out ignored environment names. Pass undefined to use config/defaults; pass [] to disable filtering entirely (used by init to detect what's being auto-ignored).
FileUtils.findEnvFiles(env, cwd) returns both plain and .gpg-encrypted files for a given environment, marking each with an encrypted flag.
src/schemas/index.ts — Zod schemas validate all command inputs. The --all flag dynamically alters schema requirements:
encrypt/decrypt:--allmakesenvironmentforbidden (mutually exclusive).copy:--allmakesenvironmentrequired (it iterates over directories, not environments).
validateEncryptOptions / validateDecryptOptions swap to a permissive schema when --all is set.
src/types/index.ts — Core interfaces: CliOptions, EncryptOptions, DecryptOptions, CreateOptions, InteractiveOptions, EnvFile, StageSecret, EnvrcConfig (Record<string, string>), EnvxrcConfig ({ ignore?, environments?, excludeDirs? }), CommandResult, FileOperationResult, and ExitCode enum.
- Passphrase:
--passphraseflag >.envrcfile > interactive prompt. - Working directory:
--cwdflag >process.cwd(). - Ignore patterns / exclude dirs: explicit argument >
.envxrc> defaults onFileUtils.
- TypeScript strict mode with all strict checks enabled.
- Path alias
@/*maps tosrc/*(tsconfig). - Single quotes, semicolons, trailing commas (es5), 2-space indent, 80-char line width.
console.logis allowed (CLI tool) — ESLintno-consoleis off.- Pre-commit hooks run ESLint + Prettier via husky/lint-staged.
- Pre-existing
@typescript-eslint/no-explicit-anywarnings throughoutcommands/(raw Commander option objects) — leave them alone unless changing the surrounding logic.
Tests use Jest with ts-jest. Core tests validate schemas, file utilities, .envxrc handling, the --all flag matrix, and command workflows using mocks and temp directories (fs.mkdtemp cleaned in afterEach). Integration tests exec the actual built CLI via execSync against dist/index.js, so the build must be current. Tests focus on user-facing behavior — UI/cosmetic output (chalk colors, inquirer styling) is intentionally not tested.
When adding patterns to FileUtils.updateGitignore (the envPatterns / secretPatterns arrays), update the corresponding pattern-count assertions in __tests__/core/file.test.ts.
Commander.js converts dashed flags to camelCase in rawOptions (e.g. --dry-run → rawOptions.dryRun, --all-dirs → rawOptions.allDirs). Read flags off the raw options object accordingly.