@chriscode/hush 4.2.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ import { resolveCommand } from './commands/resolve.js';
19
19
  import { traceCommand } from './commands/trace.js';
20
20
  import { templateCommand } from './commands/template.js';
21
21
  import { expansionsCommand } from './commands/expansions.js';
22
+ import { migrateCommand } from './commands/migrate.js';
22
23
  import { findConfigPath, loadConfig, checkSchemaVersion } from './config/loader.js';
23
24
  import { checkForUpdate } from './utils/version-check.js';
24
25
  const require = createRequire(import.meta.url);
@@ -32,7 +33,7 @@ ${pc.bold('Usage:')}
32
33
 
33
34
  ${pc.bold('Commands:')}
34
35
  init Initialize hush.yaml config
35
- encrypt Encrypt source .env files
36
+ encrypt Encrypt source .hush files
36
37
  run -- <cmd> Run command with secrets in memory (AI-safe)
37
38
  set <KEY> Set a single secret interactively (AI-safe)
38
39
  edit [file] Edit all secrets in $EDITOR
@@ -44,6 +45,7 @@ ${pc.bold('Commands:')}
44
45
  status Show configuration and status
45
46
  skill Install Claude Code / OpenCode skill
46
47
  keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
48
+ migrate Migrate from v4 (.env.encrypted) to v5 (.hush.encrypted)
47
49
 
48
50
  ${pc.bold('Debugging Commands:')}
49
51
  resolve <target> Show what variables a target receives (AI-safe)
@@ -72,7 +74,7 @@ ${pc.bold('Options:')}
72
74
  -h, --help Show this help message
73
75
  -v, --version Show version number
74
76
 
75
- ${pc.bold('Variable Expansion (v4+):')}
77
+ ${pc.bold('Variable Expansion (v5+):')}
76
78
  Subdirectory .env files can reference root secrets:
77
79
 
78
80
  \${VAR} Pull VAR from root secrets
@@ -90,9 +92,20 @@ ${pc.bold('Variable Expansion (v4+):')}
90
92
 
91
93
  Subdirectory templates are safe to commit - they contain no secrets.
92
94
 
95
+ ${pc.bold('File Naming (v5+):')}
96
+ Hush uses .hush files instead of .env to avoid conflicts with other tools:
97
+
98
+ .hush Shared secrets (source file)
99
+ .hush.development Development secrets (source file)
100
+ .hush.encrypted Encrypted shared secrets (committed)
101
+ .hush.development.encrypted Encrypted dev secrets (committed)
102
+
103
+ The .env files are reserved for other tools (Wrangler, Metro, etc.).
104
+
93
105
  ${pc.bold('Examples:')}
94
106
  hush init Initialize config + generate keys
95
- hush encrypt Encrypt .env files
107
+ hush migrate Migrate v4 .env.encrypted to v5 .hush.encrypted
108
+ hush encrypt Encrypt .hush files
96
109
  hush run -- npm start Run with secrets in memory (AI-safe!)
97
110
  hush run -e prod -- npm build Run with production secrets
98
111
  hush run -t api -- wrangler dev Run filtered for 'api' target (root secrets only)
@@ -282,7 +295,7 @@ function parseArgs(args) {
282
295
  return { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs };
283
296
  }
284
297
  function checkMigrationNeeded(root, command) {
285
- const skipCommands = ['', 'help', 'version', 'init', 'skill'];
298
+ const skipCommands = ['', 'help', 'version', 'init', 'skill', 'migrate'];
286
299
  if (skipCommands.includes(command))
287
300
  return;
288
301
  const configPath = findConfigPath(root);
@@ -403,6 +416,9 @@ async function main() {
403
416
  case 'expansions':
404
417
  await expansionsCommand({ root, env });
405
418
  break;
419
+ case 'migrate':
420
+ await migrateCommand({ root, dryRun });
421
+ break;
406
422
  default:
407
423
  if (command) {
408
424
  console.error(pc.red(`Unknown command: ${command}`));
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAmC,MAAM,aAAa,CAAC;AAkF/G,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BvE;AAuMD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAmC,MAAM,aAAa,CAAC;AAmF/G,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BvE;AAuMD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
@@ -44,6 +44,7 @@ function getGitChangedFiles(root) {
44
44
  }
45
45
  function findPlaintextEnvFiles(root) {
46
46
  const results = [];
47
+ // Only warn about .env files (legacy/output files), NOT .hush files (Hush's source files)
47
48
  const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
48
49
  const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt']);
49
50
  function scanDir(dir, relativePath = '') {
@@ -206,8 +207,8 @@ function formatTextOutput(result) {
206
207
  lines.push(pc.yellow('These files contain plaintext secrets that could be exposed to AI assistants.'));
207
208
  lines.push('');
208
209
  lines.push(pc.bold('To fix:'));
209
- lines.push(pc.dim(' 1. Run: hush encrypt'));
210
- lines.push(pc.dim(' 2. Delete the plaintext files (the .encrypted versions are your source of truth)'));
210
+ lines.push(pc.dim(' 1. Run: hush migrate (if upgrading from v4)'));
211
+ lines.push(pc.dim(' 2. Delete or gitignore these .env files'));
211
212
  lines.push(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars'));
212
213
  lines.push('');
213
214
  lines.push(pc.dim('To allow plaintext files (not recommended): --allow-plaintext'));
@@ -34,7 +34,7 @@ export async function encryptCommand(options) {
34
34
  }
35
35
  if (encryptedFiles.length === 0) {
36
36
  console.error(pc.red('\nNo source files found to encrypt'));
37
- console.error(pc.dim('Create at least .env with your secrets'));
37
+ console.error(pc.dim('Create at least .hush with your secrets'));
38
38
  process.exit(1);
39
39
  }
40
40
  console.log(pc.blue('\nVerifying encryption...'));
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,WAAW,EAAU,MAAM,aAAa,CAAC;AAyJnE,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAsErE"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,WAAW,EAAU,MAAM,aAAa,CAAC;AAyKnE,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFrE"}
@@ -126,6 +126,7 @@ function detectTargets(root) {
126
126
  return targets;
127
127
  }
128
128
  function findExistingPlaintextEnvFiles(root) {
129
+ // Look for legacy .env files that may need migration
129
130
  const patterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
130
131
  const found = [];
131
132
  for (const pattern of patterns) {
@@ -136,6 +137,18 @@ function findExistingPlaintextEnvFiles(root) {
136
137
  }
137
138
  return found;
138
139
  }
140
+ function findExistingEncryptedFiles(root) {
141
+ // Look for v4 encrypted files that need migration to v5 (.hush.encrypted)
142
+ const patterns = ['.env.encrypted', '.env.development.encrypted', '.env.production.encrypted', '.env.local.encrypted'];
143
+ const found = [];
144
+ for (const pattern of patterns) {
145
+ const filePath = join(root, pattern);
146
+ if (existsSync(filePath)) {
147
+ found.push(pattern);
148
+ }
149
+ }
150
+ return found;
151
+ }
139
152
  export async function initCommand(options) {
140
153
  const { root } = options;
141
154
  const existingConfig = findConfigPath(root);
@@ -144,14 +157,24 @@ export async function initCommand(options) {
144
157
  process.exit(1);
145
158
  }
146
159
  console.log(pc.blue('Initializing hush...\n'));
160
+ const existingEncryptedFiles = findExistingEncryptedFiles(root);
161
+ if (existingEncryptedFiles.length > 0) {
162
+ console.log(pc.bgYellow(pc.black(' V4 ENCRYPTED FILES DETECTED ')));
163
+ console.log(pc.yellow('\nFound existing v4 encrypted files:'));
164
+ for (const file of existingEncryptedFiles) {
165
+ console.log(pc.yellow(` ${file}`));
166
+ }
167
+ console.log(pc.dim('\nRun "npx hush migrate" to convert to v5 format (.hush.encrypted).\n'));
168
+ }
147
169
  const existingEnvFiles = findExistingPlaintextEnvFiles(root);
148
170
  if (existingEnvFiles.length > 0) {
149
- console.log(pc.bgYellow(pc.black(' EXISTING SECRETS DETECTED ')));
171
+ console.log(pc.bgYellow(pc.black(' PLAINTEXT .ENV FILES DETECTED ')));
150
172
  console.log(pc.yellow('\nFound existing .env files:'));
151
173
  for (const file of existingEnvFiles) {
152
174
  console.log(pc.yellow(` ${file}`));
153
175
  }
154
- console.log(pc.dim('\nThese will be encrypted after setup. Run "npx hush encrypt" when ready.\n'));
176
+ console.log(pc.dim('\nRename these to .hush files, then run "npx hush encrypt".\n'));
177
+ console.log(pc.dim('Example: mv .env .hush && mv .env.development .hush.development\n'));
155
178
  }
156
179
  const project = getProjectFromPackageJson(root);
157
180
  if (!project) {
@@ -179,14 +202,19 @@ export async function initCommand(options) {
179
202
  console.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
180
203
  }
181
204
  console.log(pc.bold('\nNext steps:'));
182
- if (existingEnvFiles.length > 0) {
183
- console.log(pc.green(' 1. npx hush encrypt') + pc.dim(' # Encrypt existing .env files (deletes plaintext)'));
184
- console.log(pc.dim(' 2. npx hush inspect') + pc.dim(' # Verify your secrets'));
185
- console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
205
+ if (existingEncryptedFiles.length > 0) {
206
+ console.log(pc.green(' 1. npx hush migrate') + pc.dim(' # Convert v4 .env.encrypted to v5 .hush.encrypted'));
207
+ console.log(pc.dim(' 2. npx hush inspect') + pc.dim(' # Verify your secrets'));
208
+ console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
209
+ }
210
+ else if (existingEnvFiles.length > 0) {
211
+ console.log(pc.green(' 1. Rename .env files to .hush') + pc.dim(' # mv .env .hush'));
212
+ console.log(pc.dim(' 2. npx hush encrypt') + pc.dim(' # Encrypt .hush files'));
213
+ console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
186
214
  }
187
215
  else {
188
- console.log(pc.dim(' 1. npx hush set <KEY>') + pc.dim(' # Add secrets interactively'));
189
- console.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
216
+ console.log(pc.dim(' 1. npx hush set <KEY>') + pc.dim(' # Add secrets interactively'));
217
+ console.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
190
218
  }
191
219
  console.log(pc.dim('\nGit setup:'));
192
220
  console.log(pc.dim(' git add hush.yaml .sops.yaml'));
@@ -0,0 +1,6 @@
1
+ export interface MigrateOptions {
2
+ root: string;
3
+ dryRun: boolean;
4
+ }
5
+ export declare function migrateCommand(options: MigrateOptions): Promise<void>;
6
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AA8DD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA0E3E"}
@@ -0,0 +1,116 @@
1
+ import { existsSync, renameSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
5
+ import { findConfigPath } from '../config/loader.js';
6
+ const FILE_MIGRATIONS = [
7
+ { from: '.env.encrypted', to: '.hush.encrypted' },
8
+ { from: '.env.development.encrypted', to: '.hush.development.encrypted' },
9
+ { from: '.env.production.encrypted', to: '.hush.production.encrypted' },
10
+ { from: '.env.local.encrypted', to: '.hush.local.encrypted' },
11
+ ];
12
+ const SOURCE_MIGRATIONS = {
13
+ '.env': '.hush',
14
+ '.env.development': '.hush.development',
15
+ '.env.production': '.hush.production',
16
+ '.env.local': '.hush.local',
17
+ };
18
+ function getMigrationFiles(root) {
19
+ return FILE_MIGRATIONS.map(({ from, to }) => ({
20
+ from,
21
+ to,
22
+ exists: existsSync(join(root, from)),
23
+ }));
24
+ }
25
+ function migrateConfig(root, dryRun) {
26
+ const configPath = findConfigPath(root);
27
+ if (!configPath)
28
+ return false;
29
+ const content = readFileSync(configPath, 'utf-8');
30
+ const config = parseYaml(content);
31
+ let modified = false;
32
+ const sources = config.sources;
33
+ if (sources) {
34
+ for (const [oldValue, newValue] of Object.entries(SOURCE_MIGRATIONS)) {
35
+ for (const [key, value] of Object.entries(sources)) {
36
+ if (value === oldValue) {
37
+ if (!dryRun) {
38
+ sources[key] = newValue;
39
+ }
40
+ modified = true;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ if (modified && !dryRun) {
46
+ const schemaComment = content.startsWith('#') ? content.split('\n')[0] + '\n' : '';
47
+ const newContent = schemaComment + stringifyYaml(config, { indent: 2 });
48
+ writeFileSync(configPath, newContent, 'utf-8');
49
+ }
50
+ return modified;
51
+ }
52
+ export async function migrateCommand(options) {
53
+ const { root, dryRun } = options;
54
+ console.log(pc.blue('Hush v4 → v5 Migration\n'));
55
+ if (dryRun) {
56
+ console.log(pc.yellow('DRY RUN - no changes will be made\n'));
57
+ }
58
+ const migrations = getMigrationFiles(root);
59
+ const filesToMigrate = migrations.filter(m => m.exists);
60
+ if (filesToMigrate.length === 0) {
61
+ console.log(pc.dim('No v4 encrypted files found (.env.encrypted, etc.)'));
62
+ console.log(pc.dim('Already on v5 or no encrypted files exist.\n'));
63
+ const configNeedsMigration = migrateConfig(root, true);
64
+ if (configNeedsMigration) {
65
+ console.log(pc.yellow('hush.yaml contains v4 source paths that need updating.\n'));
66
+ if (!dryRun) {
67
+ migrateConfig(root, false);
68
+ console.log(pc.green('Updated hush.yaml source paths to v5 format.\n'));
69
+ }
70
+ }
71
+ return;
72
+ }
73
+ console.log(pc.bold('Files to migrate:'));
74
+ for (const { from, to, exists } of migrations) {
75
+ if (exists) {
76
+ console.log(` ${pc.yellow(from)} → ${pc.green(to)}`);
77
+ }
78
+ else {
79
+ console.log(pc.dim(` ${from} (not found, skipping)`));
80
+ }
81
+ }
82
+ console.log('');
83
+ if (dryRun) {
84
+ console.log(pc.dim('Run without --dry-run to apply changes.'));
85
+ return;
86
+ }
87
+ let migratedCount = 0;
88
+ for (const { from, to, exists } of migrations) {
89
+ if (!exists)
90
+ continue;
91
+ const fromPath = join(root, from);
92
+ const toPath = join(root, to);
93
+ if (existsSync(toPath)) {
94
+ console.log(pc.yellow(` Skipping ${from}: ${to} already exists`));
95
+ continue;
96
+ }
97
+ renameSync(fromPath, toPath);
98
+ console.log(pc.green(` Migrated ${from} → ${to}`));
99
+ migratedCount++;
100
+ }
101
+ const configUpdated = migrateConfig(root, false);
102
+ if (configUpdated) {
103
+ console.log(pc.green(' Updated hush.yaml source paths'));
104
+ }
105
+ console.log('');
106
+ if (migratedCount > 0 || configUpdated) {
107
+ console.log(pc.green(pc.bold(`Migration complete.`)));
108
+ console.log(pc.dim('\nNext steps:'));
109
+ console.log(pc.dim(' 1. git add .hush.encrypted .hush.*.encrypted hush.yaml'));
110
+ console.log(pc.dim(' 2. git rm .env.encrypted .env.*.encrypted (if tracked)'));
111
+ console.log(pc.dim(' 3. git commit -m "chore: migrate to Hush v5 format"'));
112
+ }
113
+ else {
114
+ console.log(pc.dim('No changes made.'));
115
+ }
116
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA8hDhD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
1
+ {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAuiDhD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
@@ -6,15 +6,15 @@ import pc from 'picocolors';
6
6
  const SKILL_FILES = {
7
7
  'SKILL.md': `---
8
8
  name: hush-secrets
9
- description: Manage secrets safely using Hush CLI. Use when working with .env files, environment variables, secrets, API keys, database URLs, credentials, or configuration. NEVER read .env files directly - always use hush commands instead to prevent exposing secrets to the LLM.
9
+ description: Manage secrets safely using Hush CLI. Use when working with .hush files, environment variables, secrets, API keys, database URLs, credentials, or configuration. NEVER read .hush files directly - always use hush commands instead to prevent exposing secrets to the LLM.
10
10
  allowed-tools: Bash(hush:*), Bash(npx hush:*), Bash(brew:*), Bash(npm:*), Bash(pnpm:*), Bash(age-keygen:*), Read, Grep, Glob, Write, Bash(cat:*), Bash(grep:*)
11
11
  ---
12
12
 
13
13
  # Hush - AI-Native Secrets Management
14
14
 
15
- **CRITICAL: NEVER read root .env files directly.** Always use \`npx hush status\`, \`npx hush inspect\`, or \`npx hush has\` to check secrets.
15
+ **CRITICAL: NEVER read .hush files directly.** Always use \`npx hush status\`, \`npx hush inspect\`, or \`npx hush has\` to check secrets.
16
16
 
17
- Hush keeps secrets **encrypted at rest** at the project root. Subdirectory \`.env\` files are **templates** (safe to commit and read) that reference root secrets via \`\${VAR}\` syntax.
17
+ Hush keeps secrets **encrypted at rest** at the project root using \`.hush.encrypted\` files. Subdirectory \`.env\` files are **templates** (safe to commit and read) that reference root secrets via \`\${VAR}\` syntax.
18
18
 
19
19
  ## First Step: Investigate Current State
20
20
 
@@ -35,7 +35,7 @@ This tells you:
35
35
 
36
36
  | You See | What It Means | Action |
37
37
  |---------|---------------|--------|
38
- | \`SECURITY WARNING: Unencrypted .env files\` | Plaintext secrets at project root! | Run \`npx hush encrypt\` immediately |
38
+ | \`SECURITY WARNING: Unencrypted .env files\` | Plaintext .env files found (legacy or output) | Run \`npx hush migrate\` or delete them |
39
39
  | \`No hush.yaml found\` | Hush not initialized | Run \`npx hush init\` |
40
40
  | \`SOPS not installed\` | Missing prerequisite | \`brew install sops\` |
41
41
  | \`age key not found\` | Missing encryption key | \`npx hush keys setup\` |
@@ -53,12 +53,12 @@ npx hush encrypt # Encrypts any existing .env files, deletes plaintext
53
53
  npx hush inspect # Verify setup
54
54
  \`\`\`
55
55
 
56
- ### Scenario 2: Existing .env Files Found
56
+ ### Scenario 2: Existing .env Files Found (Migration from v4)
57
57
 
58
58
  \`\`\`bash
59
59
  npx hush status # Check what's there
60
- npx hush encrypt # Encrypt them (auto-deletes plaintext after verification)
61
- npx hush inspect # Confirm everything is encrypted
60
+ npx hush migrate # Migrate .env.encrypted to .hush.encrypted
61
+ npx hush inspect # Confirm everything is migrated
62
62
  \`\`\`
63
63
 
64
64
  ### Scenario 3: Hush Already Set Up (Team Member Joining)
@@ -423,9 +423,9 @@ The generated config looks like:
423
423
 
424
424
  \`\`\`yaml
425
425
  sources:
426
- shared: .env
427
- development: .env.development
428
- production: .env.production
426
+ shared: .hush
427
+ development: .hush.development
428
+ production: .hush.production
429
429
 
430
430
  targets:
431
431
  - name: root
@@ -461,29 +461,29 @@ Customize targets for your monorepo. Common patterns:
461
461
  format: yaml
462
462
  \`\`\`
463
463
 
464
- ### Step 5: Create initial \`.env\` files
464
+ ### Step 5: Create initial \`.hush\` files
465
465
 
466
- Create \`.env\` with shared secrets:
466
+ Create \`.hush\` with shared secrets:
467
467
 
468
468
  \`\`\`bash
469
- # .env
469
+ # .hush
470
470
  DATABASE_URL=postgres://localhost/mydb
471
471
  API_KEY=your_api_key_here
472
472
  NEXT_PUBLIC_API_URL=http://localhost:3000
473
473
  \`\`\`
474
474
 
475
- Create \`.env.development\` for dev-specific values:
475
+ Create \`.hush.development\` for dev-specific values:
476
476
 
477
477
  \`\`\`bash
478
- # .env.development
478
+ # .hush.development
479
479
  DEBUG=true
480
480
  LOG_LEVEL=debug
481
481
  \`\`\`
482
482
 
483
- Create \`.env.production\` for production values:
483
+ Create \`.hush.production\` for production values:
484
484
 
485
485
  \`\`\`bash
486
- # .env.production
486
+ # .hush.production
487
487
  DEBUG=false
488
488
  LOG_LEVEL=error
489
489
  \`\`\`
@@ -495,9 +495,9 @@ npx hush encrypt
495
495
  \`\`\`
496
496
 
497
497
  This creates:
498
- - \`.env.encrypted\`
499
- - \`.env.development.encrypted\`
500
- - \`.env.production.encrypted\`
498
+ - \`.hush.encrypted\`
499
+ - \`.hush.development.encrypted\`
500
+ - \`.hush.production.encrypted\`
501
501
 
502
502
  ### Step 7: Verify setup
503
503
 
@@ -511,22 +511,26 @@ npx hush inspect
511
511
  Add these lines to \`.gitignore\`:
512
512
 
513
513
  \`\`\`gitignore
514
- # Hush - plaintext env files (generated, not committed)
514
+ # Hush - plaintext source files (encrypted versions are committed)
515
+ .hush
516
+ .hush.local
517
+ .hush.development
518
+ .hush.production
519
+
520
+ # Output files (generated by hush decrypt, not committed)
515
521
  .env
516
- .env.local
517
- .env.development
518
- .env.production
522
+ .env.*
519
523
  .dev.vars
520
524
 
521
525
  # Keep encrypted files (these ARE committed)
522
- !.env.encrypted
523
- !.env.*.encrypted
526
+ !.hush.encrypted
527
+ !.hush.*.encrypted
524
528
  \`\`\`
525
529
 
526
530
  ### Step 9: Commit encrypted files
527
531
 
528
532
  \`\`\`bash
529
- git add .sops.yaml hush.yaml .env*.encrypted .gitignore
533
+ git add .sops.yaml hush.yaml .hush*.encrypted .gitignore
530
534
  git commit -m "chore: add Hush secrets management"
531
535
  \`\`\`
532
536
 
@@ -552,8 +556,8 @@ After setup, verify everything works:
552
556
  - [ ] \`npx hush status\` shows configuration
553
557
  - [ ] \`npx hush inspect\` shows masked variables
554
558
  - [ ] \`npx hush run -- env\` can decrypt and run (secrets stay in memory!)
555
- - [ ] \`.env.encrypted\` files are committed to git
556
- - [ ] Plaintext \`.env\` files are in \`.gitignore\`
559
+ - [ ] \`.hush.encrypted\` files are committed to git
560
+ - [ ] Plaintext \`.hush\` and \`.env\` files are in \`.gitignore\`
557
561
 
558
562
  ---
559
563
 
@@ -701,7 +705,7 @@ hush init
701
705
 
702
706
  ### hush encrypt
703
707
 
704
- Encrypt source \`.env\` files to \`.env.encrypted\` files.
708
+ Encrypt source \`.hush\` files to \`.hush.encrypted\` files.
705
709
 
706
710
  \`\`\`bash
707
711
  hush encrypt
@@ -989,9 +993,9 @@ hush skill --local # Install to ./.claude/skills/
989
993
 
990
994
  \`\`\`yaml
991
995
  sources:
992
- shared: .env
993
- development: .env.development
994
- production: .env.production
996
+ shared: .hush
997
+ development: .hush.development
998
+ production: .hush.production
995
999
 
996
1000
  targets:
997
1001
  - name: root
@@ -1046,7 +1050,7 @@ targets:
1046
1050
  | Expo | \`EXPO_PUBLIC_*\` | \`include: [EXPO_PUBLIC_*]\` |
1047
1051
  | Gatsby | \`GATSBY_*\` | \`include: [GATSBY_*]\` |
1048
1052
 
1049
- ### Variable Interpolation (v4+)
1053
+ ### Variable Interpolation (v5+)
1050
1054
 
1051
1055
  Reference other variables using \`\${VAR}\` syntax:
1052
1056
 
@@ -1112,8 +1116,13 @@ This will show:
1112
1116
 
1113
1117
  #### Path A: "SECURITY WARNING: Unencrypted .env files detected"
1114
1118
  \`\`\`bash
1119
+ # If migrating from v4 (has .env.encrypted files):
1120
+ npx hush migrate # Converts to .hush.encrypted format
1121
+
1122
+ # If new setup with plaintext .env files:
1123
+ mv .env .hush # Rename to .hush
1115
1124
  npx hush init # If no hush.yaml exists
1116
- npx hush encrypt # Encrypts files and DELETES plaintext automatically
1125
+ npx hush encrypt # Encrypts .hush files
1117
1126
  npx hush status # Verify the warning is gone
1118
1127
  \`\`\`
1119
1128
 
@@ -1495,11 +1504,11 @@ Key Status:
1495
1504
  \`\`\`
1496
1505
 
1497
1506
  **Reading this:**
1498
- - There's a security issue - plaintext files exist
1507
+ - There are .env files present (legacy or output from decrypt)
1499
1508
  - The project is configured with key management
1500
1509
  - Keys are properly set up and backed up
1501
1510
 
1502
- **To fix:** Run \`npx hush encrypt\`
1511
+ **To fix:** Run \`npx hush migrate\` (if v4) or delete/gitignore these .env files
1503
1512
 
1504
1513
  ### npx hush inspect output explained
1505
1514
 
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwCjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmHzE"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAyCjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmHzE"}
@@ -9,6 +9,7 @@ import { opInstalled } from '../lib/onepassword.js';
9
9
  import { FORMAT_OUTPUT_FILES } from '../types.js';
10
10
  function findRootPlaintextEnvFiles(root) {
11
11
  const results = [];
12
+ // Only warn about .env files (legacy/output), not .hush files (Hush's source files)
12
13
  const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
13
14
  for (const pattern of plaintextPatterns) {
14
15
  const filePath = join(root, pattern);
@@ -58,8 +59,8 @@ export async function statusCommand(options) {
58
59
  console.log('');
59
60
  console.log(pc.yellow('These files may expose secrets to AI assistants and version control.'));
60
61
  console.log(pc.bold('\nTo fix:'));
61
- console.log(pc.dim(' 1. Run: npx hush encrypt'));
62
- console.log(pc.dim(' 2. The plaintext files will be automatically deleted after encryption'));
62
+ console.log(pc.dim(' 1. Run: npx hush migrate (if upgrading from v4)'));
63
+ console.log(pc.dim(' 2. Delete or gitignore these .env files'));
63
64
  console.log(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars\n'));
64
65
  }
65
66
  console.log(pc.bold('Config:'));
@@ -118,10 +119,10 @@ export async function statusCommand(options) {
118
119
  ? pc.green(` ${label}`)
119
120
  : pc.dim(` ${label} (not found)`));
120
121
  }
121
- const localPath = join(root, '.env.local');
122
- console.log(existsSync(localPath)
123
- ? pc.green(' .env.local (overrides)')
124
- : pc.dim(' .env.local (optional, not found)'));
122
+ const localEncryptedPath = join(root, config.sources.local + '.encrypted');
123
+ console.log(existsSync(localEncryptedPath)
124
+ ? pc.green(` ${config.sources.local}.encrypted (overrides)`)
125
+ : pc.dim(` ${config.sources.local}.encrypted (optional, not found)`));
125
126
  console.log(pc.bold('\nTargets:'));
126
127
  for (const target of config.targets) {
127
128
  const filter = describeFilter(target);
package/dist/types.js CHANGED
@@ -1,9 +1,9 @@
1
1
  export const CURRENT_SCHEMA_VERSION = 2;
2
2
  export const DEFAULT_SOURCES = {
3
- shared: '.env',
4
- development: '.env.development',
5
- production: '.env.production',
6
- local: '.env.local',
3
+ shared: '.hush',
4
+ development: '.hush.development',
5
+ production: '.hush.production',
6
+ local: '.hush.local',
7
7
  };
8
8
  export const FORMAT_OUTPUT_FILES = {
9
9
  dotenv: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscode/hush",
3
- "version": "4.2.0",
3
+ "version": "5.0.0",
4
4
  "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
5
  "type": "module",
6
6
  "bin": {