@chriscode/hush 2.6.0 → 2.7.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
@@ -15,7 +15,8 @@ import { checkCommand } from './commands/check.js';
15
15
  import { skillCommand } from './commands/skill.js';
16
16
  import { keysCommand } from './commands/keys.js';
17
17
  import { findConfigPath, loadConfig, checkSchemaVersion } from './config/loader.js';
18
- const VERSION = '2.3.0';
18
+ import { checkForUpdate } from './utils/version-check.js';
19
+ const VERSION = '2.5.1';
19
20
  function printHelp() {
20
21
  console.log(`
21
22
  ${pc.bold('hush')} - SOPS-based secrets management for monorepos
@@ -269,6 +270,9 @@ async function main() {
269
270
  process.exit(0);
270
271
  }
271
272
  const { command, subcommand, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs } = parseArgs(args);
273
+ if (command !== 'run' && !json && !quiet) {
274
+ checkForUpdate(VERSION);
275
+ }
272
276
  checkMigrationNeeded(root, command);
273
277
  try {
274
278
  switch (command) {
@@ -1 +1 @@
1
- {"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../src/commands/encrypt.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC3E"}
1
+ {"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../src/commands/encrypt.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AASlD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAsF3E"}
@@ -1,18 +1,19 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, unlinkSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import pc from 'picocolors';
4
4
  import { loadConfig } from '../config/loader.js';
5
- import { encrypt as sopsEncrypt } from '../core/sops.js';
5
+ import { encrypt as sopsEncrypt, decrypt as sopsDecrypt } from '../core/sops.js';
6
+ import { parseEnvContent } from '../core/parse.js';
6
7
  export async function encryptCommand(options) {
7
8
  const { root } = options;
8
9
  const config = loadConfig(root);
9
- console.log(pc.blue('Encrypting secrets...'));
10
+ console.log(pc.blue('Encrypting secrets...\n'));
10
11
  const sourceFiles = [
11
12
  { key: 'shared', path: config.sources.shared },
12
13
  { key: 'development', path: config.sources.development },
13
14
  { key: 'production', path: config.sources.production },
14
15
  ];
15
- let encryptedCount = 0;
16
+ const encryptedFiles = [];
16
17
  for (const { key, path } of sourceFiles) {
17
18
  const sourcePath = join(root, path);
18
19
  const encryptedPath = sourcePath + '.encrypted';
@@ -20,15 +21,60 @@ export async function encryptCommand(options) {
20
21
  console.log(pc.dim(` ${path} - not found, skipping`));
21
22
  continue;
22
23
  }
24
+ const sourceContent = readFileSync(sourcePath, 'utf-8');
25
+ const vars = parseEnvContent(sourceContent);
23
26
  sopsEncrypt(sourcePath, encryptedPath);
24
- encryptedCount++;
25
- console.log(pc.green(` ${path}`) + pc.dim(` -> ${path}.encrypted`));
27
+ console.log(pc.green(` ${path}`) + pc.dim(` -> ${path}.encrypted (${vars.length} vars)`));
28
+ encryptedFiles.push({
29
+ sourcePath,
30
+ encryptedPath,
31
+ displayPath: path,
32
+ originalKeyCount: vars.length,
33
+ });
26
34
  }
27
- if (encryptedCount === 0) {
35
+ if (encryptedFiles.length === 0) {
28
36
  console.error(pc.red('\nNo source files found to encrypt'));
29
37
  console.error(pc.dim('Create at least .env with your secrets'));
30
38
  process.exit(1);
31
39
  }
32
- console.log(pc.green(`\nEncrypted ${encryptedCount} file(s)`));
33
- console.log(pc.dim('You can now commit the .encrypted files to git'));
40
+ console.log(pc.blue('\nVerifying encryption...'));
41
+ let allVerified = true;
42
+ for (const file of encryptedFiles) {
43
+ try {
44
+ const decrypted = sopsDecrypt(file.encryptedPath);
45
+ const decryptedVars = parseEnvContent(decrypted);
46
+ if (decryptedVars.length === file.originalKeyCount) {
47
+ console.log(pc.green(` ${file.displayPath}.encrypted - verified (${decryptedVars.length} vars)`));
48
+ }
49
+ else {
50
+ console.log(pc.yellow(` ${file.displayPath}.encrypted - warning: expected ${file.originalKeyCount} vars, got ${decryptedVars.length}`));
51
+ allVerified = false;
52
+ }
53
+ }
54
+ catch (error) {
55
+ console.log(pc.red(` ${file.displayPath}.encrypted - FAILED to decrypt`));
56
+ console.log(pc.dim(` ${error.message}`));
57
+ allVerified = false;
58
+ }
59
+ }
60
+ if (!allVerified) {
61
+ console.log(pc.yellow('\nEncryption completed but verification failed.'));
62
+ console.log(pc.yellow('Plaintext files have NOT been deleted. Please check your setup.'));
63
+ process.exit(1);
64
+ }
65
+ console.log(pc.blue('\nCleaning up plaintext files...'));
66
+ for (const file of encryptedFiles) {
67
+ try {
68
+ unlinkSync(file.sourcePath);
69
+ console.log(pc.green(` Deleted ${file.displayPath}`));
70
+ }
71
+ catch (error) {
72
+ console.log(pc.yellow(` Could not delete ${file.displayPath}: ${error.message}`));
73
+ }
74
+ }
75
+ console.log(pc.green(pc.bold(`\n✓ Encrypted ${encryptedFiles.length} file(s) and removed plaintext`)));
76
+ console.log(pc.dim('\nNext steps:'));
77
+ console.log(pc.dim(' 1. Commit the .encrypted files to git'));
78
+ console.log(pc.dim(' 2. Use "npx hush run -- <command>" to run with secrets'));
79
+ console.log(pc.dim(' 3. Use "npx hush inspect" to see what variables are set'));
34
80
  }
@@ -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;AA2InE,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAyCrE"}
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,CAoErE"}
@@ -125,6 +125,17 @@ function detectTargets(root) {
125
125
  }
126
126
  return targets;
127
127
  }
128
+ function findExistingPlaintextEnvFiles(root) {
129
+ const patterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
130
+ const found = [];
131
+ for (const pattern of patterns) {
132
+ const filePath = join(root, pattern);
133
+ if (existsSync(filePath)) {
134
+ found.push(pattern);
135
+ }
136
+ }
137
+ return found;
138
+ }
128
139
  export async function initCommand(options) {
129
140
  const { root } = options;
130
141
  const existingConfig = findConfigPath(root);
@@ -132,8 +143,21 @@ export async function initCommand(options) {
132
143
  console.error(pc.red(`Config already exists: ${existingConfig}`));
133
144
  process.exit(1);
134
145
  }
135
- console.log(pc.blue('Initializing hush...'));
146
+ console.log(pc.blue('Initializing hush...\n'));
147
+ const existingEnvFiles = findExistingPlaintextEnvFiles(root);
148
+ if (existingEnvFiles.length > 0) {
149
+ console.log(pc.bgYellow(pc.black(' EXISTING SECRETS DETECTED ')));
150
+ console.log(pc.yellow('\nFound existing .env files:'));
151
+ for (const file of existingEnvFiles) {
152
+ console.log(pc.yellow(` ${file}`));
153
+ }
154
+ console.log(pc.dim('\nThese will be encrypted after setup. Run "npx hush encrypt" when ready.\n'));
155
+ }
136
156
  const project = getProjectFromPackageJson(root);
157
+ if (!project) {
158
+ console.log(pc.yellow('No project identifier found in package.json.'));
159
+ console.log(pc.dim('Tip: Add "project: my-org/my-repo" to hush.yaml after creation for key management.\n'));
160
+ }
137
161
  const keyResult = await setupKey(root, project);
138
162
  if (keyResult) {
139
163
  createSopsConfig(root, keyResult.publicKey);
@@ -152,7 +176,17 @@ export async function initCommand(options) {
152
176
  for (const target of targets) {
153
177
  console.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
154
178
  }
155
- console.log(pc.dim('\nNext steps:'));
156
- console.log(' 1. Run "hush set <KEY>" to add secrets');
157
- console.log(' 2. Run "hush run -- <command>" to run with secrets in memory');
179
+ console.log(pc.bold('\nNext steps:'));
180
+ if (existingEnvFiles.length > 0) {
181
+ console.log(pc.green(' 1. npx hush encrypt') + pc.dim(' # Encrypt existing .env files (deletes plaintext)'));
182
+ console.log(pc.dim(' 2. npx hush inspect') + pc.dim(' # Verify your secrets'));
183
+ console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
184
+ }
185
+ else {
186
+ console.log(pc.dim(' 1. npx hush set <KEY>') + pc.dim(' # Add secrets interactively'));
187
+ console.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
188
+ }
189
+ console.log(pc.dim('\nGit setup:'));
190
+ console.log(pc.dim(' git add hush.yaml .sops.yaml'));
191
+ console.log(pc.dim(' git commit -m "chore: add Hush secrets management"'));
158
192
  }
@@ -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;AAwhChD,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;AA2jChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
@@ -6,53 +6,102 @@ 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. Secrets are always encrypted at rest - .env files contain only encrypted data.
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.
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
- Hush keeps secrets **encrypted at rest**. All \`.env\` files contain encrypted data only—you can freely read them with \`cat\` or \`grep\` and you'll only see encrypted gibberish, never actual secrets.
15
+ **CRITICAL: NEVER read .env files directly.** Always use \`npx hush status\`, \`npx hush inspect\`, or \`npx hush has\` to check secrets.
16
16
 
17
- ## How It Works
17
+ Hush keeps secrets **encrypted at rest**. When properly set up, all secrets are stored in \`.env.encrypted\` files and plaintext \`.env\` files should NOT exist.
18
18
 
19
- Secrets are stored encrypted on disk. When you need to use them:
20
- - \`hush run -- <command>\` decrypts to memory and runs your command
21
- - \`hush set <KEY>\` adds secrets interactively (you invoke, user enters value)
22
- - \`hush inspect\` shows what exists with masked values
23
- - \`hush edit\` opens encrypted file in editor, re-encrypts on save
19
+ ## First Step: Investigate Current State
24
20
 
25
- ## Safe to Read (Always Encrypted)
21
+ **ALWAYS run this first when working with a new repo:**
26
22
 
27
- You CAN freely read these files—they only contain encrypted data:
28
- - \`.env.encrypted\`, \`.env.*.encrypted\` - encrypted secrets
29
- - \`.env\`, \`.env.*\` - if they exist, they're encrypted too (Hush doesn't create plaintext files)
23
+ \`\`\`bash
24
+ npx hush status
25
+ \`\`\`
26
+
27
+ This tells you:
28
+ - Whether Hush is configured (\`hush.yaml\` exists)
29
+ - If SOPS/age are installed
30
+ - If encryption keys are set up
31
+ - **CRITICAL: If unencrypted .env files exist (security risk!)**
32
+ - What source files are configured
33
+
34
+ ### Interpreting Status Output
30
35
 
31
- Feel free to use \`cat\`, \`grep\`, \`Read\` on any \`.env\` file. You'll see encrypted content like:
36
+ | You See | What It Means | Action |
37
+ |---------|---------------|--------|
38
+ | \`SECURITY WARNING: Unencrypted .env files\` | Plaintext secrets exist! | Run \`npx hush encrypt\` immediately |
39
+ | \`No hush.yaml found\` | Hush not initialized | Run \`npx hush init\` |
40
+ | \`SOPS not installed\` | Missing prerequisite | \`brew install sops\` |
41
+ | \`age key not found\` | Missing encryption key | \`npx hush keys setup\` |
42
+ | \`Project: not set\` | Key management limited | Add \`project:\` to hush.yaml |
43
+ | \`1Password backup: not synced\` | Key not backed up | \`npx hush keys push\` |
44
+
45
+ ## Decision Tree: What Do I Do?
46
+
47
+ ### Scenario 1: Fresh Repo (No Hush Setup)
48
+
49
+ \`\`\`bash
50
+ npx hush init # Creates hush.yaml and .sops.yaml
51
+ npx hush encrypt # Encrypts any existing .env files, deletes plaintext
52
+ npx hush inspect # Verify setup
32
53
  \`\`\`
33
- DATABASE_URL=ENC[AES256_GCM,data:abc123...,type:str]
54
+
55
+ ### Scenario 2: Existing .env Files Found
56
+
57
+ \`\`\`bash
58
+ npx hush status # Check what's there
59
+ npx hush encrypt # Encrypt them (auto-deletes plaintext after verification)
60
+ npx hush inspect # Confirm everything is encrypted
34
61
  \`\`\`
35
62
 
36
- ## Commands Reference
63
+ ### Scenario 3: Hush Already Set Up (Team Member Joining)
37
64
 
38
- ### Primary Commands:
39
- - \`npx hush run -- <command>\` - Run programs with secrets (decrypts to memory only!)
40
- - \`npx hush set <KEY>\` - Add a secret interactively (you invoke, user enters value)
41
- - \`npx hush edit\` - Let user edit all secrets in $EDITOR
42
- - \`npx hush inspect\` - See what variables exist (values are masked)
43
- - \`npx hush has <KEY>\` - Check if a specific variable is set
44
- - \`npx hush status\` - View configuration
65
+ \`\`\`bash
66
+ npx hush keys setup # Pull key from 1Password or prompt for setup
67
+ npx hush status # Verify everything works
68
+ npx hush inspect # See what secrets exist
69
+ \`\`\`
45
70
 
46
- ### Avoid These (Deprecated):
47
- - \`hush decrypt\` / \`hush unsafe:decrypt\` - Writes unencrypted secrets to disk (defeats the purpose!)
71
+ ### Scenario 4: Need to Add/Modify Secrets
48
72
 
49
- ## Quick Check: Is Hush Set Up?
73
+ \`\`\`bash
74
+ npx hush set <KEY> # Add interactively (you invoke, user types value)
75
+ npx hush edit # Edit all secrets in $EDITOR
76
+ npx hush inspect # Verify changes
77
+ \`\`\`
78
+
79
+ ### Scenario 5: Run Application with Secrets
50
80
 
51
81
  \`\`\`bash
52
- npx hush status
82
+ npx hush run -- npm start # Development
83
+ npx hush run -e production -- npm build # Production
53
84
  \`\`\`
54
85
 
55
- **If this fails**, see [SETUP.md](SETUP.md) for first-time setup instructions.
86
+ ---
87
+
88
+ ## Commands Quick Reference
89
+
90
+ | Command | Purpose | When to Use |
91
+ |---------|---------|-------------|
92
+ | \`npx hush status\` | **Full diagnostic** | First step, always |
93
+ | \`npx hush inspect\` | See variables (masked) | Check what's configured |
94
+ | \`npx hush has <KEY>\` | Check specific variable | Verify a secret exists |
95
+ | \`npx hush set <KEY>\` | Add secret interactively | User needs to enter a value |
96
+ | \`npx hush edit\` | Edit all secrets | Bulk editing |
97
+ | \`npx hush run -- <cmd>\` | Run with secrets in memory | Actually use the secrets |
98
+ | \`npx hush init\` | Initialize Hush | First-time setup |
99
+ | \`npx hush encrypt\` | Encrypt .env files | After creating/modifying plaintext |
100
+ | \`npx hush keys setup\` | Set up encryption keys | New team member |
101
+
102
+ ### Commands to AVOID:
103
+ - \`hush decrypt\` - Writes plaintext to disk (security risk!)
104
+ - \`cat .env\` - Never read plaintext .env files directly
56
105
 
57
106
  ---
58
107
 
@@ -66,14 +115,11 @@ npx hush run -e production -- npm build # Run with production secrets
66
115
  npx hush run -t api -- wrangler dev # Run filtered for 'api' target
67
116
  \`\`\`
68
117
 
69
- The secrets are decrypted to memory and injected as environment variables.
70
- The child process inherits them. No plaintext files are written.
71
-
72
118
  ---
73
119
 
74
120
  ## Checking Secrets
75
121
 
76
- ### See what variables exist (human-readable)
122
+ ### See what variables exist
77
123
 
78
124
  \`\`\`bash
79
125
  npx hush inspect # Development
@@ -99,14 +145,6 @@ npx hush has DATABASE_URL # Verbose output
99
145
  npx hush has API_KEY -q # Quiet: exit code only (0=set, 1=missing)
100
146
  \`\`\`
101
147
 
102
- ### Read encrypted files directly
103
-
104
- You can also just read the encrypted files:
105
- \`\`\`bash
106
- cat .env.encrypted # See encrypted content (safe!)
107
- grep DATABASE .env.encrypted # Search for keys in encrypted file
108
- \`\`\`
109
-
110
148
  ---
111
149
 
112
150
  ## Adding/Modifying Secrets
@@ -120,45 +158,7 @@ npx hush set DEBUG --local # Set personal local override
120
158
  \`\`\`
121
159
 
122
160
  The user will be prompted to enter the value (hidden input).
123
- You never see the actual secret - just invoke the command!
124
-
125
- ### Edit all secrets in editor
126
-
127
- \`\`\`bash
128
- npx hush edit # Edit shared secrets
129
- npx hush edit development # Edit development secrets
130
- npx hush edit local # Edit personal overrides
131
- \`\`\`
132
-
133
- ---
134
-
135
- ## Common Workflows
136
-
137
- ### "Help user add DATABASE_URL"
138
- \`\`\`bash
139
- npx hush set DATABASE_URL
140
- \`\`\`
141
- Tell user: "Enter your database URL when prompted"
142
-
143
- ### "Check all required secrets"
144
- \`\`\`bash
145
- npx hush has DATABASE_URL -q && npx hush has API_KEY -q && echo "All configured" || echo "Some missing"
146
- \`\`\`
147
-
148
- ### "Run the development server"
149
- \`\`\`bash
150
- npx hush run -- npm run dev
151
- \`\`\`
152
-
153
- ### "Build for production"
154
- \`\`\`bash
155
- npx hush run -e production -- npm run build
156
- \`\`\`
157
-
158
- ### "See what's in the encrypted file"
159
- \`\`\`bash
160
- cat .env.encrypted # Safe! Shows encrypted data only
161
- \`\`\`
161
+ **You never see the actual secret - just invoke the command!**
162
162
 
163
163
  ---
164
164
 
@@ -796,32 +796,71 @@ targets:
796
796
 
797
797
  Step-by-step examples for common workflows when working with secrets.
798
798
 
799
- **Remember:** All \`.env\` files are encrypted at rest. You can freely read them with \`cat\` or \`grep\`—you'll only see encrypted data, never actual secrets.
799
+ **CRITICAL: NEVER read .env files directly. Use hush commands instead.**
800
+
801
+ ---
802
+
803
+ ## First-Time Setup (Most Important!)
804
+
805
+ ### "Help me set up Hush for this project"
806
+
807
+ **Step 1: Check current state**
808
+ \`\`\`bash
809
+ npx hush status
810
+ \`\`\`
811
+
812
+ This will show:
813
+ - If Hush is already configured
814
+ - If there are unencrypted .env files (security risk!)
815
+ - What prerequisites are missing
816
+
817
+ **Step 2: Based on the output, follow the appropriate path:**
818
+
819
+ #### Path A: "SECURITY WARNING: Unencrypted .env files detected"
820
+ \`\`\`bash
821
+ npx hush init # If no hush.yaml exists
822
+ npx hush encrypt # Encrypts files and DELETES plaintext automatically
823
+ npx hush status # Verify the warning is gone
824
+ \`\`\`
825
+
826
+ #### Path B: "No hush.yaml found"
827
+ \`\`\`bash
828
+ npx hush init # Creates config and sets up keys
829
+ npx hush set <KEY> # Add secrets (if none exist yet)
830
+ \`\`\`
831
+
832
+ #### Path C: "age key not found"
833
+ \`\`\`bash
834
+ npx hush keys setup # Pull from 1Password or generate new key
835
+ \`\`\`
836
+
837
+ #### Path D: Everything looks good
838
+ \`\`\`bash
839
+ npx hush inspect # See what secrets are configured
840
+ \`\`\`
841
+
842
+ ---
800
843
 
801
844
  ## Running Programs (Most Common)
802
845
 
803
846
  ### "Start the development server"
804
-
805
847
  \`\`\`bash
806
- hush run -- npm run dev
848
+ npx hush run -- npm run dev
807
849
  \`\`\`
808
850
 
809
851
  ### "Build for production"
810
-
811
852
  \`\`\`bash
812
- hush run -e production -- npm run build
853
+ npx hush run -e production -- npm run build
813
854
  \`\`\`
814
855
 
815
856
  ### "Run tests with secrets"
816
-
817
857
  \`\`\`bash
818
- hush run -- npm test
858
+ npx hush run -- npm test
819
859
  \`\`\`
820
860
 
821
861
  ### "Run Wrangler for Cloudflare Worker"
822
-
823
862
  \`\`\`bash
824
- hush run -t api -- wrangler dev
863
+ npx hush run -t api -- wrangler dev
825
864
  \`\`\`
826
865
 
827
866
  ---
@@ -829,66 +868,53 @@ hush run -t api -- wrangler dev
829
868
  ## Checking Secrets
830
869
 
831
870
  ### "What environment variables does this project use?"
832
-
833
871
  \`\`\`bash
834
- hush inspect # Human-readable masked output
835
- # or
836
- cat .env.encrypted # Raw encrypted file (safe!)
872
+ npx hush inspect # Shows all variables with masked values
837
873
  \`\`\`
838
874
 
839
875
  ### "Is the database configured?"
840
-
841
876
  \`\`\`bash
842
- hush has DATABASE_URL
877
+ npx hush has DATABASE_URL
843
878
  \`\`\`
844
879
 
845
- If "not found", help user add it with \`hush set DATABASE_URL\`.
880
+ If "not found", help user add it:
881
+ \`\`\`bash
882
+ npx hush set DATABASE_URL
883
+ \`\`\`
884
+ Tell user: "Enter your database URL when prompted"
846
885
 
847
886
  ### "Check all required secrets"
848
-
849
887
  \`\`\`bash
850
- hush has DATABASE_URL -q && \\
851
- hush has API_KEY -q && \\
888
+ npx hush has DATABASE_URL -q && \\
889
+ npx hush has API_KEY -q && \\
852
890
  echo "All configured" || \\
853
891
  echo "Some missing"
854
892
  \`\`\`
855
893
 
856
- ### "Search for a key in encrypted files"
857
-
858
- \`\`\`bash
859
- grep DATABASE .env.encrypted # Safe! Shows encrypted line
860
- \`\`\`
861
-
862
894
  ---
863
895
 
864
896
  ## Adding Secrets
865
897
 
866
898
  ### "Help me add DATABASE_URL"
867
-
868
899
  \`\`\`bash
869
- hush set DATABASE_URL
900
+ npx hush set DATABASE_URL
870
901
  \`\`\`
871
-
872
902
  Tell user: "Enter your database URL when prompted (input will be hidden)"
873
903
 
874
904
  ### "Add a production-only secret"
875
-
876
905
  \`\`\`bash
877
- hush set STRIPE_SECRET_KEY -e production
906
+ npx hush set STRIPE_SECRET_KEY -e production
878
907
  \`\`\`
879
908
 
880
909
  ### "Add a personal local override"
881
-
882
910
  \`\`\`bash
883
- hush set DEBUG --local
911
+ npx hush set DEBUG --local
884
912
  \`\`\`
885
913
 
886
914
  ### "Edit multiple secrets at once"
887
-
888
915
  \`\`\`bash
889
- hush edit
916
+ npx hush edit
890
917
  \`\`\`
891
-
892
918
  Tell user: "Your editor will open. Add or modify secrets, then save and close."
893
919
 
894
920
  ---
@@ -899,27 +925,22 @@ Tell user: "Your editor will open. Add or modify secrets, then save and close."
899
925
 
900
926
  1. Check if it exists:
901
927
  \`\`\`bash
902
- hush has DATABASE_URL
928
+ npx hush has DATABASE_URL
903
929
  \`\`\`
904
930
 
905
931
  2. Check target distribution:
906
932
  \`\`\`bash
907
- hush inspect
933
+ npx hush inspect
908
934
  \`\`\`
909
935
 
910
936
  3. Check hush.yaml for filtering:
911
937
  \`\`\`bash
912
- cat hush.yaml
913
- \`\`\`
914
-
915
- 4. Look at the encrypted file:
916
- \`\`\`bash
917
- grep DATABASE .env.encrypted # Safe to read!
938
+ cat hush.yaml # Safe - this is config, not secrets
918
939
  \`\`\`
919
940
 
920
- 5. Try running directly:
941
+ 4. Try running directly:
921
942
  \`\`\`bash
922
- hush run -- env | grep DATABASE
943
+ npx hush run -- env | grep DATABASE
923
944
  \`\`\`
924
945
 
925
946
  ---
@@ -928,17 +949,25 @@ Tell user: "Your editor will open. Add or modify secrets, then save and close."
928
949
 
929
950
  ### "New team member setup"
930
951
 
931
- Guide them:
932
- > 1. Get the age private key from a team member
933
- > 2. Save it to \`~/.config/sops/age/key.txt\`
934
- > 3. Run \`hush run -- npm install\` to verify setup
935
- > 4. Start developing with \`hush run -- npm run dev\`
952
+ Guide them through these steps:
953
+ \`\`\`bash
954
+ # 1. Pull key from 1Password (or get from team member)
955
+ npx hush keys setup
936
956
 
937
- ### "Someone added new secrets"
957
+ # 2. Verify setup
958
+ npx hush status
959
+
960
+ # 3. Check secrets are accessible
961
+ npx hush inspect
938
962
 
963
+ # 4. Start developing
964
+ npx hush run -- npm run dev
965
+ \`\`\`
966
+
967
+ ### "Someone added new secrets"
939
968
  \`\`\`bash
940
969
  git pull
941
- hush inspect # See what's new
970
+ npx hush inspect # See what's new
942
971
  \`\`\`
943
972
 
944
973
  ---
@@ -946,24 +975,50 @@ hush inspect # See what's new
946
975
  ## Deployment
947
976
 
948
977
  ### "Push to Cloudflare Workers"
949
-
950
978
  \`\`\`bash
951
- hush push --dry-run # Preview first
952
- hush push # Actually push
979
+ npx hush push --dry-run # Preview first
980
+ npx hush push # Actually push
953
981
  \`\`\`
954
982
 
955
983
  ### "Build and deploy"
956
-
957
984
  \`\`\`bash
958
- hush run -e production -- npm run build
959
- hush push
985
+ npx hush run -e production -- npm run build
986
+ npx hush push
960
987
  \`\`\`
961
988
 
962
989
  ---
963
990
 
964
991
  ## Understanding the Output
965
992
 
966
- ### hush inspect output explained
993
+ ### npx hush status output explained
994
+
995
+ \`\`\`
996
+ SECURITY WARNING
997
+ Unencrypted .env files detected!
998
+ .env
999
+ .env.development
1000
+
1001
+ Config:
1002
+ hush.yaml
1003
+ Project: my-org/my-repo
1004
+
1005
+ Prerequisites:
1006
+ SOPS installed
1007
+ age key configured
1008
+
1009
+ Key Status:
1010
+ Local key: ~/.config/sops/age/keys/my-org-my-repo.txt
1011
+ 1Password backup: synced
1012
+ \`\`\`
1013
+
1014
+ **Reading this:**
1015
+ - There's a security issue - plaintext files exist
1016
+ - The project is configured with key management
1017
+ - Keys are properly set up and backed up
1018
+
1019
+ **To fix:** Run \`npx hush encrypt\`
1020
+
1021
+ ### npx hush inspect output explained
967
1022
 
968
1023
  \`\`\`
969
1024
  Secrets for development:
@@ -973,32 +1028,12 @@ Secrets for development:
973
1028
  API_KEY = (not set)
974
1029
 
975
1030
  Total: 3 variables
976
-
977
- Target distribution:
978
-
979
- root (.) - 3 vars
980
- app (./app/) - 1 vars
981
- include: EXPO_PUBLIC_*
982
- api (./api/) - 2 vars
983
- exclude: EXPO_PUBLIC_*
984
1031
  \`\`\`
985
1032
 
986
1033
  **Reading this:**
987
1034
  - \`DATABASE_URL\` is set, starts with "post", is 45 characters (likely a postgres:// URL)
988
1035
  - \`STRIPE_SECRET_KEY\` starts with "sk_t" (Stripe test key format)
989
1036
  - \`API_KEY\` is not set - user needs to add it
990
- - The \`app\` folder only gets \`EXPO_PUBLIC_*\` variables
991
- - The \`api\` folder gets everything except \`EXPO_PUBLIC_*\`
992
-
993
- ### Reading encrypted files directly
994
-
995
- \`\`\`bash
996
- $ cat .env.encrypted
997
- DATABASE_URL=ENC[AES256_GCM,data:7xH2kL9...,iv:abc...,tag:xyz...,type:str]
998
- STRIPE_SECRET_KEY=ENC[AES256_GCM,data:mN3pQ8...,iv:def...,tag:uvw...,type:str]
999
- \`\`\`
1000
-
1001
- This is safe to view—the actual values are encrypted. You can see what keys exist without exposing secrets.
1002
1037
  `,
1003
1038
  };
1004
1039
  function getSkillPath(location, root) {
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAkEzE"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA8DjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAyHzE"}
@@ -1,15 +1,91 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import pc from 'picocolors';
4
4
  import { findConfigPath, loadConfig } from '../config/loader.js';
5
5
  import { describeFilter } from '../core/filter.js';
6
6
  import { isAgeKeyConfigured, isSopsInstalled } from '../core/sops.js';
7
+ import { keyExists } from '../lib/age.js';
8
+ import { opAvailable, opListKeys } from '../lib/onepassword.js';
7
9
  import { FORMAT_OUTPUT_FILES } from '../types.js';
10
+ function findPlaintextEnvFiles(root) {
11
+ const results = [];
12
+ const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
13
+ const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt']);
14
+ function scanDir(dir, relativePath = '') {
15
+ let entries;
16
+ try {
17
+ entries = readdirSync(dir);
18
+ }
19
+ catch {
20
+ return;
21
+ }
22
+ for (const entry of entries) {
23
+ if (skipDirs.has(entry))
24
+ continue;
25
+ if (entry.endsWith('.encrypted'))
26
+ continue;
27
+ const fullPath = join(dir, entry);
28
+ const relPath = relativePath ? `${relativePath}/${entry}` : entry;
29
+ try {
30
+ if (statSync(fullPath).isDirectory()) {
31
+ scanDir(fullPath, relPath);
32
+ }
33
+ else if (plaintextPatterns.includes(entry)) {
34
+ results.push(relPath);
35
+ }
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ }
41
+ }
42
+ scanDir(root);
43
+ return results;
44
+ }
45
+ function getProjectFromConfig(root) {
46
+ const config = loadConfig(root);
47
+ if (config.project)
48
+ return config.project;
49
+ const pkgPath = join(root, 'package.json');
50
+ if (existsSync(pkgPath)) {
51
+ try {
52
+ const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf-8'));
53
+ if (typeof pkg.repository === 'string') {
54
+ const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
55
+ if (match)
56
+ return match[1];
57
+ }
58
+ if (pkg.repository?.url) {
59
+ const match = pkg.repository.url.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
60
+ if (match)
61
+ return match[1];
62
+ }
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ return null;
69
+ }
8
70
  export async function statusCommand(options) {
9
71
  const { root } = options;
10
72
  const config = loadConfig(root);
11
73
  const configPath = findConfigPath(root);
12
74
  console.log(pc.blue('Hush Status\n'));
75
+ const plaintextFiles = findPlaintextEnvFiles(root);
76
+ if (plaintextFiles.length > 0) {
77
+ console.log(pc.bgRed(pc.white(pc.bold(' SECURITY WARNING '))));
78
+ console.log(pc.red(pc.bold('\nUnencrypted .env files detected!\n')));
79
+ for (const file of plaintextFiles) {
80
+ console.log(pc.red(` ${file}`));
81
+ }
82
+ console.log('');
83
+ console.log(pc.yellow('These files may expose secrets to AI assistants and version control.'));
84
+ console.log(pc.bold('\nTo fix:'));
85
+ console.log(pc.dim(' 1. Run: npx hush encrypt'));
86
+ console.log(pc.dim(' 2. The plaintext files will be automatically deleted after encryption'));
87
+ console.log(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars\n'));
88
+ }
13
89
  console.log(pc.bold('Config:'));
14
90
  if (configPath) {
15
91
  console.log(pc.green(` ${configPath.replace(root + '/', '')}`));
@@ -17,6 +93,16 @@ export async function statusCommand(options) {
17
93
  else {
18
94
  console.log(pc.dim(' No hush.yaml found (using defaults)'));
19
95
  }
96
+ const project = getProjectFromConfig(root);
97
+ if (configPath) {
98
+ if (project) {
99
+ console.log(pc.green(` Project: ${project}`));
100
+ }
101
+ else {
102
+ console.log(pc.yellow(' Project: not set'));
103
+ console.log(pc.dim(' Add "project: my-org/my-repo" to hush.yaml for key management'));
104
+ }
105
+ }
20
106
  console.log(pc.bold('\nPrerequisites:'));
21
107
  console.log(isSopsInstalled()
22
108
  ? pc.green(' SOPS installed')
@@ -24,6 +110,29 @@ export async function statusCommand(options) {
24
110
  console.log(isAgeKeyConfigured()
25
111
  ? pc.green(' age key configured')
26
112
  : pc.yellow(' age key not found at ~/.config/sops/age/key.txt'));
113
+ if (project) {
114
+ const hasLocalKey = keyExists(project);
115
+ const has1PasswordBackup = opAvailable() && opListKeys().includes(project);
116
+ console.log(pc.bold('\nKey Status:'));
117
+ console.log(hasLocalKey
118
+ ? pc.green(` Local key: ~/.config/sops/age/keys/${project.replace(/\//g, '-')}.txt`)
119
+ : pc.yellow(' Local key: not found'));
120
+ if (opAvailable()) {
121
+ console.log(has1PasswordBackup
122
+ ? pc.green(' 1Password backup: synced')
123
+ : pc.yellow(' 1Password backup: not synced'));
124
+ if (!has1PasswordBackup && hasLocalKey) {
125
+ console.log(pc.dim(' Run "npx hush keys push" to backup to 1Password'));
126
+ }
127
+ }
128
+ else {
129
+ console.log(pc.dim(' 1Password CLI: not available'));
130
+ }
131
+ if (!hasLocalKey) {
132
+ console.log(pc.bold('\n To set up keys:'));
133
+ console.log(pc.dim(' npx hush keys setup # Pull from 1Password or generate'));
134
+ }
135
+ }
27
136
  console.log(pc.bold('\nSource Files:'));
28
137
  const sources = [
29
138
  { key: 'shared', path: config.sources.shared },
@@ -0,0 +1,2 @@
1
+ export declare function checkForUpdate(currentVersion: string): void;
2
+ //# sourceMappingURL=version-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-check.d.ts","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"AAeA,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CA6B3D"}
@@ -0,0 +1,88 @@
1
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { spawn } from 'node:child_process';
5
+ import pc from 'picocolors';
6
+ const CONFIG_DIR = join(homedir(), '.config', 'hush');
7
+ const CACHE_FILE = join(CONFIG_DIR, 'update-check.json');
8
+ const CHECK_INTERVAL_MS = 1000 * 60 * 60 * 24;
9
+ export function checkForUpdate(currentVersion) {
10
+ try {
11
+ if (!existsSync(CONFIG_DIR)) {
12
+ mkdirSync(CONFIG_DIR, { recursive: true });
13
+ }
14
+ let cache = null;
15
+ if (existsSync(CACHE_FILE)) {
16
+ try {
17
+ cache = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
18
+ }
19
+ catch {
20
+ }
21
+ }
22
+ if (cache && cache.latestVersion && isNewer(cache.latestVersion, currentVersion)) {
23
+ console.error(pc.bgYellow(pc.black(' UPDATE ')) +
24
+ pc.yellow(` New version available: ${cache.latestVersion} (current: ${currentVersion})`));
25
+ console.error(pc.dim(`Run "npm install -D @chriscode/hush@latest" to update`));
26
+ console.error('');
27
+ }
28
+ const now = Date.now();
29
+ if (!cache || now - cache.lastCheck > CHECK_INTERVAL_MS) {
30
+ spawnBackgroundCheck();
31
+ }
32
+ }
33
+ catch {
34
+ }
35
+ }
36
+ function spawnBackgroundCheck() {
37
+ const script = `
38
+ const https = require('https');
39
+ const fs = require('fs');
40
+ const path = require('path');
41
+
42
+ const cacheFile = '${CACHE_FILE.replace(/\\/g, '\\\\')}';
43
+
44
+ const req = https.get('https://registry.npmjs.org/@chriscode/hush/latest', {
45
+ timeout: 3000,
46
+ headers: { 'User-Agent': 'hush-cli' }
47
+ }, (res) => {
48
+ if (res.statusCode !== 200) process.exit(0);
49
+
50
+ let data = '';
51
+ res.on('data', chunk => data += chunk);
52
+ res.on('end', () => {
53
+ try {
54
+ const json = JSON.parse(data);
55
+ const content = JSON.stringify({
56
+ lastCheck: Date.now(),
57
+ latestVersion: json.version
58
+ });
59
+ fs.writeFileSync(cacheFile, content);
60
+ } catch (e) {}
61
+ });
62
+ });
63
+
64
+ req.on('error', () => {});
65
+ req.end();
66
+ `;
67
+ const child = spawn(process.execPath, ['-e', script], {
68
+ detached: true,
69
+ stdio: 'ignore',
70
+ env: { ...process.env, NO_UPDATE_NOTIFIER: '1' }
71
+ });
72
+ child.unref();
73
+ }
74
+ function isNewer(latest, current) {
75
+ const l = latest.split('.').map(Number);
76
+ const c = current.split('.').map(Number);
77
+ if (l[0] > c[0])
78
+ return true;
79
+ if (l[0] < c[0])
80
+ return false;
81
+ if (l[1] > c[1])
82
+ return true;
83
+ if (l[1] < c[1])
84
+ return false;
85
+ if (l[2] > c[2])
86
+ return true;
87
+ return false;
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscode/hush",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
5
  "type": "module",
6
6
  "bin": {