@chriscode/hush 2.0.0 → 2.1.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 +28 -7
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +1005 -0
- package/dist/formats/index.d.ts +2 -1
- package/dist/formats/index.d.ts.map +1 -1
- package/dist/formats/index.js +4 -1
- package/dist/formats/yaml.d.ts +13 -0
- package/dist/formats/yaml.d.ts.map +1 -0
- package/dist/formats/yaml.js +50 -0
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/package.json +10 -9
- package/LICENSE +0 -21
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,8 @@ import { listCommand } from './commands/list.js';
|
|
|
10
10
|
import { inspectCommand } from './commands/inspect.js';
|
|
11
11
|
import { hasCommand } from './commands/has.js';
|
|
12
12
|
import { checkCommand } from './commands/check.js';
|
|
13
|
-
|
|
13
|
+
import { skillCommand } from './commands/skill.js';
|
|
14
|
+
const VERSION = '2.1.0';
|
|
14
15
|
function printHelp() {
|
|
15
16
|
console.log(`
|
|
16
17
|
${pc.bold('hush')} - SOPS-based secrets management for monorepos
|
|
@@ -22,13 +23,14 @@ ${pc.bold('Commands:')}
|
|
|
22
23
|
init Initialize hush.yaml config
|
|
23
24
|
encrypt Encrypt source .env files
|
|
24
25
|
decrypt Decrypt and distribute to targets
|
|
25
|
-
|
|
26
|
+
set [file] Set/edit secrets in $EDITOR (alias: edit)
|
|
26
27
|
list List all variables (shows values)
|
|
27
28
|
inspect List all variables (masked values, AI-safe)
|
|
28
29
|
has <key> Check if a secret exists (exit 0 if set, 1 if not)
|
|
29
30
|
check Verify secrets are encrypted (for pre-commit hooks)
|
|
30
31
|
push Push secrets to Cloudflare Workers
|
|
31
32
|
status Show configuration and status
|
|
33
|
+
skill Install Claude Code / OpenCode skill
|
|
32
34
|
|
|
33
35
|
${pc.bold('Options:')}
|
|
34
36
|
-e, --env <env> Environment: development or production (default: development)
|
|
@@ -39,6 +41,8 @@ ${pc.bold('Options:')}
|
|
|
39
41
|
--json Output machine-readable JSON (check only)
|
|
40
42
|
--only-changed Only check git-modified files (check only)
|
|
41
43
|
--require-source Fail if source file is missing (check only)
|
|
44
|
+
--global Install skill to ~/.claude/skills/ (skill only)
|
|
45
|
+
--local Install skill to ./.claude/skills/ (skill only)
|
|
42
46
|
-h, --help Show this help message
|
|
43
47
|
-v, --version Show version number
|
|
44
48
|
|
|
@@ -47,8 +51,8 @@ ${pc.bold('Examples:')}
|
|
|
47
51
|
hush encrypt Encrypt .env files
|
|
48
52
|
hush decrypt Decrypt for development
|
|
49
53
|
hush decrypt -e production Decrypt for production
|
|
50
|
-
hush edit
|
|
51
|
-
hush
|
|
54
|
+
hush set Set/edit shared secrets
|
|
55
|
+
hush set development Set/edit development secrets
|
|
52
56
|
hush list List all variables (shows values)
|
|
53
57
|
hush inspect List all variables (masked, AI-safe)
|
|
54
58
|
hush has DATABASE_URL Check if DATABASE_URL is set
|
|
@@ -58,6 +62,9 @@ ${pc.bold('Examples:')}
|
|
|
58
62
|
hush check --json Output JSON for CI
|
|
59
63
|
hush push --dry-run Preview push to Cloudflare
|
|
60
64
|
hush status Show current status
|
|
65
|
+
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
|
|
61
68
|
`);
|
|
62
69
|
}
|
|
63
70
|
function parseEnvironment(value) {
|
|
@@ -86,6 +93,8 @@ function parseArgs(args) {
|
|
|
86
93
|
let json = false;
|
|
87
94
|
let onlyChanged = false;
|
|
88
95
|
let requireSource = false;
|
|
96
|
+
let global = false;
|
|
97
|
+
let local = false;
|
|
89
98
|
let file;
|
|
90
99
|
let key;
|
|
91
100
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -139,11 +148,19 @@ function parseArgs(args) {
|
|
|
139
148
|
requireSource = true;
|
|
140
149
|
continue;
|
|
141
150
|
}
|
|
151
|
+
if (arg === '--global') {
|
|
152
|
+
global = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (arg === '--local') {
|
|
156
|
+
local = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
142
159
|
if (!command && !arg.startsWith('-')) {
|
|
143
160
|
command = arg;
|
|
144
161
|
continue;
|
|
145
162
|
}
|
|
146
|
-
if (command === 'edit' && !arg.startsWith('-')) {
|
|
163
|
+
if ((command === 'set' || command === 'edit') && !arg.startsWith('-')) {
|
|
147
164
|
const parsed = parseFileKey(arg);
|
|
148
165
|
if (parsed) {
|
|
149
166
|
file = parsed;
|
|
@@ -160,7 +177,7 @@ function parseArgs(args) {
|
|
|
160
177
|
continue;
|
|
161
178
|
}
|
|
162
179
|
}
|
|
163
|
-
return { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, file, key };
|
|
180
|
+
return { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key };
|
|
164
181
|
}
|
|
165
182
|
async function main() {
|
|
166
183
|
const args = process.argv.slice(2);
|
|
@@ -168,7 +185,7 @@ async function main() {
|
|
|
168
185
|
printHelp();
|
|
169
186
|
process.exit(0);
|
|
170
187
|
}
|
|
171
|
-
const { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, file, key } = parseArgs(args);
|
|
188
|
+
const { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key } = parseArgs(args);
|
|
172
189
|
try {
|
|
173
190
|
switch (command) {
|
|
174
191
|
case 'init':
|
|
@@ -180,6 +197,7 @@ async function main() {
|
|
|
180
197
|
case 'decrypt':
|
|
181
198
|
await decryptCommand({ root, env });
|
|
182
199
|
break;
|
|
200
|
+
case 'set':
|
|
183
201
|
case 'edit':
|
|
184
202
|
await editCommand({ root, file });
|
|
185
203
|
break;
|
|
@@ -205,6 +223,9 @@ async function main() {
|
|
|
205
223
|
case 'status':
|
|
206
224
|
await statusCommand({ root });
|
|
207
225
|
break;
|
|
226
|
+
case 'skill':
|
|
227
|
+
await skillCommand({ root, global, local });
|
|
228
|
+
break;
|
|
208
229
|
default:
|
|
209
230
|
if (command) {
|
|
210
231
|
console.error(pc.red(`Unknown command: ${command}`));
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
const SKILL_FILES = {
|
|
7
|
+
'SKILL.md': `---
|
|
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
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Hush - AI-Native Secrets Management
|
|
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.
|
|
16
|
+
|
|
17
|
+
## CRITICAL RULES
|
|
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
|
|
25
|
+
|
|
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
|
|
31
|
+
|
|
32
|
+
## Quick Check: Is Hush Set Up?
|
|
33
|
+
|
|
34
|
+
Run this first to check if Hush is configured:
|
|
35
|
+
|
|
36
|
+
\`\`\`bash
|
|
37
|
+
npx hush status
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
**If this fails or shows errors**, see [SETUP.md](SETUP.md) for first-time setup instructions.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Daily Usage (AI-Safe Commands)
|
|
45
|
+
|
|
46
|
+
### See what variables exist
|
|
47
|
+
|
|
48
|
+
\`\`\`bash
|
|
49
|
+
npx hush inspect # Development
|
|
50
|
+
npx hush inspect -e production # Production
|
|
51
|
+
\`\`\`
|
|
52
|
+
|
|
53
|
+
Output shows **masked values** - safe for AI to read:
|
|
54
|
+
|
|
55
|
+
\`\`\`
|
|
56
|
+
Secrets for development:
|
|
57
|
+
|
|
58
|
+
DATABASE_URL = post****************... (45 chars)
|
|
59
|
+
STRIPE_SECRET_KEY = sk_t****************... (32 chars)
|
|
60
|
+
API_KEY = (not set)
|
|
61
|
+
|
|
62
|
+
Total: 3 variables
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
### Check if a specific variable exists
|
|
66
|
+
|
|
67
|
+
\`\`\`bash
|
|
68
|
+
npx hush has DATABASE_URL # Verbose output
|
|
69
|
+
npx hush has API_KEY -q # Quiet: exit code only (0=set, 1=missing)
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
### View configuration
|
|
73
|
+
|
|
74
|
+
\`\`\`bash
|
|
75
|
+
npx hush status
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
### Set/modify secrets (requires user interaction)
|
|
79
|
+
|
|
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
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
After setting, encrypt:
|
|
87
|
+
|
|
88
|
+
\`\`\`bash
|
|
89
|
+
npx hush encrypt
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
### Decrypt to targets
|
|
93
|
+
|
|
94
|
+
\`\`\`bash
|
|
95
|
+
npx hush decrypt # Development
|
|
96
|
+
npx hush decrypt -e production # Production
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Common Workflows
|
|
102
|
+
|
|
103
|
+
### "What secrets are configured?"
|
|
104
|
+
\`\`\`bash
|
|
105
|
+
npx hush inspect
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
### "Is DATABASE_URL set?"
|
|
109
|
+
\`\`\`bash
|
|
110
|
+
npx hush has DATABASE_URL
|
|
111
|
+
\`\`\`
|
|
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\`
|
|
119
|
+
|
|
120
|
+
### "Check all required secrets"
|
|
121
|
+
\`\`\`bash
|
|
122
|
+
npx hush has DATABASE_URL -q && npx hush has API_KEY -q && echo "All configured" || echo "Some missing"
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
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
|
|
133
|
+
|
|
134
|
+
## Files That Are Safe to Read
|
|
135
|
+
|
|
136
|
+
- \`hush.yaml\` - Configuration (no secrets)
|
|
137
|
+
- \`.sops.yaml\` - SOPS config (public key only)
|
|
138
|
+
- \`.env.encrypted\`, \`.env.*.encrypted\` - Encrypted files
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Additional Resources
|
|
143
|
+
|
|
144
|
+
- **First-time setup**: [SETUP.md](SETUP.md)
|
|
145
|
+
- **Command reference**: [REFERENCE.md](REFERENCE.md)
|
|
146
|
+
- **Workflow examples**: [examples/workflows.md](examples/workflows.md)
|
|
147
|
+
`,
|
|
148
|
+
'SETUP.md': `# Hush First-Time Setup
|
|
149
|
+
|
|
150
|
+
This guide walks through setting up Hush from scratch. Follow these steps in order.
|
|
151
|
+
|
|
152
|
+
## Prerequisites Check
|
|
153
|
+
|
|
154
|
+
### 1. Check if SOPS and age are installed
|
|
155
|
+
|
|
156
|
+
\`\`\`bash
|
|
157
|
+
which sops && which age-keygen && echo "Prerequisites installed" || echo "Need to install prerequisites"
|
|
158
|
+
\`\`\`
|
|
159
|
+
|
|
160
|
+
If not installed:
|
|
161
|
+
|
|
162
|
+
**macOS:**
|
|
163
|
+
\`\`\`bash
|
|
164
|
+
brew install sops age
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
**Linux (Debian/Ubuntu):**
|
|
168
|
+
\`\`\`bash
|
|
169
|
+
sudo apt install age
|
|
170
|
+
# SOPS: Download from https://github.com/getsops/sops/releases
|
|
171
|
+
wget https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64 -O /usr/local/bin/sops
|
|
172
|
+
chmod +x /usr/local/bin/sops
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
**Windows (Chocolatey):**
|
|
176
|
+
\`\`\`powershell
|
|
177
|
+
choco install sops age
|
|
178
|
+
\`\`\`
|
|
179
|
+
|
|
180
|
+
### 2. Check for age encryption key
|
|
181
|
+
|
|
182
|
+
\`\`\`bash
|
|
183
|
+
test -f ~/.config/sops/age/key.txt && echo "Key exists" || echo "Need to create key"
|
|
184
|
+
\`\`\`
|
|
185
|
+
|
|
186
|
+
If no key exists, create one:
|
|
187
|
+
|
|
188
|
+
\`\`\`bash
|
|
189
|
+
mkdir -p ~/.config/sops/age
|
|
190
|
+
age-keygen -o ~/.config/sops/age/key.txt
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
### 3. Get your public key
|
|
194
|
+
|
|
195
|
+
\`\`\`bash
|
|
196
|
+
grep "public key:" ~/.config/sops/age/key.txt
|
|
197
|
+
\`\`\`
|
|
198
|
+
|
|
199
|
+
Save this \`age1...\` value - you'll need it for the next step.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Project Setup
|
|
204
|
+
|
|
205
|
+
### Step 1: Install Hush
|
|
206
|
+
|
|
207
|
+
\`\`\`bash
|
|
208
|
+
npm install -D @chriscode/hush
|
|
209
|
+
# or
|
|
210
|
+
pnpm add -D @chriscode/hush
|
|
211
|
+
\`\`\`
|
|
212
|
+
|
|
213
|
+
### Step 2: Create \`.sops.yaml\`
|
|
214
|
+
|
|
215
|
+
Create \`.sops.yaml\` in your repo root with your public key:
|
|
216
|
+
|
|
217
|
+
\`\`\`yaml
|
|
218
|
+
creation_rules:
|
|
219
|
+
- encrypted_regex: '.*'
|
|
220
|
+
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
221
|
+
\`\`\`
|
|
222
|
+
|
|
223
|
+
Replace \`age1xxx...\` with your actual public key from the prerequisites step.
|
|
224
|
+
|
|
225
|
+
### Step 3: Initialize Hush
|
|
226
|
+
|
|
227
|
+
\`\`\`bash
|
|
228
|
+
npx hush init
|
|
229
|
+
\`\`\`
|
|
230
|
+
|
|
231
|
+
This creates \`hush.yaml\` with auto-detected targets based on your project structure.
|
|
232
|
+
|
|
233
|
+
### Step 4: Review \`hush.yaml\`
|
|
234
|
+
|
|
235
|
+
The generated config looks like:
|
|
236
|
+
|
|
237
|
+
\`\`\`yaml
|
|
238
|
+
sources:
|
|
239
|
+
shared: .env
|
|
240
|
+
development: .env.development
|
|
241
|
+
production: .env.production
|
|
242
|
+
|
|
243
|
+
targets:
|
|
244
|
+
- name: root
|
|
245
|
+
path: .
|
|
246
|
+
format: dotenv
|
|
247
|
+
\`\`\`
|
|
248
|
+
|
|
249
|
+
Customize targets for your monorepo. Common patterns:
|
|
250
|
+
|
|
251
|
+
**Next.js app (client vars only):**
|
|
252
|
+
\`\`\`yaml
|
|
253
|
+
- name: web
|
|
254
|
+
path: ./apps/web
|
|
255
|
+
format: dotenv
|
|
256
|
+
include:
|
|
257
|
+
- NEXT_PUBLIC_*
|
|
258
|
+
\`\`\`
|
|
259
|
+
|
|
260
|
+
**API server (exclude client vars):**
|
|
261
|
+
\`\`\`yaml
|
|
262
|
+
- name: api
|
|
263
|
+
path: ./apps/api
|
|
264
|
+
format: wrangler # or dotenv
|
|
265
|
+
exclude:
|
|
266
|
+
- NEXT_PUBLIC_*
|
|
267
|
+
- VITE_*
|
|
268
|
+
\`\`\`
|
|
269
|
+
|
|
270
|
+
**Kubernetes:**
|
|
271
|
+
\`\`\`yaml
|
|
272
|
+
- name: k8s
|
|
273
|
+
path: ./k8s
|
|
274
|
+
format: yaml
|
|
275
|
+
\`\`\`
|
|
276
|
+
|
|
277
|
+
### Step 5: Create initial \`.env\` files
|
|
278
|
+
|
|
279
|
+
Create \`.env\` with shared secrets:
|
|
280
|
+
|
|
281
|
+
\`\`\`bash
|
|
282
|
+
# .env
|
|
283
|
+
DATABASE_URL=postgres://localhost/mydb
|
|
284
|
+
API_KEY=your_api_key_here
|
|
285
|
+
NEXT_PUBLIC_API_URL=http://localhost:3000
|
|
286
|
+
\`\`\`
|
|
287
|
+
|
|
288
|
+
Create \`.env.development\` for dev-specific values:
|
|
289
|
+
|
|
290
|
+
\`\`\`bash
|
|
291
|
+
# .env.development
|
|
292
|
+
DEBUG=true
|
|
293
|
+
LOG_LEVEL=debug
|
|
294
|
+
\`\`\`
|
|
295
|
+
|
|
296
|
+
Create \`.env.production\` for production values:
|
|
297
|
+
|
|
298
|
+
\`\`\`bash
|
|
299
|
+
# .env.production
|
|
300
|
+
DEBUG=false
|
|
301
|
+
LOG_LEVEL=error
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
### Step 6: Encrypt secrets
|
|
305
|
+
|
|
306
|
+
\`\`\`bash
|
|
307
|
+
npx hush encrypt
|
|
308
|
+
\`\`\`
|
|
309
|
+
|
|
310
|
+
This creates:
|
|
311
|
+
- \`.env.encrypted\`
|
|
312
|
+
- \`.env.development.encrypted\`
|
|
313
|
+
- \`.env.production.encrypted\`
|
|
314
|
+
|
|
315
|
+
### Step 7: Verify setup
|
|
316
|
+
|
|
317
|
+
\`\`\`bash
|
|
318
|
+
npx hush status
|
|
319
|
+
npx hush inspect
|
|
320
|
+
\`\`\`
|
|
321
|
+
|
|
322
|
+
### Step 8: Update \`.gitignore\`
|
|
323
|
+
|
|
324
|
+
Add these lines to \`.gitignore\`:
|
|
325
|
+
|
|
326
|
+
\`\`\`gitignore
|
|
327
|
+
# Hush - plaintext env files (generated, not committed)
|
|
328
|
+
.env
|
|
329
|
+
.env.local
|
|
330
|
+
.env.development
|
|
331
|
+
.env.production
|
|
332
|
+
.dev.vars
|
|
333
|
+
|
|
334
|
+
# Keep encrypted files (these ARE committed)
|
|
335
|
+
!.env.encrypted
|
|
336
|
+
!.env.*.encrypted
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
### Step 9: Commit encrypted files
|
|
340
|
+
|
|
341
|
+
\`\`\`bash
|
|
342
|
+
git add .sops.yaml hush.yaml .env*.encrypted .gitignore
|
|
343
|
+
git commit -m "chore: add Hush secrets management"
|
|
344
|
+
\`\`\`
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Team Member Setup
|
|
349
|
+
|
|
350
|
+
When a new team member joins:
|
|
351
|
+
|
|
352
|
+
1. **Get the age private key** from an existing team member
|
|
353
|
+
2. **Save it** to \`~/.config/sops/age/key.txt\`
|
|
354
|
+
3. **Run** \`npx hush decrypt\` to generate local env files
|
|
355
|
+
4. **Start developing**
|
|
356
|
+
|
|
357
|
+
The private key should be shared securely (password manager, encrypted channel, etc.)
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Verification Checklist
|
|
362
|
+
|
|
363
|
+
After setup, verify everything works:
|
|
364
|
+
|
|
365
|
+
- [ ] \`npx hush status\` shows configuration
|
|
366
|
+
- [ ] \`npx hush inspect\` shows masked variables
|
|
367
|
+
- [ ] \`npx hush decrypt\` creates local env files
|
|
368
|
+
- [ ] \`.env.encrypted\` files are committed to git
|
|
369
|
+
- [ ] Plaintext \`.env\` files are in \`.gitignore\`
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Troubleshooting Setup
|
|
374
|
+
|
|
375
|
+
### "age: command not found"
|
|
376
|
+
\`\`\`bash
|
|
377
|
+
brew install age # macOS
|
|
378
|
+
\`\`\`
|
|
379
|
+
|
|
380
|
+
### "sops: command not found"
|
|
381
|
+
\`\`\`bash
|
|
382
|
+
brew install sops # macOS
|
|
383
|
+
\`\`\`
|
|
384
|
+
|
|
385
|
+
### "Error: no matching keys found"
|
|
386
|
+
Your age key doesn't match. Get the correct private key from a team member.
|
|
387
|
+
|
|
388
|
+
### "hush.yaml not found"
|
|
389
|
+
Run \`npx hush init\` to generate configuration.
|
|
390
|
+
|
|
391
|
+
### "No sources defined in hush.yaml"
|
|
392
|
+
Edit \`hush.yaml\` and add your source files under \`sources:\`.
|
|
393
|
+
`,
|
|
394
|
+
'REFERENCE.md': `# Hush Command Reference
|
|
395
|
+
|
|
396
|
+
Complete reference for all Hush CLI commands with flags, options, and examples.
|
|
397
|
+
|
|
398
|
+
## Global Options
|
|
399
|
+
|
|
400
|
+
These options work with most commands:
|
|
401
|
+
|
|
402
|
+
| Option | Description |
|
|
403
|
+
|--------|-------------|
|
|
404
|
+
| \`-e, --env <env>\` | Environment: \`development\` (or \`dev\`) / \`production\` (or \`prod\`). Default: \`development\` |
|
|
405
|
+
| \`-r, --root <dir>\` | Root directory containing \`hush.yaml\`. Default: current directory |
|
|
406
|
+
| \`-h, --help\` | Show help message |
|
|
407
|
+
| \`-v, --version\` | Show version number |
|
|
408
|
+
|
|
409
|
+
## Commands
|
|
410
|
+
|
|
411
|
+
### hush init
|
|
412
|
+
|
|
413
|
+
Generate a \`hush.yaml\` configuration file with auto-detected targets.
|
|
414
|
+
|
|
415
|
+
\`\`\`bash
|
|
416
|
+
hush init
|
|
417
|
+
\`\`\`
|
|
418
|
+
|
|
419
|
+
Scans for \`package.json\` and \`wrangler.toml\` files to auto-detect targets.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
### hush encrypt
|
|
424
|
+
|
|
425
|
+
Encrypt source \`.env\` files to \`.env.encrypted\` files.
|
|
426
|
+
|
|
427
|
+
\`\`\`bash
|
|
428
|
+
hush encrypt
|
|
429
|
+
\`\`\`
|
|
430
|
+
|
|
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\`
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### hush decrypt
|
|
439
|
+
|
|
440
|
+
Decrypt and distribute secrets to all configured targets.
|
|
441
|
+
|
|
442
|
+
\`\`\`bash
|
|
443
|
+
hush decrypt # Development (default)
|
|
444
|
+
hush decrypt -e production # Production
|
|
445
|
+
hush decrypt -e prod # Short form
|
|
446
|
+
\`\`\`
|
|
447
|
+
|
|
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
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### hush set (alias: edit)
|
|
458
|
+
|
|
459
|
+
Set or modify secrets. Opens encrypted file in your \`$EDITOR\`.
|
|
460
|
+
|
|
461
|
+
\`\`\`bash
|
|
462
|
+
hush set # Set shared secrets
|
|
463
|
+
hush set development # Set development secrets
|
|
464
|
+
hush set production # Set production secrets
|
|
465
|
+
\`\`\`
|
|
466
|
+
|
|
467
|
+
Opens a temporary decrypted file, re-encrypts on save.
|
|
468
|
+
|
|
469
|
+
**Tip:** Set your editor with \`export EDITOR=vim\` or use \`code --wait\` for VS Code.
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
### hush list
|
|
474
|
+
|
|
475
|
+
List all variables with their **actual values**.
|
|
476
|
+
|
|
477
|
+
\`\`\`bash
|
|
478
|
+
hush list # Development
|
|
479
|
+
hush list -e production # Production
|
|
480
|
+
\`\`\`
|
|
481
|
+
|
|
482
|
+
**WARNING:** This shows real secret values. Use \`hush inspect\` for AI-safe output.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
### hush inspect (AI-Safe)
|
|
487
|
+
|
|
488
|
+
List all variables with **masked values**. Safe for AI agents.
|
|
489
|
+
|
|
490
|
+
\`\`\`bash
|
|
491
|
+
hush inspect # Development
|
|
492
|
+
hush inspect -e production # Production
|
|
493
|
+
\`\`\`
|
|
494
|
+
|
|
495
|
+
**Output format:**
|
|
496
|
+
\`\`\`
|
|
497
|
+
Secrets for development:
|
|
498
|
+
|
|
499
|
+
DATABASE_URL = post****************... (45 chars)
|
|
500
|
+
STRIPE_SECRET_KEY = sk_t****************... (32 chars)
|
|
501
|
+
API_KEY = (not set)
|
|
502
|
+
|
|
503
|
+
Total: 3 variables
|
|
504
|
+
|
|
505
|
+
Target distribution:
|
|
506
|
+
|
|
507
|
+
root (.) - 3 vars
|
|
508
|
+
app (./app/) - 1 vars
|
|
509
|
+
include: EXPO_PUBLIC_*
|
|
510
|
+
api (./api/) - 2 vars
|
|
511
|
+
exclude: EXPO_PUBLIC_*
|
|
512
|
+
\`\`\`
|
|
513
|
+
|
|
514
|
+
**What's visible:**
|
|
515
|
+
- Variable names
|
|
516
|
+
- First 4 characters (helps identify type: \`sk_\` = Stripe, \`ghp_\` = GitHub)
|
|
517
|
+
- Value length
|
|
518
|
+
- Which targets receive which variables
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
### hush has (AI-Safe)
|
|
523
|
+
|
|
524
|
+
Check if a specific secret exists.
|
|
525
|
+
|
|
526
|
+
\`\`\`bash
|
|
527
|
+
hush has <KEY> # Verbose output
|
|
528
|
+
hush has <KEY> -q # Quiet mode (exit code only)
|
|
529
|
+
hush has <KEY> --quiet # Same as -q
|
|
530
|
+
\`\`\`
|
|
531
|
+
|
|
532
|
+
**Exit codes:**
|
|
533
|
+
- \`0\` - Variable is set
|
|
534
|
+
- \`1\` - Variable not found
|
|
535
|
+
|
|
536
|
+
**Examples:**
|
|
537
|
+
\`\`\`bash
|
|
538
|
+
# Check with output
|
|
539
|
+
hush has DATABASE_URL
|
|
540
|
+
# Output: DATABASE_URL is set (45 chars)
|
|
541
|
+
|
|
542
|
+
# Check missing variable
|
|
543
|
+
hush has MISSING_KEY
|
|
544
|
+
# Output: MISSING_KEY not found
|
|
545
|
+
# Exit code: 1
|
|
546
|
+
|
|
547
|
+
# Use in scripts
|
|
548
|
+
hush has DATABASE_URL -q && echo "DB ready"
|
|
549
|
+
|
|
550
|
+
# Check multiple
|
|
551
|
+
hush has DB_URL -q && hush has API_KEY -q && echo "All set"
|
|
552
|
+
\`\`\`
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
### hush push
|
|
557
|
+
|
|
558
|
+
Push production secrets to Cloudflare Workers.
|
|
559
|
+
|
|
560
|
+
\`\`\`bash
|
|
561
|
+
hush push # Push secrets
|
|
562
|
+
hush push --dry-run # Preview without pushing
|
|
563
|
+
\`\`\`
|
|
564
|
+
|
|
565
|
+
**Options:**
|
|
566
|
+
|
|
567
|
+
| Option | Description |
|
|
568
|
+
|--------|-------------|
|
|
569
|
+
| \`--dry-run\` | Preview what would be pushed, don't actually push |
|
|
570
|
+
|
|
571
|
+
**Requirements:**
|
|
572
|
+
- Target must have \`format: wrangler\`
|
|
573
|
+
- \`wrangler.toml\` must exist in target path
|
|
574
|
+
- \`wrangler\` CLI must be installed and authenticated
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
### hush status
|
|
579
|
+
|
|
580
|
+
Show configuration and file status.
|
|
581
|
+
|
|
582
|
+
\`\`\`bash
|
|
583
|
+
hush status
|
|
584
|
+
\`\`\`
|
|
585
|
+
|
|
586
|
+
**Output includes:**
|
|
587
|
+
- Configuration file location
|
|
588
|
+
- Source files and their encryption status
|
|
589
|
+
- Target configuration (paths, formats, filters)
|
|
590
|
+
- Whether files are in sync
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
### hush check
|
|
595
|
+
|
|
596
|
+
Verify secrets are encrypted (useful for pre-commit hooks).
|
|
597
|
+
|
|
598
|
+
\`\`\`bash
|
|
599
|
+
hush check # Basic check
|
|
600
|
+
hush check --warn # Warn but don't fail
|
|
601
|
+
hush check --json # JSON output for CI
|
|
602
|
+
hush check --only-changed # Only check git-modified files
|
|
603
|
+
hush check --require-source # Fail if source file missing
|
|
604
|
+
\`\`\`
|
|
605
|
+
|
|
606
|
+
**Exit codes:**
|
|
607
|
+
- \`0\` - All in sync
|
|
608
|
+
- \`1\` - Drift detected (run \`hush encrypt\`)
|
|
609
|
+
- \`2\` - Config error
|
|
610
|
+
- \`3\` - Runtime error (sops missing, decrypt failed)
|
|
611
|
+
|
|
612
|
+
**Pre-commit hook (Husky):**
|
|
613
|
+
\`\`\`bash
|
|
614
|
+
# .husky/pre-commit
|
|
615
|
+
npx hush check || exit 1
|
|
616
|
+
\`\`\`
|
|
617
|
+
|
|
618
|
+
Bypass with: \`HUSH_SKIP_CHECK=1 git commit -m "message"\`
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
### hush skill
|
|
623
|
+
|
|
624
|
+
Install the Claude Code / OpenCode skill for AI-safe secrets management.
|
|
625
|
+
|
|
626
|
+
\`\`\`bash
|
|
627
|
+
hush skill # Interactive: choose global or local
|
|
628
|
+
hush skill --global # Install to ~/.claude/skills/
|
|
629
|
+
hush skill --local # Install to ./.claude/skills/
|
|
630
|
+
\`\`\`
|
|
631
|
+
|
|
632
|
+
**Global install:** Works across all your projects. Recommended for personal use.
|
|
633
|
+
|
|
634
|
+
**Local install:** Bundled with the project. Recommended for teams (skill travels with the repo).
|
|
635
|
+
|
|
636
|
+
## Configuration File (hush.yaml)
|
|
637
|
+
|
|
638
|
+
\`\`\`yaml
|
|
639
|
+
sources:
|
|
640
|
+
shared: .env
|
|
641
|
+
development: .env.development
|
|
642
|
+
production: .env.production
|
|
643
|
+
|
|
644
|
+
targets:
|
|
645
|
+
- name: root
|
|
646
|
+
path: .
|
|
647
|
+
format: dotenv
|
|
648
|
+
|
|
649
|
+
- name: app
|
|
650
|
+
path: ./packages/app
|
|
651
|
+
format: dotenv
|
|
652
|
+
include:
|
|
653
|
+
- EXPO_PUBLIC_*
|
|
654
|
+
- NEXT_PUBLIC_*
|
|
655
|
+
|
|
656
|
+
- name: api
|
|
657
|
+
path: ./packages/api
|
|
658
|
+
format: wrangler
|
|
659
|
+
exclude:
|
|
660
|
+
- EXPO_PUBLIC_*
|
|
661
|
+
\`\`\`
|
|
662
|
+
|
|
663
|
+
### Target Options
|
|
664
|
+
|
|
665
|
+
| Option | Description |
|
|
666
|
+
|--------|-------------|
|
|
667
|
+
| \`name\` | Identifier for the target |
|
|
668
|
+
| \`path\` | Directory to write output file |
|
|
669
|
+
| \`format\` | Output format: \`dotenv\`, \`wrangler\`, \`json\`, \`shell\`, \`yaml\` |
|
|
670
|
+
| \`include\` | Glob patterns to include (e.g., \`NEXT_PUBLIC_*\`) |
|
|
671
|
+
| \`exclude\` | Glob patterns to exclude |
|
|
672
|
+
|
|
673
|
+
### Output Formats
|
|
674
|
+
|
|
675
|
+
| Format | Output File | Use Case |
|
|
676
|
+
|--------|-------------|----------|
|
|
677
|
+
| \`dotenv\` | \`.env.development\` / \`.env.production\` | Next.js, Vite, Expo, Remix, Node.js |
|
|
678
|
+
| \`wrangler\` | \`.dev.vars\` | Cloudflare Workers & Pages |
|
|
679
|
+
| \`json\` | \`.env.development.json\` | AWS Lambda, serverless, JSON configs |
|
|
680
|
+
| \`shell\` | \`.env.development.sh\` | CI/CD pipelines, Docker builds |
|
|
681
|
+
| \`yaml\` | \`.env.development.yaml\` | Kubernetes ConfigMaps, Docker Compose |
|
|
682
|
+
|
|
683
|
+
### Framework Client Prefixes
|
|
684
|
+
|
|
685
|
+
| Framework | Client Prefix | Example |
|
|
686
|
+
|-----------|--------------|---------|
|
|
687
|
+
| Next.js | \`NEXT_PUBLIC_*\` | \`include: [NEXT_PUBLIC_*]\` |
|
|
688
|
+
| Vite | \`VITE_*\` | \`include: [VITE_*]\` |
|
|
689
|
+
| Create React App | \`REACT_APP_*\` | \`include: [REACT_APP_*]\` |
|
|
690
|
+
| Vue CLI | \`VUE_APP_*\` | \`include: [VUE_APP_*]\` |
|
|
691
|
+
| Nuxt | \`NUXT_PUBLIC_*\` | \`include: [NUXT_PUBLIC_*]\` |
|
|
692
|
+
| Astro | \`PUBLIC_*\` | \`include: [PUBLIC_*]\` |
|
|
693
|
+
| SvelteKit | \`PUBLIC_*\` | \`include: [PUBLIC_*]\` |
|
|
694
|
+
| Expo | \`EXPO_PUBLIC_*\` | \`include: [EXPO_PUBLIC_*]\` |
|
|
695
|
+
| Gatsby | \`GATSBY_*\` | \`include: [GATSBY_*]\` |
|
|
696
|
+
`,
|
|
697
|
+
'examples/workflows.md': `# Hush Workflow Examples
|
|
698
|
+
|
|
699
|
+
Step-by-step examples for common AI assistant workflows when working with secrets.
|
|
700
|
+
|
|
701
|
+
## Checking Configuration
|
|
702
|
+
|
|
703
|
+
### "What environment variables does this project use?"
|
|
704
|
+
|
|
705
|
+
\`\`\`bash
|
|
706
|
+
hush inspect
|
|
707
|
+
\`\`\`
|
|
708
|
+
|
|
709
|
+
Read the output to see all configured variables, their approximate lengths, and which targets receive them.
|
|
710
|
+
|
|
711
|
+
### "Is the database configured?"
|
|
712
|
+
|
|
713
|
+
\`\`\`bash
|
|
714
|
+
hush has DATABASE_URL
|
|
715
|
+
\`\`\`
|
|
716
|
+
|
|
717
|
+
If the output says "not found", guide the user to add it.
|
|
718
|
+
|
|
719
|
+
### "Are all required secrets set?"
|
|
720
|
+
|
|
721
|
+
\`\`\`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"
|
|
726
|
+
\`\`\`
|
|
727
|
+
|
|
728
|
+
Or check all at once:
|
|
729
|
+
\`\`\`bash
|
|
730
|
+
hush has DATABASE_URL -q && \\
|
|
731
|
+
hush has API_KEY -q && \\
|
|
732
|
+
hush has STRIPE_SECRET_KEY -q && \\
|
|
733
|
+
echo "All secrets configured" || \\
|
|
734
|
+
echo "Some secrets missing"
|
|
735
|
+
\`\`\`
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
## Helping Users Add Secrets
|
|
740
|
+
|
|
741
|
+
### "Help me add a new API key"
|
|
742
|
+
|
|
743
|
+
1. **Check if it already exists:**
|
|
744
|
+
\`\`\`bash
|
|
745
|
+
hush has NEW_API_KEY
|
|
746
|
+
\`\`\`
|
|
747
|
+
|
|
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
|
+
\`\`\`
|
|
763
|
+
|
|
764
|
+
### "I need to add secrets for production"
|
|
765
|
+
|
|
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\`.
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
## Debugging Issues
|
|
774
|
+
|
|
775
|
+
### "My app can't find DATABASE_URL"
|
|
776
|
+
|
|
777
|
+
1. **Check if the variable exists:**
|
|
778
|
+
\`\`\`bash
|
|
779
|
+
hush has DATABASE_URL
|
|
780
|
+
\`\`\`
|
|
781
|
+
|
|
782
|
+
2. **If it exists, check target distribution:**
|
|
783
|
+
\`\`\`bash
|
|
784
|
+
hush inspect
|
|
785
|
+
\`\`\`
|
|
786
|
+
Look at the "Target distribution" section to see which targets receive it.
|
|
787
|
+
|
|
788
|
+
3. **Check if it's filtered out:**
|
|
789
|
+
\`\`\`bash
|
|
790
|
+
cat hush.yaml
|
|
791
|
+
\`\`\`
|
|
792
|
+
Look for \`include\`/\`exclude\` patterns that might filter the variable.
|
|
793
|
+
|
|
794
|
+
4. **Regenerate env files:**
|
|
795
|
+
\`\`\`bash
|
|
796
|
+
hush decrypt
|
|
797
|
+
\`\`\`
|
|
798
|
+
|
|
799
|
+
### "Secrets aren't reaching my API folder"
|
|
800
|
+
|
|
801
|
+
1. **Check target configuration:**
|
|
802
|
+
\`\`\`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
|
|
817
|
+
\`\`\`
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
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
|
+
## Team Workflows
|
|
856
|
+
|
|
857
|
+
### "New team member setup"
|
|
858
|
+
|
|
859
|
+
Guide them:
|
|
860
|
+
> 1. Get the age private key from a team member
|
|
861
|
+
> 2. Save it to \`~/.config/sops/age/key.txt\`
|
|
862
|
+
> 3. Run \`hush decrypt\` to generate local env files
|
|
863
|
+
> 4. Start developing!
|
|
864
|
+
|
|
865
|
+
### "Someone added new secrets, my app is broken"
|
|
866
|
+
|
|
867
|
+
\`\`\`bash
|
|
868
|
+
# Pull latest changes
|
|
869
|
+
git pull
|
|
870
|
+
|
|
871
|
+
# Regenerate env files
|
|
872
|
+
hush decrypt
|
|
873
|
+
\`\`\`
|
|
874
|
+
|
|
875
|
+
### "Check if I forgot to encrypt changes"
|
|
876
|
+
|
|
877
|
+
\`\`\`bash
|
|
878
|
+
hush check
|
|
879
|
+
\`\`\`
|
|
880
|
+
|
|
881
|
+
If drift detected:
|
|
882
|
+
\`\`\`bash
|
|
883
|
+
hush encrypt
|
|
884
|
+
git add .env*.encrypted
|
|
885
|
+
git commit -m "chore: encrypt new secrets"
|
|
886
|
+
\`\`\`
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## Understanding the Output
|
|
891
|
+
|
|
892
|
+
### hush inspect output explained
|
|
893
|
+
|
|
894
|
+
\`\`\`
|
|
895
|
+
Secrets for development:
|
|
896
|
+
|
|
897
|
+
DATABASE_URL = post****************... (45 chars)
|
|
898
|
+
STRIPE_SECRET_KEY = sk_t****************... (32 chars)
|
|
899
|
+
API_KEY = (not set)
|
|
900
|
+
|
|
901
|
+
Total: 3 variables
|
|
902
|
+
|
|
903
|
+
Target distribution:
|
|
904
|
+
|
|
905
|
+
root (.) - 3 vars
|
|
906
|
+
app (./app/) - 1 vars
|
|
907
|
+
include: EXPO_PUBLIC_*
|
|
908
|
+
api (./api/) - 2 vars
|
|
909
|
+
exclude: EXPO_PUBLIC_*
|
|
910
|
+
\`\`\`
|
|
911
|
+
|
|
912
|
+
**Reading this:**
|
|
913
|
+
- \`DATABASE_URL\` is set, starts with "post", is 45 characters (likely a postgres:// URL)
|
|
914
|
+
- \`STRIPE_SECRET_KEY\` starts with "sk_t" (Stripe test key format)
|
|
915
|
+
- \`API_KEY\` is not set - user needs to add it
|
|
916
|
+
- The \`app\` folder only gets \`EXPO_PUBLIC_*\` variables
|
|
917
|
+
- The \`api\` folder gets everything except \`EXPO_PUBLIC_*\`
|
|
918
|
+
|
|
919
|
+
### hush has output explained
|
|
920
|
+
|
|
921
|
+
\`\`\`bash
|
|
922
|
+
$ hush has DATABASE_URL
|
|
923
|
+
DATABASE_URL is set (45 chars)
|
|
924
|
+
|
|
925
|
+
$ hush has MISSING_VAR
|
|
926
|
+
MISSING_VAR not found
|
|
927
|
+
\`\`\`
|
|
928
|
+
|
|
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).
|
|
930
|
+
`,
|
|
931
|
+
};
|
|
932
|
+
function getSkillPath(location, root) {
|
|
933
|
+
if (location === 'global') {
|
|
934
|
+
return join(homedir(), '.claude', 'skills', 'hush-secrets');
|
|
935
|
+
}
|
|
936
|
+
return join(root, '.claude', 'skills', 'hush-secrets');
|
|
937
|
+
}
|
|
938
|
+
async function promptForLocation() {
|
|
939
|
+
const rl = createInterface({
|
|
940
|
+
input: process.stdin,
|
|
941
|
+
output: process.stdout,
|
|
942
|
+
});
|
|
943
|
+
return new Promise((resolve) => {
|
|
944
|
+
console.log(pc.bold('\nWhere would you like to install the Claude skill?\n'));
|
|
945
|
+
console.log(` ${pc.cyan('1)')} ${pc.bold('Global')} ${pc.dim('(~/.claude/skills/)')}`);
|
|
946
|
+
console.log(` Works across all your projects. Recommended for personal use.\n`);
|
|
947
|
+
console.log(` ${pc.cyan('2)')} ${pc.bold('Local')} ${pc.dim('(.claude/skills/)')}`);
|
|
948
|
+
console.log(` Bundled with this project. Recommended for teams.\n`);
|
|
949
|
+
rl.question(`${pc.bold('Choice')} ${pc.dim('[1/2]')}: `, (answer) => {
|
|
950
|
+
rl.close();
|
|
951
|
+
const choice = answer.trim();
|
|
952
|
+
if (choice === '2' || choice.toLowerCase() === 'local') {
|
|
953
|
+
resolve('local');
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
resolve('global');
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
function writeSkillFiles(skillPath) {
|
|
962
|
+
mkdirSync(skillPath, { recursive: true });
|
|
963
|
+
mkdirSync(join(skillPath, 'examples'), { recursive: true });
|
|
964
|
+
for (const [filename, content] of Object.entries(SKILL_FILES)) {
|
|
965
|
+
const filePath = join(skillPath, filename);
|
|
966
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
export async function skillCommand(options) {
|
|
970
|
+
const { root, global: isGlobal, local: isLocal } = options;
|
|
971
|
+
let location;
|
|
972
|
+
if (isGlobal) {
|
|
973
|
+
location = 'global';
|
|
974
|
+
}
|
|
975
|
+
else if (isLocal) {
|
|
976
|
+
location = 'local';
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
location = await promptForLocation();
|
|
980
|
+
}
|
|
981
|
+
const skillPath = getSkillPath(location, root);
|
|
982
|
+
const alreadyInstalled = existsSync(join(skillPath, 'SKILL.md'));
|
|
983
|
+
if (alreadyInstalled) {
|
|
984
|
+
console.log(pc.yellow(`\nSkill already installed at: ${skillPath}`));
|
|
985
|
+
console.log(pc.dim('To reinstall, delete the directory first.\n'));
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
console.log(pc.blue(`\nInstalling Claude skill to: ${skillPath}`));
|
|
989
|
+
writeSkillFiles(skillPath);
|
|
990
|
+
console.log(pc.green('\n✓ Skill installed successfully!\n'));
|
|
991
|
+
if (location === 'global') {
|
|
992
|
+
console.log(pc.dim('The skill is now active for all projects using Claude Code.\n'));
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
console.log(pc.dim('The skill is now bundled with this project.'));
|
|
996
|
+
console.log(pc.dim('Commit the .claude/ directory to share with your team.\n'));
|
|
997
|
+
console.log(pc.bold('Suggested:'));
|
|
998
|
+
console.log(` git add .claude/`);
|
|
999
|
+
console.log(` git commit -m "chore: add Hush Claude skill"\n`);
|
|
1000
|
+
}
|
|
1001
|
+
console.log(pc.bold('What the skill does:'));
|
|
1002
|
+
console.log(` • Teaches AI to use ${pc.cyan('hush inspect')} instead of reading .env files`);
|
|
1003
|
+
console.log(` • Prevents accidental exposure of secrets to LLMs`);
|
|
1004
|
+
console.log(` • Guides AI through adding/modifying secrets safely\n`);
|
|
1005
|
+
}
|
package/dist/formats/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { formatDotenv } from './dotenv.js';
|
|
|
3
3
|
import { formatJson } from './json.js';
|
|
4
4
|
import { formatShell } from './shell.js';
|
|
5
5
|
import { formatWrangler } from './wrangler.js';
|
|
6
|
+
import { formatYaml } from './yaml.js';
|
|
6
7
|
export declare function formatVars(vars: EnvVar[], format: OutputFormat): string;
|
|
7
|
-
export { formatDotenv, formatJson, formatShell, formatWrangler };
|
|
8
|
+
export { formatDotenv, formatJson, formatShell, formatWrangler, formatYaml };
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/formats/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/formats/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,CAavE;AAED,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC"}
|
package/dist/formats/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { formatDotenv } from './dotenv.js';
|
|
|
2
2
|
import { formatJson } from './json.js';
|
|
3
3
|
import { formatShell } from './shell.js';
|
|
4
4
|
import { formatWrangler } from './wrangler.js';
|
|
5
|
+
import { formatYaml } from './yaml.js';
|
|
5
6
|
export function formatVars(vars, format) {
|
|
6
7
|
switch (format) {
|
|
7
8
|
case 'dotenv':
|
|
@@ -12,6 +13,8 @@ export function formatVars(vars, format) {
|
|
|
12
13
|
return formatJson(vars);
|
|
13
14
|
case 'shell':
|
|
14
15
|
return formatShell(vars);
|
|
16
|
+
case 'yaml':
|
|
17
|
+
return formatYaml(vars);
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
|
-
export { formatDotenv, formatJson, formatShell, formatWrangler };
|
|
20
|
+
export { formatDotenv, formatJson, formatShell, formatWrangler, formatYaml };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EnvVar } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Format environment variables as YAML.
|
|
4
|
+
* Useful for Kubernetes ConfigMaps, Docker Compose, and other YAML-based configs.
|
|
5
|
+
*
|
|
6
|
+
* Output format:
|
|
7
|
+
* ```yaml
|
|
8
|
+
* DATABASE_URL: "postgres://localhost/db"
|
|
9
|
+
* API_KEY: "sk_test_xxx"
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatYaml(vars: EnvVar[]): string;
|
|
13
|
+
//# sourceMappingURL=yaml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../../src/formats/yaml.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CA6CjD"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format environment variables as YAML.
|
|
3
|
+
* Useful for Kubernetes ConfigMaps, Docker Compose, and other YAML-based configs.
|
|
4
|
+
*
|
|
5
|
+
* Output format:
|
|
6
|
+
* ```yaml
|
|
7
|
+
* DATABASE_URL: "postgres://localhost/db"
|
|
8
|
+
* API_KEY: "sk_test_xxx"
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export function formatYaml(vars) {
|
|
12
|
+
if (vars.length === 0) {
|
|
13
|
+
return '{}\n';
|
|
14
|
+
}
|
|
15
|
+
return (vars
|
|
16
|
+
.map(({ key, value }) => {
|
|
17
|
+
// YAML string escaping rules:
|
|
18
|
+
// - Simple alphanumeric values don't need quotes
|
|
19
|
+
// - Values with special chars need double quotes
|
|
20
|
+
// - Double quotes inside values need escaping
|
|
21
|
+
const needsQuotes = value === '' ||
|
|
22
|
+
value.includes(':') ||
|
|
23
|
+
value.includes('#') ||
|
|
24
|
+
value.includes("'") ||
|
|
25
|
+
value.includes('"') ||
|
|
26
|
+
value.includes('\n') ||
|
|
27
|
+
value.includes('\\') ||
|
|
28
|
+
value.startsWith(' ') ||
|
|
29
|
+
value.endsWith(' ') ||
|
|
30
|
+
value.startsWith('!') ||
|
|
31
|
+
value.startsWith('&') ||
|
|
32
|
+
value.startsWith('*') ||
|
|
33
|
+
value.startsWith('|') ||
|
|
34
|
+
value.startsWith('>') ||
|
|
35
|
+
value.startsWith('%') ||
|
|
36
|
+
value.startsWith('@') ||
|
|
37
|
+
value.startsWith('`') ||
|
|
38
|
+
/^(true|false|yes|no|on|off|null|~)$/i.test(value) ||
|
|
39
|
+
/^-?\d+(\.\d+)?$/.test(value) ||
|
|
40
|
+
/^0x[0-9a-fA-F]+$/.test(value) ||
|
|
41
|
+
/^0o[0-7]+$/.test(value);
|
|
42
|
+
if (needsQuotes) {
|
|
43
|
+
// Escape backslashes and double quotes for YAML double-quoted strings
|
|
44
|
+
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
45
|
+
return `${key}: "${escaped}"`;
|
|
46
|
+
}
|
|
47
|
+
return `${key}: ${value}`;
|
|
48
|
+
})
|
|
49
|
+
.join('\n') + '\n');
|
|
50
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type OutputFormat = 'dotenv' | 'wrangler' | 'json' | 'shell';
|
|
1
|
+
export type OutputFormat = 'dotenv' | 'wrangler' | 'json' | 'shell' | 'yaml';
|
|
2
2
|
export type Environment = 'development' | 'production';
|
|
3
3
|
export interface Target {
|
|
4
4
|
name: string;
|
|
@@ -67,6 +67,11 @@ export interface CheckResult {
|
|
|
67
67
|
status: 'ok' | 'drift' | 'error';
|
|
68
68
|
files: CheckFileResult[];
|
|
69
69
|
}
|
|
70
|
+
export interface SkillOptions {
|
|
71
|
+
root: string;
|
|
72
|
+
global?: boolean;
|
|
73
|
+
local?: boolean;
|
|
74
|
+
}
|
|
70
75
|
export declare const DEFAULT_SOURCES: SourceFiles;
|
|
71
76
|
export declare const FORMAT_OUTPUT_FILES: Record<OutputFormat, Record<Environment, string>>;
|
|
72
77
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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,CAAC;
|
|
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"}
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chriscode/hush",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,14 @@
|
|
|
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
|
+
},
|
|
15
23
|
"keywords": [
|
|
16
24
|
"secrets",
|
|
17
25
|
"sops",
|
|
@@ -53,12 +61,5 @@
|
|
|
53
61
|
],
|
|
54
62
|
"publishConfig": {
|
|
55
63
|
"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"
|
|
63
64
|
}
|
|
64
|
-
}
|
|
65
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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.
|