@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 +20 -4
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +3 -2
- package/dist/commands/encrypt.js +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +36 -8
- package/dist/commands/migrate.d.ts +6 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +116 -0
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +46 -37
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +7 -6
- package/dist/types.js +4 -4
- package/package.json +1 -1
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 .
|
|
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 (
|
|
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
|
|
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;
|
|
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"}
|
package/dist/commands/check.js
CHANGED
|
@@ -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
|
|
210
|
-
lines.push(pc.dim(' 2. Delete
|
|
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'));
|
package/dist/commands/encrypt.js
CHANGED
|
@@ -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 .
|
|
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;
|
|
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"}
|
package/dist/commands/init.js
CHANGED
|
@@ -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('
|
|
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('\
|
|
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 (
|
|
183
|
-
console.log(pc.green(' 1. npx hush
|
|
184
|
-
console.log(pc.dim(' 2. npx hush inspect') + pc.dim('
|
|
185
|
-
console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim('
|
|
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('
|
|
189
|
-
console.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim('
|
|
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 @@
|
|
|
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;
|
|
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"}
|
package/dist/commands/skill.js
CHANGED
|
@@ -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 .
|
|
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
|
|
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
|
|
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
|
|
61
|
-
npx hush inspect # Confirm everything is
|
|
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: .
|
|
427
|
-
development: .
|
|
428
|
-
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 \`.
|
|
464
|
+
### Step 5: Create initial \`.hush\` files
|
|
465
465
|
|
|
466
|
-
Create \`.
|
|
466
|
+
Create \`.hush\` with shared secrets:
|
|
467
467
|
|
|
468
468
|
\`\`\`bash
|
|
469
|
-
# .
|
|
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 \`.
|
|
475
|
+
Create \`.hush.development\` for dev-specific values:
|
|
476
476
|
|
|
477
477
|
\`\`\`bash
|
|
478
|
-
# .
|
|
478
|
+
# .hush.development
|
|
479
479
|
DEBUG=true
|
|
480
480
|
LOG_LEVEL=debug
|
|
481
481
|
\`\`\`
|
|
482
482
|
|
|
483
|
-
Create \`.
|
|
483
|
+
Create \`.hush.production\` for production values:
|
|
484
484
|
|
|
485
485
|
\`\`\`bash
|
|
486
|
-
# .
|
|
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
|
-
- \`.
|
|
499
|
-
- \`.
|
|
500
|
-
- \`.
|
|
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
|
|
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
|
|
517
|
-
.env.development
|
|
518
|
-
.env.production
|
|
522
|
+
.env.*
|
|
519
523
|
.dev.vars
|
|
520
524
|
|
|
521
525
|
# Keep encrypted files (these ARE committed)
|
|
522
|
-
!.
|
|
523
|
-
!.
|
|
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 .
|
|
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
|
-
- [ ] \`.
|
|
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 \`.
|
|
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: .
|
|
993
|
-
development: .
|
|
994
|
-
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 (
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
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"}
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
|
62
|
-
console.log(pc.dim(' 2.
|
|
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
|
|
122
|
-
console.log(existsSync(
|
|
123
|
-
? pc.green(
|
|
124
|
-
: pc.dim(
|
|
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: '.
|
|
4
|
-
development: '.
|
|
5
|
-
production: '.
|
|
6
|
-
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: {
|