@chriscode/hush 2.1.0 → 2.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Chris Hasson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/cli.js CHANGED
@@ -3,6 +3,8 @@ import pc from 'picocolors';
3
3
  import { decryptCommand } from './commands/decrypt.js';
4
4
  import { encryptCommand } from './commands/encrypt.js';
5
5
  import { editCommand } from './commands/edit.js';
6
+ import { setCommand } from './commands/set.js';
7
+ import { runCommand } from './commands/run.js';
6
8
  import { statusCommand } from './commands/status.js';
7
9
  import { pushCommand } from './commands/push.js';
8
10
  import { initCommand } from './commands/init.js';
@@ -22,8 +24,9 @@ ${pc.bold('Usage:')}
22
24
  ${pc.bold('Commands:')}
23
25
  init Initialize hush.yaml config
24
26
  encrypt Encrypt source .env files
25
- decrypt Decrypt and distribute to targets
26
- set [file] Set/edit secrets in $EDITOR (alias: edit)
27
+ run -- <cmd> Run command with secrets in memory (AI-safe)
28
+ set <KEY> Set a single secret interactively (AI-safe)
29
+ edit [file] Edit all secrets in $EDITOR
27
30
  list List all variables (shows values)
28
31
  inspect List all variables (masked values, AI-safe)
29
32
  has <key> Check if a secret exists (exit 0 if set, 1 if not)
@@ -31,10 +34,14 @@ ${pc.bold('Commands:')}
31
34
  push Push secrets to Cloudflare Workers
32
35
  status Show configuration and status
33
36
  skill Install Claude Code / OpenCode skill
37
+
38
+ ${pc.bold('Deprecated Commands:')}
39
+ decrypt Write secrets to disk (unsafe - use 'run' instead)
34
40
 
35
41
  ${pc.bold('Options:')}
36
42
  -e, --env <env> Environment: development or production (default: development)
37
43
  -r, --root <dir> Root directory (default: current directory)
44
+ -t, --target <t> Target name from hush.yaml (run only)
38
45
  -q, --quiet Suppress output (has/check commands)
39
46
  --dry-run Preview changes without applying (push only)
40
47
  --warn Warn but exit 0 on drift (check only)
@@ -42,29 +49,29 @@ ${pc.bold('Options:')}
42
49
  --only-changed Only check git-modified files (check only)
43
50
  --require-source Fail if source file is missing (check only)
44
51
  --global Install skill to ~/.claude/skills/ (skill only)
45
- --local Install skill to ./.claude/skills/ (skill only)
52
+ --local Install skill to ./.claude/skills/ (skill/set only)
46
53
  -h, --help Show this help message
47
54
  -v, --version Show version number
48
55
 
49
56
  ${pc.bold('Examples:')}
50
57
  hush init Initialize hush.yaml config
51
58
  hush encrypt Encrypt .env files
52
- hush decrypt Decrypt for development
53
- hush decrypt -e production Decrypt for production
54
- hush set Set/edit shared secrets
55
- hush set development Set/edit development secrets
56
- hush list List all variables (shows values)
59
+ hush run -- npm start Run with secrets in memory (AI-safe!)
60
+ hush run -e prod -- npm build Run with production secrets
61
+ hush run -t api -- wrangler dev Run filtered for 'api' target
62
+ hush set DATABASE_URL Set a secret interactively (AI-safe)
63
+ hush set API_KEY -e prod Set a production secret
64
+ hush set API_KEY --local Set a personal local override
65
+ hush edit Edit all shared secrets in $EDITOR
66
+ hush edit development Edit development secrets in $EDITOR
67
+ hush edit local Edit personal local overrides
57
68
  hush inspect List all variables (masked, AI-safe)
58
69
  hush has DATABASE_URL Check if DATABASE_URL is set
59
70
  hush has API_KEY -q && echo "API_KEY is configured"
60
71
  hush check Verify secrets are encrypted
61
- hush check --warn Check but don't fail on drift
62
- hush check --json Output JSON for CI
63
72
  hush push --dry-run Preview push to Cloudflare
64
73
  hush status Show current status
65
74
  hush skill Install Claude skill (interactive)
66
- hush skill --global Install skill for all projects
67
- hush skill --local Install skill for this project only
68
75
  `);
69
76
  }
70
77
  function parseEnvironment(value) {
@@ -75,7 +82,7 @@ function parseEnvironment(value) {
75
82
  return null;
76
83
  }
77
84
  function parseFileKey(value) {
78
- if (value === 'shared' || value === 'development' || value === 'production')
85
+ if (value === 'shared' || value === 'development' || value === 'production' || value === 'local')
79
86
  return value;
80
87
  if (value === 'dev')
81
88
  return 'development';
@@ -86,6 +93,7 @@ function parseFileKey(value) {
86
93
  function parseArgs(args) {
87
94
  let command = '';
88
95
  let env = 'development';
96
+ let envExplicit = false;
89
97
  let root = process.cwd();
90
98
  let dryRun = false;
91
99
  let quiet = false;
@@ -97,6 +105,8 @@ function parseArgs(args) {
97
105
  let local = false;
98
106
  let file;
99
107
  let key;
108
+ let target;
109
+ let cmdArgs = [];
100
110
  for (let i = 0; i < args.length; i++) {
101
111
  const arg = args[i];
102
112
  if (arg === '-h' || arg === '--help') {
@@ -112,6 +122,7 @@ function parseArgs(args) {
112
122
  const parsed = parseEnvironment(nextArg);
113
123
  if (parsed) {
114
124
  env = parsed;
125
+ envExplicit = true;
115
126
  }
116
127
  else {
117
128
  console.error(pc.red(`Invalid environment: ${nextArg}`));
@@ -156,28 +167,40 @@ function parseArgs(args) {
156
167
  local = true;
157
168
  continue;
158
169
  }
170
+ if (arg === '-t' || arg === '--target') {
171
+ target = args[++i];
172
+ continue;
173
+ }
174
+ if (arg === '--') {
175
+ cmdArgs = args.slice(i + 1);
176
+ break;
177
+ }
159
178
  if (!command && !arg.startsWith('-')) {
160
179
  command = arg;
161
180
  continue;
162
181
  }
163
- if ((command === 'set' || command === 'edit') && !arg.startsWith('-')) {
182
+ if (command === 'edit' && !arg.startsWith('-')) {
164
183
  const parsed = parseFileKey(arg);
165
184
  if (parsed) {
166
185
  file = parsed;
167
186
  }
168
187
  else {
169
188
  console.error(pc.red(`Invalid file: ${arg}`));
170
- console.error(pc.dim('Use: shared, development, or production'));
189
+ console.error(pc.dim('Use: shared, development, production, or local'));
171
190
  process.exit(1);
172
191
  }
173
192
  continue;
174
193
  }
194
+ if (command === 'set' && !arg.startsWith('-') && !key) {
195
+ key = arg;
196
+ continue;
197
+ }
175
198
  if (command === 'has' && !arg.startsWith('-') && !key) {
176
199
  key = arg;
177
200
  continue;
178
201
  }
179
202
  }
180
- return { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key };
203
+ return { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs };
181
204
  }
182
205
  async function main() {
183
206
  const args = process.argv.slice(2);
@@ -185,7 +208,7 @@ async function main() {
185
208
  printHelp();
186
209
  process.exit(0);
187
210
  }
188
- const { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key } = parseArgs(args);
211
+ const { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs } = parseArgs(args);
189
212
  try {
190
213
  switch (command) {
191
214
  case 'init':
@@ -195,9 +218,32 @@ async function main() {
195
218
  await encryptCommand({ root });
196
219
  break;
197
220
  case 'decrypt':
221
+ console.warn(pc.yellow('⚠️ Warning: "hush decrypt" is deprecated and writes unencrypted secrets to disk.'));
222
+ console.warn(pc.yellow(' Use "hush run -- <command>" instead for better security.'));
223
+ console.warn(pc.dim(' To suppress this warning, use "hush unsafe:decrypt"'));
224
+ console.warn('');
198
225
  await decryptCommand({ root, env });
199
226
  break;
200
- case 'set':
227
+ case 'unsafe:decrypt':
228
+ console.warn(pc.red('⚠️ UNSAFE MODE: Writing unencrypted secrets to disk.'));
229
+ console.warn(pc.red(' These files will be readable by AI assistants and other tools.'));
230
+ console.warn('');
231
+ await decryptCommand({ root, env });
232
+ break;
233
+ case 'run':
234
+ await runCommand({ root, env, target, command: cmdArgs });
235
+ break;
236
+ case 'set': {
237
+ let setFile = 'shared';
238
+ if (local) {
239
+ setFile = 'local';
240
+ }
241
+ else if (envExplicit) {
242
+ setFile = env;
243
+ }
244
+ await setCommand({ root, file: setFile, key });
245
+ break;
246
+ }
201
247
  case 'edit':
202
248
  await editCommand({ root, file });
203
249
  break;
@@ -0,0 +1,3 @@
1
+ import type { RunOptions } from '../types.js';
2
+ export declare function runCommand(options: RunOptions): Promise<void>;
3
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAoC/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDnE"}
@@ -0,0 +1,77 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import pc from 'picocolors';
5
+ import { loadConfig } from '../config/loader.js';
6
+ import { filterVarsForTarget } from '../core/filter.js';
7
+ import { interpolateVars, getUnresolvedVars } from '../core/interpolate.js';
8
+ import { mergeVars } from '../core/merge.js';
9
+ import { parseEnvContent } from '../core/parse.js';
10
+ import { decrypt as sopsDecrypt } from '../core/sops.js';
11
+ function getEncryptedPath(sourcePath) {
12
+ return sourcePath + '.encrypted';
13
+ }
14
+ function getDecryptedSecrets(root, env, config) {
15
+ const sharedEncrypted = join(root, getEncryptedPath(config.sources.shared));
16
+ const envEncrypted = join(root, getEncryptedPath(config.sources[env]));
17
+ const localEncrypted = join(root, getEncryptedPath(config.sources.local));
18
+ const varSources = [];
19
+ if (existsSync(sharedEncrypted)) {
20
+ const content = sopsDecrypt(sharedEncrypted);
21
+ varSources.push(parseEnvContent(content));
22
+ }
23
+ if (existsSync(envEncrypted)) {
24
+ const content = sopsDecrypt(envEncrypted);
25
+ varSources.push(parseEnvContent(content));
26
+ }
27
+ if (existsSync(localEncrypted)) {
28
+ const content = sopsDecrypt(localEncrypted);
29
+ varSources.push(parseEnvContent(content));
30
+ }
31
+ if (varSources.length === 0) {
32
+ throw new Error(`No encrypted files found. Expected: ${sharedEncrypted}`);
33
+ }
34
+ const merged = mergeVars(...varSources);
35
+ return interpolateVars(merged);
36
+ }
37
+ export async function runCommand(options) {
38
+ const { root, env, target, command } = options;
39
+ if (!command || command.length === 0) {
40
+ console.error(pc.red('Usage: hush run -- <command>'));
41
+ console.error(pc.dim('Example: hush run -- npm start'));
42
+ console.error(pc.dim(' hush run -e production -- npm run build'));
43
+ console.error(pc.dim(' hush run --target api -- wrangler dev'));
44
+ process.exit(1);
45
+ }
46
+ const config = loadConfig(root);
47
+ let vars = getDecryptedSecrets(root, env, config);
48
+ if (target) {
49
+ const targetConfig = config.targets.find(t => t.name === target);
50
+ if (!targetConfig) {
51
+ console.error(pc.red(`Target "${target}" not found in hush.yaml`));
52
+ console.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
53
+ process.exit(1);
54
+ }
55
+ vars = filterVarsForTarget(vars, targetConfig);
56
+ }
57
+ const unresolved = getUnresolvedVars(vars);
58
+ if (unresolved.length > 0) {
59
+ console.warn(pc.yellow(`Warning: ${unresolved.length} vars have unresolved references`));
60
+ }
61
+ const childEnv = {
62
+ ...process.env,
63
+ ...Object.fromEntries(vars.map(v => [v.key, v.value])),
64
+ };
65
+ const [cmd, ...args] = command;
66
+ const result = spawnSync(cmd, args, {
67
+ stdio: 'inherit',
68
+ env: childEnv,
69
+ shell: true,
70
+ cwd: root,
71
+ });
72
+ if (result.error) {
73
+ console.error(pc.red(`Failed to execute: ${result.error.message}`));
74
+ process.exit(1);
75
+ }
76
+ process.exit(result.status ?? 1);
77
+ }
@@ -0,0 +1,3 @@
1
+ import type { SetOptions } from '../types.js';
2
+ export declare function setCommand(options: SetOptions): Promise<void>;
3
+ //# sourceMappingURL=set.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAuD9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
@@ -0,0 +1,87 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { loadConfig } from '../config/loader.js';
5
+ import { setKey } from '../core/sops.js';
6
+ function promptForValue(key) {
7
+ return new Promise((resolve, reject) => {
8
+ if (!process.stdin.isTTY) {
9
+ reject(new Error('Interactive input requires a terminal (TTY)'));
10
+ return;
11
+ }
12
+ process.stdout.write(`Enter value for ${pc.cyan(key)}: `);
13
+ const stdin = process.stdin;
14
+ stdin.setRawMode(true);
15
+ stdin.resume();
16
+ stdin.setEncoding('utf8');
17
+ let value = '';
18
+ const onData = (char) => {
19
+ switch (char) {
20
+ case '\n':
21
+ case '\r':
22
+ case '\u0004': // Ctrl+D
23
+ stdin.setRawMode(false);
24
+ stdin.pause();
25
+ stdin.removeListener('data', onData);
26
+ process.stdout.write('\n');
27
+ resolve(value);
28
+ break;
29
+ case '\u0003': // Ctrl+C
30
+ stdin.setRawMode(false);
31
+ stdin.pause();
32
+ stdin.removeListener('data', onData);
33
+ process.stdout.write('\n');
34
+ reject(new Error('Cancelled'));
35
+ break;
36
+ case '\u007F': // Backspace
37
+ case '\b':
38
+ if (value.length > 0) {
39
+ value = value.slice(0, -1);
40
+ process.stdout.write('\b \b');
41
+ }
42
+ break;
43
+ default:
44
+ value += char;
45
+ process.stdout.write('\u2022'); // Bullet character for hidden input
46
+ }
47
+ };
48
+ stdin.on('data', onData);
49
+ });
50
+ }
51
+ export async function setCommand(options) {
52
+ const { root, file, key } = options;
53
+ const config = loadConfig(root);
54
+ const fileKey = file ?? 'shared';
55
+ const sourcePath = config.sources[fileKey];
56
+ const encryptedPath = join(root, sourcePath + '.encrypted');
57
+ if (!key) {
58
+ console.error(pc.red('Usage: hush set <KEY> [-e environment]'));
59
+ console.error(pc.dim('Example: hush set DATABASE_URL'));
60
+ console.error(pc.dim(' hush set API_KEY -e production'));
61
+ console.error(pc.dim('\nTo edit all secrets in an editor, use: hush edit'));
62
+ process.exit(1);
63
+ }
64
+ if (!existsSync(encryptedPath) && !existsSync(join(root, '.sops.yaml'))) {
65
+ console.error(pc.red('Hush is not initialized in this directory'));
66
+ console.error(pc.dim('Run "hush init" first, then "hush encrypt"'));
67
+ process.exit(1);
68
+ }
69
+ try {
70
+ const value = await promptForValue(key);
71
+ if (!value) {
72
+ console.error(pc.yellow('No value entered, aborting'));
73
+ process.exit(1);
74
+ }
75
+ setKey(encryptedPath, key, value);
76
+ const envLabel = fileKey === 'shared' ? '' : ` in ${fileKey}`;
77
+ console.log(pc.green(`\n${key} set${envLabel} (${value.length} chars, encrypted)`));
78
+ }
79
+ catch (error) {
80
+ const err = error;
81
+ if (err.message === 'Cancelled') {
82
+ console.log(pc.yellow('Cancelled'));
83
+ process.exit(1);
84
+ }
85
+ throw err;
86
+ }
87
+ }
@@ -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;AAg9BhD,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;AAwhChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
@@ -6,51 +6,81 @@ 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.
10
- allowed-tools: Bash(hush:*), Bash(npx hush:*), Bash(brew:*), Bash(npm:*), Bash(pnpm:*), Bash(age-keygen:*), Read, Grep, Glob, Write
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.
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 encrypts secrets so they can be committed to git, and provides AI-safe commands that let you work with secrets without exposing values to LLMs.
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.
16
16
 
17
- ## CRITICAL RULES
17
+ ## How It Works
18
18
 
19
- ### NEVER do these things:
20
- - Read \`.env\`, \`.env.*\`, \`.env.local\`, or \`.dev.vars\` files directly
21
- - Use \`cat\`, \`grep\`, \`head\`, \`tail\`, \`less\`, \`more\` on env files
22
- - Echo or print environment variable values like \`echo $SECRET\`
23
- - Include actual secret values in your responses
24
- - Write secrets directly to \`.env\` files
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
25
24
 
26
- ### ALWAYS use Hush commands instead:
27
- - \`npx hush inspect\` to see what variables exist (values are masked)
28
- - \`npx hush has <KEY>\` to check if a specific variable is set
29
- - \`npx hush set\` to add or modify secrets (opens secure editor)
30
- - \`npx hush status\` to view configuration
25
+ ## Safe to Read (Always Encrypted)
31
26
 
32
- ## Quick Check: Is Hush Set Up?
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)
30
+
31
+ Feel free to use \`cat\`, \`grep\`, \`Read\` on any \`.env\` file. You'll see encrypted content like:
32
+ \`\`\`
33
+ DATABASE_URL=ENC[AES256_GCM,data:abc123...,type:str]
34
+ \`\`\`
35
+
36
+ ## Commands Reference
37
+
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
33
45
 
34
- Run this first to check if Hush is configured:
46
+ ### Avoid These (Deprecated):
47
+ - \`hush decrypt\` / \`hush unsafe:decrypt\` - Writes unencrypted secrets to disk (defeats the purpose!)
48
+
49
+ ## Quick Check: Is Hush Set Up?
35
50
 
36
51
  \`\`\`bash
37
52
  npx hush status
38
53
  \`\`\`
39
54
 
40
- **If this fails or shows errors**, see [SETUP.md](SETUP.md) for first-time setup instructions.
55
+ **If this fails**, see [SETUP.md](SETUP.md) for first-time setup instructions.
41
56
 
42
57
  ---
43
58
 
44
- ## Daily Usage (AI-Safe Commands)
59
+ ## Running Programs with Secrets
45
60
 
46
- ### See what variables exist
61
+ **This is the primary way to use secrets - they never touch disk!**
62
+
63
+ \`\`\`bash
64
+ npx hush run -- npm start # Run with development secrets
65
+ npx hush run -e production -- npm build # Run with production secrets
66
+ npx hush run -t api -- wrangler dev # Run filtered for 'api' target
67
+ \`\`\`
68
+
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
+ ---
73
+
74
+ ## Checking Secrets
75
+
76
+ ### See what variables exist (human-readable)
47
77
 
48
78
  \`\`\`bash
49
79
  npx hush inspect # Development
50
80
  npx hush inspect -e production # Production
51
81
  \`\`\`
52
82
 
53
- Output shows **masked values** - safe for AI to read:
83
+ Output shows **masked values**:
54
84
 
55
85
  \`\`\`
56
86
  Secrets for development:
@@ -69,73 +99,66 @@ npx hush has DATABASE_URL # Verbose output
69
99
  npx hush has API_KEY -q # Quiet: exit code only (0=set, 1=missing)
70
100
  \`\`\`
71
101
 
72
- ### View configuration
102
+ ### Read encrypted files directly
73
103
 
104
+ You can also just read the encrypted files:
74
105
  \`\`\`bash
75
- npx hush status
106
+ cat .env.encrypted # See encrypted content (safe!)
107
+ grep DATABASE .env.encrypted # Search for keys in encrypted file
76
108
  \`\`\`
77
109
 
78
- ### Set/modify secrets (requires user interaction)
110
+ ---
79
111
 
80
- \`\`\`bash
81
- npx hush set # Set shared secrets
82
- npx hush set development # Set dev secrets
83
- npx hush set production # Set prod secrets
84
- \`\`\`
112
+ ## Adding/Modifying Secrets
85
113
 
86
- After setting, encrypt:
114
+ ### Add a single secret interactively
87
115
 
88
116
  \`\`\`bash
89
- npx hush encrypt
117
+ npx hush set DATABASE_URL # You invoke this, user types value
118
+ npx hush set API_KEY -e production # Set in production secrets
119
+ npx hush set DEBUG --local # Set personal local override
90
120
  \`\`\`
91
121
 
92
- ### Decrypt to targets
122
+ 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
93
126
 
94
127
  \`\`\`bash
95
- npx hush decrypt # Development
96
- npx hush decrypt -e production # Production
128
+ npx hush edit # Edit shared secrets
129
+ npx hush edit development # Edit development secrets
130
+ npx hush edit local # Edit personal overrides
97
131
  \`\`\`
98
132
 
99
133
  ---
100
134
 
101
135
  ## Common Workflows
102
136
 
103
- ### "What secrets are configured?"
104
- \`\`\`bash
105
- npx hush inspect
106
- \`\`\`
107
-
108
- ### "Is DATABASE_URL set?"
137
+ ### "Help user add DATABASE_URL"
109
138
  \`\`\`bash
110
- npx hush has DATABASE_URL
139
+ npx hush set DATABASE_URL
111
140
  \`\`\`
112
-
113
- ### "Help user add a new secret"
114
- 1. Tell user to run: \`npx hush set\`
115
- 2. They add the variable in their editor
116
- 3. They save and close
117
- 4. Tell them to run: \`npx hush encrypt\`
118
- 5. Verify: \`npx hush inspect\`
141
+ Tell user: "Enter your database URL when prompted"
119
142
 
120
143
  ### "Check all required secrets"
121
144
  \`\`\`bash
122
145
  npx hush has DATABASE_URL -q && npx hush has API_KEY -q && echo "All configured" || echo "Some missing"
123
146
  \`\`\`
124
147
 
125
- ---
126
-
127
- ## Files You Must NOT Read
128
-
129
- These contain plaintext secrets - NEVER read them:
130
- - \`.env\`, \`.env.local\`, \`.env.development\`, \`.env.production\`
131
- - \`.dev.vars\`
132
- - Any \`*/.env\` or \`*/.env.*\` files
148
+ ### "Run the development server"
149
+ \`\`\`bash
150
+ npx hush run -- npm run dev
151
+ \`\`\`
133
152
 
134
- ## Files That Are Safe to Read
153
+ ### "Build for production"
154
+ \`\`\`bash
155
+ npx hush run -e production -- npm run build
156
+ \`\`\`
135
157
 
136
- - \`hush.yaml\` - Configuration (no secrets)
137
- - \`.sops.yaml\` - SOPS config (public key only)
138
- - \`.env.encrypted\`, \`.env.*.encrypted\` - Encrypted files
158
+ ### "See what's in the encrypted file"
159
+ \`\`\`bash
160
+ cat .env.encrypted # Safe! Shows encrypted data only
161
+ \`\`\`
139
162
 
140
163
  ---
141
164
 
@@ -351,8 +374,8 @@ When a new team member joins:
351
374
 
352
375
  1. **Get the age private key** from an existing team member
353
376
  2. **Save it** to \`~/.config/sops/age/key.txt\`
354
- 3. **Run** \`npx hush decrypt\` to generate local env files
355
- 4. **Start developing**
377
+ 3. **Run** \`npx hush run -- npm install\` to verify decryption works
378
+ 4. **Start developing** with \`npx hush run -- npm run dev\`
356
379
 
357
380
  The private key should be shared securely (password manager, encrypted channel, etc.)
358
381
 
@@ -364,7 +387,7 @@ After setup, verify everything works:
364
387
 
365
388
  - [ ] \`npx hush status\` shows configuration
366
389
  - [ ] \`npx hush inspect\` shows masked variables
367
- - [ ] \`npx hush decrypt\` creates local env files
390
+ - [ ] \`npx hush run -- env\` can decrypt and run (secrets stay in memory!)
368
391
  - [ ] \`.env.encrypted\` files are committed to git
369
392
  - [ ] Plaintext \`.env\` files are in \`.gitignore\`
370
393
 
@@ -395,91 +418,166 @@ Edit \`hush.yaml\` and add your source files under \`sources:\`.
395
418
 
396
419
  Complete reference for all Hush CLI commands with flags, options, and examples.
397
420
 
398
- ## Global Options
421
+ ## Security Model: Encrypted at Rest
422
+
423
+ All secrets are stored encrypted on disk. You can safely read any \`.env\` file—they contain only encrypted data. No special precautions needed for file reading.
399
424
 
400
- These options work with most commands:
425
+ ## Global Options
401
426
 
402
427
  | Option | Description |
403
428
  |--------|-------------|
404
- | \`-e, --env <env>\` | Environment: \`development\` (or \`dev\`) / \`production\` (or \`prod\`). Default: \`development\` |
429
+ | \`-e, --env <env>\` | Environment: \`development\` / \`production\`. Default: \`development\` |
405
430
  | \`-r, --root <dir>\` | Root directory containing \`hush.yaml\`. Default: current directory |
431
+ | \`-t, --target <name>\` | Target name from hush.yaml (for \`run\` command) |
432
+ | \`--local\` | Use local overrides (for \`set\` command) |
406
433
  | \`-h, --help\` | Show help message |
407
434
  | \`-v, --version\` | Show version number |
408
435
 
409
- ## Commands
436
+ ---
410
437
 
411
- ### hush init
438
+ ## Primary Commands
412
439
 
413
- Generate a \`hush.yaml\` configuration file with auto-detected targets.
440
+ ### hush run -- <command>
441
+
442
+ **The recommended way to run programs with secrets!**
443
+
444
+ Decrypts secrets to memory and runs a command with them as environment variables.
445
+ Secrets never touch the disk as plaintext.
414
446
 
415
447
  \`\`\`bash
416
- hush init
448
+ hush run -- npm start # Run with development secrets
449
+ hush run -e production -- npm build # Run with production secrets
450
+ hush run -t api -- wrangler dev # Run filtered for 'api' target
417
451
  \`\`\`
418
452
 
419
- Scans for \`package.json\` and \`wrangler.toml\` files to auto-detect targets.
453
+ **Options:**
454
+ | Option | Description |
455
+ |--------|-------------|
456
+ | \`-e, --env\` | Environment (development/production) |
457
+ | \`-t, --target\` | Filter secrets for a specific target from hush.yaml |
420
458
 
421
459
  ---
422
460
 
423
- ### hush encrypt
461
+ ### hush set <KEY> ⭐
424
462
 
425
- Encrypt source \`.env\` files to \`.env.encrypted\` files.
463
+ Add or update a single secret interactively. You invoke this, user enters the value.
426
464
 
427
465
  \`\`\`bash
428
- hush encrypt
466
+ hush set DATABASE_URL # Set in shared secrets
467
+ hush set API_KEY -e production # Set in production secrets
468
+ hush set DEBUG --local # Set personal local override
429
469
  \`\`\`
430
470
 
431
- **What gets encrypted** (based on \`hush.yaml\` sources):
432
- - \`.env\` -> \`.env.encrypted\`
433
- - \`.env.development\` -> \`.env.development.encrypted\`
434
- - \`.env.production\` -> \`.env.production.encrypted\`
471
+ User will be prompted with hidden input - the value is never visible.
435
472
 
436
473
  ---
437
474
 
438
- ### hush decrypt
475
+ ### hush edit [file]
439
476
 
440
- Decrypt and distribute secrets to all configured targets.
477
+ Open all secrets in \`$EDITOR\` for bulk editing.
441
478
 
442
479
  \`\`\`bash
443
- hush decrypt # Development (default)
444
- hush decrypt -e production # Production
445
- hush decrypt -e prod # Short form
480
+ hush edit # Edit shared secrets
481
+ hush edit development # Edit development secrets
482
+ hush edit production # Edit production secrets
483
+ hush edit local # Edit personal local overrides
446
484
  \`\`\`
447
485
 
448
- **Process:**
449
- 1. Decrypts encrypted source files
450
- 2. Merges: shared -> environment -> local overrides
451
- 3. Interpolates variable references (\`\${VAR}\`)
452
- 4. Filters per target using \`include\`/\`exclude\` patterns
453
- 5. Writes to each target in configured format
486
+ ---
487
+
488
+ ### hush inspect
489
+
490
+ List all variables with **masked values** (human-readable format).
491
+
492
+ \`\`\`bash
493
+ hush inspect # Development
494
+ hush inspect -e production # Production
495
+ \`\`\`
454
496
 
455
497
  ---
456
498
 
457
- ### hush set (alias: edit)
499
+ ### hush has <KEY>
458
500
 
459
- Set or modify secrets. Opens encrypted file in your \`$EDITOR\`.
501
+ Check if a specific secret exists.
460
502
 
461
503
  \`\`\`bash
462
- hush set # Set shared secrets
463
- hush set development # Set development secrets
464
- hush set production # Set production secrets
504
+ hush has DATABASE_URL # Verbose output
505
+ hush has API_KEY -q # Quiet: exit code only (0=set, 1=missing)
465
506
  \`\`\`
466
507
 
467
- Opens a temporary decrypted file, re-encrypts on save.
508
+ ---
509
+
510
+ ## Setup Commands
511
+
512
+ ### hush init
513
+
514
+ Generate \`hush.yaml\` configuration with auto-detected targets.
515
+
516
+ \`\`\`bash
517
+ hush init
518
+ \`\`\`
519
+
520
+ ---
521
+
522
+ ### hush encrypt
523
+
524
+ Encrypt source \`.env\` files to \`.env.encrypted\` files.
525
+
526
+ \`\`\`bash
527
+ hush encrypt
528
+ \`\`\`
529
+
530
+ ---
531
+
532
+ ### hush status
533
+
534
+ Show configuration and file status.
535
+
536
+ \`\`\`bash
537
+ hush status
538
+ \`\`\`
539
+
540
+ ---
541
+
542
+ ## Deployment Commands
543
+
544
+ ### hush push
545
+
546
+ Push production secrets to Cloudflare Workers.
468
547
 
469
- **Tip:** Set your editor with \`export EDITOR=vim\` or use \`code --wait\` for VS Code.
548
+ \`\`\`bash
549
+ hush push # Push secrets
550
+ hush push --dry-run # Preview without pushing
551
+ \`\`\`
470
552
 
471
553
  ---
472
554
 
473
- ### hush list
555
+ ## Deprecated Commands (Avoid)
556
+
557
+ ### hush decrypt / hush unsafe:decrypt ⚠️
474
558
 
475
- List all variables with their **actual values**.
559
+ **DEPRECATED:** Writes unencrypted secrets to disk, defeating the "encrypted at rest" model.
476
560
 
477
561
  \`\`\`bash
478
- hush list # Development
479
- hush list -e production # Production
562
+ hush decrypt # Writes plaintext .env files (avoid!)
563
+ hush unsafe:decrypt # Same, explicit unsafe mode
480
564
  \`\`\`
481
565
 
482
- **WARNING:** This shows real secret values. Use \`hush inspect\` for AI-safe output.
566
+ Use \`hush run -- <command>\` instead.
567
+
568
+ ---
569
+
570
+ ## Quick Reference
571
+
572
+ | Command | Purpose |
573
+ |---------|---------|
574
+ | \`hush run -- <cmd>\` | Run with secrets (memory only) |
575
+ | \`hush set <KEY>\` | Add secret interactively |
576
+ | \`hush edit\` | Edit secrets in $EDITOR |
577
+ | \`hush inspect\` | See variables (masked) |
578
+ | \`hush has <KEY>\` | Check if variable exists |
579
+ | \`hush status\` | View configuration |
580
+ | \`cat .env.encrypted\` | Read encrypted file (safe!) |
483
581
 
484
582
  ---
485
583
 
@@ -696,162 +794,136 @@ targets:
696
794
  `,
697
795
  'examples/workflows.md': `# Hush Workflow Examples
698
796
 
699
- Step-by-step examples for common AI assistant workflows when working with secrets.
797
+ Step-by-step examples for common workflows when working with secrets.
700
798
 
701
- ## Checking Configuration
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.
702
800
 
703
- ### "What environment variables does this project use?"
801
+ ## Running Programs (Most Common)
802
+
803
+ ### "Start the development server"
704
804
 
705
805
  \`\`\`bash
706
- hush inspect
806
+ hush run -- npm run dev
707
807
  \`\`\`
708
808
 
709
- Read the output to see all configured variables, their approximate lengths, and which targets receive them.
809
+ ### "Build for production"
710
810
 
711
- ### "Is the database configured?"
811
+ \`\`\`bash
812
+ hush run -e production -- npm run build
813
+ \`\`\`
814
+
815
+ ### "Run tests with secrets"
712
816
 
713
817
  \`\`\`bash
714
- hush has DATABASE_URL
818
+ hush run -- npm test
715
819
  \`\`\`
716
820
 
717
- If the output says "not found", guide the user to add it.
821
+ ### "Run Wrangler for Cloudflare Worker"
822
+
823
+ \`\`\`bash
824
+ hush run -t api -- wrangler dev
825
+ \`\`\`
826
+
827
+ ---
828
+
829
+ ## Checking Secrets
830
+
831
+ ### "What environment variables does this project use?"
832
+
833
+ \`\`\`bash
834
+ hush inspect # Human-readable masked output
835
+ # or
836
+ cat .env.encrypted # Raw encrypted file (safe!)
837
+ \`\`\`
718
838
 
719
- ### "Are all required secrets set?"
839
+ ### "Is the database configured?"
720
840
 
721
841
  \`\`\`bash
722
- # Check each required secret
723
- hush has DATABASE_URL -q || echo "Missing: DATABASE_URL"
724
- hush has API_KEY -q || echo "Missing: API_KEY"
725
- hush has STRIPE_SECRET_KEY -q || echo "Missing: STRIPE_SECRET_KEY"
842
+ hush has DATABASE_URL
726
843
  \`\`\`
727
844
 
728
- Or check all at once:
845
+ If "not found", help user add it with \`hush set DATABASE_URL\`.
846
+
847
+ ### "Check all required secrets"
848
+
729
849
  \`\`\`bash
730
850
  hush has DATABASE_URL -q && \\
731
851
  hush has API_KEY -q && \\
732
- hush has STRIPE_SECRET_KEY -q && \\
733
- echo "All secrets configured" || \\
734
- echo "Some secrets missing"
852
+ echo "All configured" || \\
853
+ echo "Some missing"
854
+ \`\`\`
855
+
856
+ ### "Search for a key in encrypted files"
857
+
858
+ \`\`\`bash
859
+ grep DATABASE .env.encrypted # Safe! Shows encrypted line
735
860
  \`\`\`
736
861
 
737
862
  ---
738
863
 
739
- ## Helping Users Add Secrets
864
+ ## Adding Secrets
740
865
 
741
- ### "Help me add a new API key"
866
+ ### "Help me add DATABASE_URL"
742
867
 
743
- 1. **Check if it already exists:**
744
- \`\`\`bash
745
- hush has NEW_API_KEY
746
- \`\`\`
868
+ \`\`\`bash
869
+ hush set DATABASE_URL
870
+ \`\`\`
747
871
 
748
- 2. **If not set, guide the user:**
749
- > To add \`NEW_API_KEY\`, run:
750
- > \`\`\`bash
751
- > hush set
752
- > \`\`\`
753
- > Add a line like: \`NEW_API_KEY=your_actual_key_here\`
754
- > Save and close the editor, then run:
755
- > \`\`\`bash
756
- > hush encrypt
757
- > \`\`\`
758
-
759
- 3. **Verify it was added:**
760
- \`\`\`bash
761
- hush has NEW_API_KEY
762
- \`\`\`
872
+ Tell user: "Enter your database URL when prompted (input will be hidden)"
873
+
874
+ ### "Add a production-only secret"
763
875
 
764
- ### "I need to add secrets for production"
876
+ \`\`\`bash
877
+ hush set STRIPE_SECRET_KEY -e production
878
+ \`\`\`
765
879
 
766
- Guide the user:
767
- > Run \`hush set production\` to set production secrets.
768
- > After saving, run \`hush encrypt\` to encrypt the changes.
769
- > To deploy, run \`hush decrypt -e production\`.
880
+ ### "Add a personal local override"
881
+
882
+ \`\`\`bash
883
+ hush set DEBUG --local
884
+ \`\`\`
885
+
886
+ ### "Edit multiple secrets at once"
887
+
888
+ \`\`\`bash
889
+ hush edit
890
+ \`\`\`
891
+
892
+ Tell user: "Your editor will open. Add or modify secrets, then save and close."
770
893
 
771
894
  ---
772
895
 
773
- ## Debugging Issues
896
+ ## Debugging
774
897
 
775
898
  ### "My app can't find DATABASE_URL"
776
899
 
777
- 1. **Check if the variable exists:**
900
+ 1. Check if it exists:
778
901
  \`\`\`bash
779
902
  hush has DATABASE_URL
780
903
  \`\`\`
781
904
 
782
- 2. **If it exists, check target distribution:**
905
+ 2. Check target distribution:
783
906
  \`\`\`bash
784
907
  hush inspect
785
908
  \`\`\`
786
- Look at the "Target distribution" section to see which targets receive it.
787
909
 
788
- 3. **Check if it's filtered out:**
910
+ 3. Check hush.yaml for filtering:
789
911
  \`\`\`bash
790
912
  cat hush.yaml
791
913
  \`\`\`
792
- Look for \`include\`/\`exclude\` patterns that might filter the variable.
793
914
 
794
- 4. **Regenerate env files:**
915
+ 4. Look at the encrypted file:
795
916
  \`\`\`bash
796
- hush decrypt
917
+ grep DATABASE .env.encrypted # Safe to read!
797
918
  \`\`\`
798
919
 
799
- ### "Secrets aren't reaching my API folder"
800
-
801
- 1. **Check target configuration:**
920
+ 5. Try running directly:
802
921
  \`\`\`bash
803
- hush status
804
- \`\`\`
805
- Verify the API target path and format are correct.
806
-
807
- 2. **Check filters:**
808
- \`\`\`bash
809
- cat hush.yaml
810
- \`\`\`
811
- If there's an \`exclude: EXPO_PUBLIC_*\` pattern, that's intentional.
812
- If there's an \`include\` pattern, only matching variables are sent.
813
-
814
- 3. **Run inspect to see distribution:**
815
- \`\`\`bash
816
- hush inspect
922
+ hush run -- env | grep DATABASE
817
923
  \`\`\`
818
924
 
819
925
  ---
820
926
 
821
- ## Deployment Workflows
822
-
823
- ### "Deploy to production"
824
-
825
- \`\`\`bash
826
- # Decrypt production secrets to all targets
827
- hush decrypt -e production
828
- \`\`\`
829
-
830
- ### "Push secrets to Cloudflare Workers"
831
-
832
- \`\`\`bash
833
- # Preview what would be pushed
834
- hush push --dry-run
835
-
836
- # Actually push (requires wrangler auth)
837
- hush push
838
- \`\`\`
839
-
840
- ### "Verify before deploying"
841
-
842
- \`\`\`bash
843
- # Check all encrypted files are up to date
844
- hush check
845
-
846
- # If drift detected, encrypt first
847
- hush encrypt
848
-
849
- # Then decrypt for production
850
- hush decrypt -e production
851
- \`\`\`
852
-
853
- ---
854
-
855
927
  ## Team Workflows
856
928
 
857
929
  ### "New team member setup"
@@ -859,30 +931,32 @@ hush decrypt -e production
859
931
  Guide them:
860
932
  > 1. Get the age private key from a team member
861
933
  > 2. Save it to \`~/.config/sops/age/key.txt\`
862
- > 3. Run \`hush decrypt\` to generate local env files
863
- > 4. Start developing!
934
+ > 3. Run \`hush run -- npm install\` to verify setup
935
+ > 4. Start developing with \`hush run -- npm run dev\`
864
936
 
865
- ### "Someone added new secrets, my app is broken"
937
+ ### "Someone added new secrets"
866
938
 
867
939
  \`\`\`bash
868
- # Pull latest changes
869
940
  git pull
870
-
871
- # Regenerate env files
872
- hush decrypt
941
+ hush inspect # See what's new
873
942
  \`\`\`
874
943
 
875
- ### "Check if I forgot to encrypt changes"
944
+ ---
945
+
946
+ ## Deployment
947
+
948
+ ### "Push to Cloudflare Workers"
876
949
 
877
950
  \`\`\`bash
878
- hush check
951
+ hush push --dry-run # Preview first
952
+ hush push # Actually push
879
953
  \`\`\`
880
954
 
881
- If drift detected:
955
+ ### "Build and deploy"
956
+
882
957
  \`\`\`bash
883
- hush encrypt
884
- git add .env*.encrypted
885
- git commit -m "chore: encrypt new secrets"
958
+ hush run -e production -- npm run build
959
+ hush push
886
960
  \`\`\`
887
961
 
888
962
  ---
@@ -916,17 +990,15 @@ Target distribution:
916
990
  - The \`app\` folder only gets \`EXPO_PUBLIC_*\` variables
917
991
  - The \`api\` folder gets everything except \`EXPO_PUBLIC_*\`
918
992
 
919
- ### hush has output explained
993
+ ### Reading encrypted files directly
920
994
 
921
995
  \`\`\`bash
922
- $ hush has DATABASE_URL
923
- DATABASE_URL is set (45 chars)
924
-
925
- $ hush has MISSING_VAR
926
- MISSING_VAR not found
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]
927
999
  \`\`\`
928
1000
 
929
- The character count helps identify if the value looks reasonable (e.g., a 45-char DATABASE_URL is plausible, a 3-char one might be wrong).
1001
+ This is safe to view—the actual values are encrypted. You can see what keys exist without exposing secrets.
930
1002
  `,
931
1003
  };
932
1004
  function getSkillPath(location, root) {
@@ -3,4 +3,9 @@ export declare function isAgeKeyConfigured(): boolean;
3
3
  export declare function decrypt(filePath: string): string;
4
4
  export declare function encrypt(inputPath: string, outputPath: string): void;
5
5
  export declare function edit(filePath: string): void;
6
+ /**
7
+ * Set a single key in an encrypted file.
8
+ * Decrypts to memory, updates the key, re-encrypts.
9
+ */
10
+ export declare function setKey(filePath: string, key: string, value: string): void;
6
11
  //# sourceMappingURL=sops.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sops.d.ts","sourceRoot":"","sources":["../../src/core/sops.ts"],"names":[],"mappings":"AAyBA,wBAAgB,eAAe,IAAI,OAAO,CAOzC;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA6BhD;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuBnE;AAED,wBAAgB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAqB3C"}
1
+ {"version":3,"file":"sops.d.ts","sourceRoot":"","sources":["../../src/core/sops.ts"],"names":[],"mappings":"AA0BA,wBAAgB,eAAe,IAAI,OAAO,CAOzC;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA6BhD;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuBnE;AAED,wBAAgB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAqB3C;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAqDzE"}
package/dist/core/sops.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execSync, spawnSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
4
5
  function getAgeKeyFile() {
5
6
  if (process.env.SOPS_AGE_KEY_FILE) {
6
7
  return process.env.SOPS_AGE_KEY_FILE;
@@ -89,3 +90,50 @@ export function edit(filePath) {
89
90
  throw new Error(`SOPS edit failed with exit code ${result.status}`);
90
91
  }
91
92
  }
93
+ /**
94
+ * Set a single key in an encrypted file.
95
+ * Decrypts to memory, updates the key, re-encrypts.
96
+ */
97
+ export function setKey(filePath, key, value) {
98
+ if (!isSopsInstalled()) {
99
+ throw new Error('SOPS is not installed. Install with: brew install sops');
100
+ }
101
+ let content = '';
102
+ // If file exists, decrypt it first
103
+ if (existsSync(filePath)) {
104
+ content = decrypt(filePath);
105
+ }
106
+ // Parse existing content into lines
107
+ const lines = content.split('\n').filter(line => line.trim() !== '');
108
+ // Find and update or add the key
109
+ let found = false;
110
+ const updatedLines = lines.map(line => {
111
+ const match = line.match(/^([^=]+)=/);
112
+ if (match && match[1] === key) {
113
+ found = true;
114
+ return `${key}=${value}`;
115
+ }
116
+ return line;
117
+ });
118
+ if (!found) {
119
+ updatedLines.push(`${key}=${value}`);
120
+ }
121
+ const newContent = updatedLines.join('\n') + '\n';
122
+ const tempFile = join(tmpdir(), `hush-temp-${Date.now()}.env`);
123
+ try {
124
+ writeFileSync(tempFile, newContent, 'utf-8');
125
+ // Encrypt temp file to the target
126
+ execSync(`sops --input-type dotenv --output-type dotenv --encrypt "${tempFile}" > "${filePath}"`, {
127
+ encoding: 'utf-8',
128
+ shell: '/bin/bash',
129
+ stdio: ['pipe', 'pipe', 'pipe'],
130
+ env: getSopsEnv(),
131
+ });
132
+ }
133
+ finally {
134
+ // Always clean up temp file
135
+ if (existsSync(tempFile)) {
136
+ unlinkSync(tempFile);
137
+ }
138
+ }
139
+ }
package/dist/types.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface SourceFiles {
11
11
  shared: string;
12
12
  development: string;
13
13
  production: string;
14
+ local: string;
14
15
  }
15
16
  export interface HushConfig {
16
17
  sources: SourceFiles;
@@ -29,7 +30,18 @@ export interface EncryptOptions {
29
30
  }
30
31
  export interface EditOptions {
31
32
  root: string;
32
- file?: 'shared' | 'development' | 'production';
33
+ file?: 'shared' | 'development' | 'production' | 'local';
34
+ }
35
+ export interface SetOptions {
36
+ root: string;
37
+ file?: 'shared' | 'development' | 'production' | 'local';
38
+ key?: string;
39
+ }
40
+ export interface RunOptions {
41
+ root: string;
42
+ env: Environment;
43
+ target?: string;
44
+ command: string[];
33
45
  }
34
46
  export interface PushOptions {
35
47
  root: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAC7E,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,YAAY,CAAC;AAEvD,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,WAAW,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,YAAY,CAAC;CAChD;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,gBAAgB,GAAG,oBAAoB,CAAC;AAE9G,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC;IACjC,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,eAAO,MAAM,eAAe,EAAE,WAI7B,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAqBjF,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -2,6 +2,7 @@ export const DEFAULT_SOURCES = {
2
2
  shared: '.env',
3
3
  development: '.env.development',
4
4
  production: '.env.production',
5
+ local: '.env.local',
5
6
  };
6
7
  export const FORMAT_OUTPUT_FILES = {
7
8
  dotenv: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscode/hush",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,14 +12,6 @@
12
12
  "types": "./dist/index.d.ts"
13
13
  }
14
14
  },
15
- "scripts": {
16
- "build": "tsc",
17
- "dev": "tsc --watch",
18
- "test": "vitest run",
19
- "test:watch": "vitest",
20
- "prepublishOnly": "pnpm build && pnpm test",
21
- "type-check": "tsc --noEmit"
22
- },
23
15
  "keywords": [
24
16
  "secrets",
25
17
  "sops",
@@ -61,5 +53,12 @@
61
53
  ],
62
54
  "publishConfig": {
63
55
  "access": "public"
56
+ },
57
+ "scripts": {
58
+ "build": "tsc",
59
+ "dev": "tsc --watch",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest",
62
+ "type-check": "tsc --noEmit"
64
63
  }
65
- }
64
+ }