no-more-leaked-keys 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +63 -0
- package/README.md +395 -0
- package/bin/install.js +216 -0
- package/commands/add-mcp.md +17 -0
- package/commands/secrets.md +12 -0
- package/hooks/block-unsafe-mcp-add.sh +17 -0
- package/keychain-secrets/SKILL.md +129 -0
- package/keychain-secrets/references/env-file-patterns.md +220 -0
- package/keychain-secrets/references/keychain-commands.md +159 -0
- package/keychain-secrets/workflows/add-key.md +99 -0
- package/keychain-secrets/workflows/add-mcp.md +171 -0
- package/keychain-secrets/workflows/list-keys.md +83 -0
- package/keychain-secrets/workflows/populate-env.md +128 -0
- package/keychain-secrets/workflows/remove-key.md +71 -0
- package/package.json +49 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Hook: Block unsafe 'claude mcp add' commands that would expose API keys
|
|
3
|
+
# Installed by: no-more-leaked-keys skill
|
|
4
|
+
|
|
5
|
+
# This hook is triggered by Claude Code's PreToolUse event
|
|
6
|
+
# It checks if the command contains 'claude mcp add' with auth headers
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
|
|
10
|
+
# Check if this is a Bash command containing unsafe mcp add
|
|
11
|
+
if echo "$INPUT" | grep -q "claude mcp add" && echo "$INPUT" | grep -qi "header.*authorization\|header.*bearer"; then
|
|
12
|
+
echo '{"decision": "block", "message": "BLOCKED: claude mcp add with auth headers exposes your API key in terminal output!\n\nUse /secrets or /add-mcp instead - these pull keys from Keychain securely without exposing them.\n\nSee: https://github.com/Vibe-Marketer/no-more-leaked-keys"}'
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Allow all other commands
|
|
17
|
+
echo '{"decision": "allow"}'
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: keychain-secrets
|
|
3
|
+
description: "Securely manage API keys using macOS Keychain. Use when working with .env files, adding new API keys, adding MCP servers, or populating environment variables from Keychain. Triggers on: api key, keychain, .env, environment variables, secrets, credentials, mcp, mcp server, claude mcp add."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<essential_principles>
|
|
7
|
+
## Security-First API Key Management
|
|
8
|
+
|
|
9
|
+
This skill uses macOS Keychain to securely store and retrieve API keys. Keys are never stored in plain text, never committed to git, and never exposed on screen during input.
|
|
10
|
+
|
|
11
|
+
### Principle 1: Keychain is the Source of Truth
|
|
12
|
+
|
|
13
|
+
API keys live in Keychain, not in files. The `.env` file is populated at runtime from Keychain. This means:
|
|
14
|
+
- Keys are encrypted at rest by macOS
|
|
15
|
+
- Keys can be used across any project on your machine
|
|
16
|
+
- Accidental exposure via git or screen share is impossible
|
|
17
|
+
|
|
18
|
+
### Principle 2: Safe Input for New Keys
|
|
19
|
+
|
|
20
|
+
When adding keys to Keychain, we use `read -s` to prevent the key from appearing on screen. The key is passed directly to the `security` command without echoing.
|
|
21
|
+
|
|
22
|
+
### Principle 3: .env Files are Ephemeral
|
|
23
|
+
|
|
24
|
+
The `.env` file should be in `.gitignore` and can be regenerated anytime from Keychain. Never edit `.env` directly with secrets - always add to Keychain first.
|
|
25
|
+
|
|
26
|
+
### Key Naming Convention
|
|
27
|
+
|
|
28
|
+
Use SCREAMING_SNAKE_CASE that matches the environment variable name:
|
|
29
|
+
- `OPENAI_API_KEY`
|
|
30
|
+
- `ANTHROPIC_API_KEY`
|
|
31
|
+
- `STRIPE_SECRET_KEY`
|
|
32
|
+
- `DATABASE_URL`
|
|
33
|
+
</essential_principles>
|
|
34
|
+
|
|
35
|
+
<intake>
|
|
36
|
+
What would you like to do?
|
|
37
|
+
|
|
38
|
+
1. **Populate .env from Keychain** - Retrieve stored keys and write them to a .env file
|
|
39
|
+
2. **Add a new key to Keychain** - Securely store a new API key (won't appear on screen)
|
|
40
|
+
3. **Add MCP server securely** - Add an MCP to Claude Code or OpenCode WITHOUT exposing keys
|
|
41
|
+
4. **List stored keys** - See what keys are available in your Keychain
|
|
42
|
+
5. **Remove a key** - Delete a key from Keychain
|
|
43
|
+
|
|
44
|
+
**Wait for response before proceeding.**
|
|
45
|
+
</intake>
|
|
46
|
+
|
|
47
|
+
<routing>
|
|
48
|
+
| Response | Workflow |
|
|
49
|
+
|----------|----------|
|
|
50
|
+
| 1, "populate", "env", "retrieve", "get", "write env" | `workflows/populate-env.md` |
|
|
51
|
+
| 2, "add", "store", "new key", "save" | `workflows/add-key.md` |
|
|
52
|
+
| 3, "mcp", "add mcp", "claude mcp", "opencode mcp" | `workflows/add-mcp.md` |
|
|
53
|
+
| 4, "list", "show", "available", "what keys" | `workflows/list-keys.md` |
|
|
54
|
+
| 5, "remove", "delete", "revoke" | `workflows/remove-key.md` |
|
|
55
|
+
|
|
56
|
+
**After reading the workflow, follow it exactly.**
|
|
57
|
+
</routing>
|
|
58
|
+
|
|
59
|
+
<critical_warning>
|
|
60
|
+
## NEVER USE `claude mcp add` WITH AUTH HEADERS
|
|
61
|
+
|
|
62
|
+
The `claude mcp add` command **exposes secrets in terminal output**:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# DANGEROUS - This will print your API key to the terminal!
|
|
66
|
+
claude mcp add --transport http tally https://api.tally.so/mcp \
|
|
67
|
+
--header "Authorization: Bearer $TALLY_API_KEY"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Always use option 3 (Add MCP server securely) instead.** This workflow:
|
|
71
|
+
- Pulls the key from Keychain silently
|
|
72
|
+
- Writes directly to config files
|
|
73
|
+
- Never echoes the key anywhere
|
|
74
|
+
</critical_warning>
|
|
75
|
+
|
|
76
|
+
<reference_index>
|
|
77
|
+
All domain knowledge in `references/`:
|
|
78
|
+
|
|
79
|
+
**Commands:** keychain-commands.md - All macOS security CLI patterns
|
|
80
|
+
**Env Files:** env-file-patterns.md - Safe .env file handling
|
|
81
|
+
</reference_index>
|
|
82
|
+
|
|
83
|
+
<workflows_index>
|
|
84
|
+
| Workflow | Purpose |
|
|
85
|
+
|----------|---------|
|
|
86
|
+
| populate-env.md | Retrieve keys from Keychain and write to .env |
|
|
87
|
+
| add-key.md | Securely add a new API key to Keychain |
|
|
88
|
+
| add-mcp.md | Securely add MCP server to Claude Code / OpenCode |
|
|
89
|
+
| list-keys.md | Show available keys in Keychain |
|
|
90
|
+
| remove-key.md | Delete a key from Keychain |
|
|
91
|
+
</workflows_index>
|
|
92
|
+
|
|
93
|
+
<quick_commands>
|
|
94
|
+
## Quick Reference
|
|
95
|
+
|
|
96
|
+
**Add key (Claude runs this - you just paste):**
|
|
97
|
+
```bash
|
|
98
|
+
read -s -p "Paste API key: " K && security add-generic-password -a "$USER" -s "KEY_NAME" -w "$K" && unset K && pbcopy < /dev/null && echo -e "\n[OK] Stored"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Retrieve key:**
|
|
102
|
+
```bash
|
|
103
|
+
security find-generic-password -a "$USER" -s "KEY_NAME" -w
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Delete key:**
|
|
107
|
+
```bash
|
|
108
|
+
security delete-generic-password -a "$USER" -s "KEY_NAME"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Populate .env from Keychain:**
|
|
112
|
+
```bash
|
|
113
|
+
echo "OPENAI_API_KEY=$(security find-generic-password -a "$USER" -s "OPENAI_API_KEY" -w 2>/dev/null)" >> .env && chmod 600 .env
|
|
114
|
+
```
|
|
115
|
+
</quick_commands>
|
|
116
|
+
|
|
117
|
+
<security_measures>
|
|
118
|
+
## Security Measures
|
|
119
|
+
|
|
120
|
+
This skill implements multiple layers of protection:
|
|
121
|
+
|
|
122
|
+
1. **Silent input** - Keys never appear on screen when you paste
|
|
123
|
+
2. **Memory cleanup** - Variables cleared immediately after use
|
|
124
|
+
3. **Clipboard clearing** - Auto-clears clipboard after storing key
|
|
125
|
+
4. **Encrypted storage** - macOS Keychain uses AES-256 encryption
|
|
126
|
+
5. **File permissions** - .env created with 600 permissions (owner only)
|
|
127
|
+
6. **Git protection** - Automatically adds .env to .gitignore
|
|
128
|
+
7. **No logging** - Keys never written to terminal history
|
|
129
|
+
</security_measures>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# .env File Patterns Reference
|
|
2
|
+
|
|
3
|
+
<overview>
|
|
4
|
+
Safe patterns for working with .env files when using Keychain as the source of truth.
|
|
5
|
+
</overview>
|
|
6
|
+
|
|
7
|
+
<gitignore_protection>
|
|
8
|
+
## Always Protect .env
|
|
9
|
+
|
|
10
|
+
Before creating any .env file, ensure it's in .gitignore:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Check if .gitignore exists and has .env
|
|
14
|
+
if [ -f .gitignore ]; then
|
|
15
|
+
grep -q "^\.env$" .gitignore || echo ".env" >> .gitignore
|
|
16
|
+
else
|
|
17
|
+
echo ".env" > .gitignore
|
|
18
|
+
fi
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Also consider ignoring:
|
|
22
|
+
```
|
|
23
|
+
.env
|
|
24
|
+
.env.local
|
|
25
|
+
.env.*.local
|
|
26
|
+
*.env
|
|
27
|
+
```
|
|
28
|
+
</gitignore_protection>
|
|
29
|
+
|
|
30
|
+
<env_file_format>
|
|
31
|
+
## .env File Format
|
|
32
|
+
|
|
33
|
+
Standard format:
|
|
34
|
+
```bash
|
|
35
|
+
KEY_NAME=value
|
|
36
|
+
ANOTHER_KEY=another_value
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Rules:
|
|
40
|
+
- No spaces around `=`
|
|
41
|
+
- No quotes needed for simple values
|
|
42
|
+
- Use quotes for values with spaces: `KEY="value with spaces"`
|
|
43
|
+
- Comments start with `#`
|
|
44
|
+
- Blank lines are ignored
|
|
45
|
+
</env_file_format>
|
|
46
|
+
|
|
47
|
+
<generation_patterns>
|
|
48
|
+
## Generating .env from Keychain
|
|
49
|
+
|
|
50
|
+
### Simple Generation
|
|
51
|
+
```bash
|
|
52
|
+
> .env # Clear/create file
|
|
53
|
+
echo "OPENAI_API_KEY=$(security find-generic-password -a "$USER" -s "OPENAI_API_KEY" -w)" >> .env
|
|
54
|
+
echo "DATABASE_URL=$(security find-generic-password -a "$USER" -s "DATABASE_URL" -w)" >> .env
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### With Error Handling
|
|
58
|
+
```bash
|
|
59
|
+
add_key_to_env() {
|
|
60
|
+
local key=$1
|
|
61
|
+
local value=$(security find-generic-password -a "$USER" -s "$key" -w 2>/dev/null)
|
|
62
|
+
if [ -n "$value" ]; then
|
|
63
|
+
echo "$key=$value" >> .env
|
|
64
|
+
echo "Added $key"
|
|
65
|
+
else
|
|
66
|
+
echo "# $key not found in Keychain" >> .env
|
|
67
|
+
echo "Warning: $key not found"
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
> .env
|
|
72
|
+
add_key_to_env "OPENAI_API_KEY"
|
|
73
|
+
add_key_to_env "DATABASE_URL"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Reusable Script (populate-env.sh)
|
|
77
|
+
```bash
|
|
78
|
+
#!/bin/bash
|
|
79
|
+
# populate-env.sh - Generate .env from Keychain
|
|
80
|
+
|
|
81
|
+
KEYS=(
|
|
82
|
+
"OPENAI_API_KEY"
|
|
83
|
+
"ANTHROPIC_API_KEY"
|
|
84
|
+
"DATABASE_URL"
|
|
85
|
+
# Add your keys here
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Ensure .gitignore protection
|
|
89
|
+
if [ -f .gitignore ]; then
|
|
90
|
+
grep -q "^\.env$" .gitignore || echo ".env" >> .gitignore
|
|
91
|
+
else
|
|
92
|
+
echo ".env" > .gitignore
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Generate .env
|
|
96
|
+
> .env
|
|
97
|
+
found=0
|
|
98
|
+
missing=0
|
|
99
|
+
|
|
100
|
+
for key in "${KEYS[@]}"; do
|
|
101
|
+
value=$(security find-generic-password -a "$USER" -s "$key" -w 2>/dev/null)
|
|
102
|
+
if [ -n "$value" ]; then
|
|
103
|
+
echo "$key=$value" >> .env
|
|
104
|
+
((found++))
|
|
105
|
+
else
|
|
106
|
+
echo "# $key - NOT FOUND" >> .env
|
|
107
|
+
((missing++))
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
echo "Generated .env: $found keys found, $missing missing"
|
|
112
|
+
```
|
|
113
|
+
</generation_patterns>
|
|
114
|
+
|
|
115
|
+
<project_templates>
|
|
116
|
+
## .env.example Template
|
|
117
|
+
|
|
118
|
+
Create a .env.example that documents required keys without values:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# .env.example - Required environment variables
|
|
122
|
+
# Copy to .env and populate from Keychain: ./populate-env.sh
|
|
123
|
+
|
|
124
|
+
# OpenAI API
|
|
125
|
+
OPENAI_API_KEY=
|
|
126
|
+
|
|
127
|
+
# Anthropic API
|
|
128
|
+
ANTHROPIC_API_KEY=
|
|
129
|
+
|
|
130
|
+
# Database
|
|
131
|
+
DATABASE_URL=
|
|
132
|
+
|
|
133
|
+
# Stripe (if using payments)
|
|
134
|
+
STRIPE_SECRET_KEY=
|
|
135
|
+
STRIPE_PUBLISHABLE_KEY=
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This file IS committed to git (no secrets) and documents what the project needs.
|
|
139
|
+
</project_templates>
|
|
140
|
+
|
|
141
|
+
<verification>
|
|
142
|
+
## Verifying .env
|
|
143
|
+
|
|
144
|
+
### Check Keys Present (values hidden)
|
|
145
|
+
```bash
|
|
146
|
+
cut -d= -f1 .env | grep -v "^#" | grep -v "^$"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Count Keys
|
|
150
|
+
```bash
|
|
151
|
+
grep -c "=" .env
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Check for Empty Values
|
|
155
|
+
```bash
|
|
156
|
+
grep "=$" .env # Lines ending with = have no value
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Validate Format
|
|
160
|
+
```bash
|
|
161
|
+
# Lines should match: KEY=value or be comments/blank
|
|
162
|
+
grep -v "^#" .env | grep -v "^$" | grep -v "^[A-Z_]*=" && echo "Invalid lines found" || echo "Format OK"
|
|
163
|
+
```
|
|
164
|
+
</verification>
|
|
165
|
+
|
|
166
|
+
<framework_loading>
|
|
167
|
+
## Framework-Specific Loading
|
|
168
|
+
|
|
169
|
+
### Node.js (dotenv)
|
|
170
|
+
```javascript
|
|
171
|
+
require('dotenv').config();
|
|
172
|
+
// or in ES modules:
|
|
173
|
+
import 'dotenv/config';
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Python (python-dotenv)
|
|
177
|
+
```python
|
|
178
|
+
from dotenv import load_dotenv
|
|
179
|
+
load_dotenv()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Next.js
|
|
183
|
+
Automatic - .env.local is loaded by default.
|
|
184
|
+
|
|
185
|
+
### Vite
|
|
186
|
+
Automatic - .env files are loaded. Use `VITE_` prefix for client-side vars.
|
|
187
|
+
</framework_loading>
|
|
188
|
+
|
|
189
|
+
<security_reminders>
|
|
190
|
+
## Security Reminders
|
|
191
|
+
|
|
192
|
+
1. **Never commit .env** - Always in .gitignore
|
|
193
|
+
2. **Never log env vars** - Don't `console.log(process.env)`
|
|
194
|
+
3. **Never expose in client code** - Only use server-side
|
|
195
|
+
4. **Regenerate, don't edit** - If you need to change keys, update Keychain and regenerate .env
|
|
196
|
+
5. **.env.example is safe** - Commit it to document required vars (no values)
|
|
197
|
+
6. **Set file permissions** - Always `chmod 600 .env` (owner read/write only)
|
|
198
|
+
7. **Clear clipboard** - After pasting keys, clipboard is auto-cleared to prevent accidents
|
|
199
|
+
</security_reminders>
|
|
200
|
+
|
|
201
|
+
<file_permissions>
|
|
202
|
+
## Securing .env File Permissions
|
|
203
|
+
|
|
204
|
+
After creating .env, always set restrictive permissions:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
chmod 600 .env
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
This means:
|
|
211
|
+
- Owner can read and write (you)
|
|
212
|
+
- Group has no access
|
|
213
|
+
- Others have no access
|
|
214
|
+
|
|
215
|
+
Verify with:
|
|
216
|
+
```bash
|
|
217
|
+
ls -la .env
|
|
218
|
+
# Should show: -rw-------
|
|
219
|
+
```
|
|
220
|
+
</file_permissions>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# macOS Keychain Security Commands Reference
|
|
2
|
+
|
|
3
|
+
<overview>
|
|
4
|
+
macOS provides the `security` CLI tool to interact with Keychain from the terminal. This reference covers all commands needed for API key management.
|
|
5
|
+
</overview>
|
|
6
|
+
|
|
7
|
+
<command_reference>
|
|
8
|
+
## Core Commands
|
|
9
|
+
|
|
10
|
+
### Add a Password
|
|
11
|
+
```bash
|
|
12
|
+
security add-generic-password -a "$USER" -s "SERVICE_NAME" -w "PASSWORD"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Parameters:
|
|
16
|
+
- `-a` : Account name (use `$USER` for current user)
|
|
17
|
+
- `-s` : Service name (the key identifier, e.g., `OPENAI_API_KEY`)
|
|
18
|
+
- `-w` : Password/secret value
|
|
19
|
+
|
|
20
|
+
**Will fail if key already exists.** Delete first to update.
|
|
21
|
+
|
|
22
|
+
### Retrieve a Password
|
|
23
|
+
```bash
|
|
24
|
+
security find-generic-password -a "$USER" -s "SERVICE_NAME" -w
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
- `-a` : Account name
|
|
29
|
+
- `-s` : Service name
|
|
30
|
+
- `-w` : Output only the password (not metadata)
|
|
31
|
+
|
|
32
|
+
Returns just the password value. Exit code 0 on success, non-zero if not found.
|
|
33
|
+
|
|
34
|
+
### Delete a Password
|
|
35
|
+
```bash
|
|
36
|
+
security delete-generic-password -a "$USER" -s "SERVICE_NAME"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Removes the entry from Keychain. Cannot be undone.
|
|
40
|
+
|
|
41
|
+
### Check if Password Exists
|
|
42
|
+
```bash
|
|
43
|
+
security find-generic-password -a "$USER" -s "SERVICE_NAME" -w >/dev/null 2>&1
|
|
44
|
+
echo $? # 0 = exists, non-zero = not found
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### List All Entries
|
|
48
|
+
```bash
|
|
49
|
+
# List all generic passwords (shows metadata, not values)
|
|
50
|
+
security dump-keychain | grep -A 4 'class: "genp"'
|
|
51
|
+
|
|
52
|
+
# Extract just service names
|
|
53
|
+
security dump-keychain | grep -A 4 'class: "genp"' | grep '"svce"' | cut -d'"' -f4 | sort -u
|
|
54
|
+
```
|
|
55
|
+
</command_reference>
|
|
56
|
+
|
|
57
|
+
<secure_patterns>
|
|
58
|
+
## Secure Input Patterns
|
|
59
|
+
|
|
60
|
+
### Silent Input with Clipboard Clear (Recommended)
|
|
61
|
+
```bash
|
|
62
|
+
read -s -p "Paste value: " VALUE && \
|
|
63
|
+
security add-generic-password -a "$USER" -s "KEY_NAME" -w "$VALUE" && \
|
|
64
|
+
unset VALUE && \
|
|
65
|
+
pbcopy < /dev/null && \
|
|
66
|
+
echo -e "\n[OK] Stored + clipboard cleared"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `read -s` : Silent mode (no echo to screen)
|
|
70
|
+
- `unset VALUE` : Clear from shell memory after use
|
|
71
|
+
- `pbcopy < /dev/null` : Clear clipboard (prevents accidental paste elsewhere)
|
|
72
|
+
- Key never appears in terminal, history, or remains in clipboard
|
|
73
|
+
|
|
74
|
+
### Update Existing (Delete + Add)
|
|
75
|
+
```bash
|
|
76
|
+
security delete-generic-password -a "$USER" -s "KEY_NAME" 2>/dev/null; \
|
|
77
|
+
read -s -p "Enter new value: " VALUE && \
|
|
78
|
+
security add-generic-password -a "$USER" -s "KEY_NAME" -w "$VALUE" && \
|
|
79
|
+
unset VALUE
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Batch Check Multiple Keys
|
|
83
|
+
```bash
|
|
84
|
+
for key in OPENAI_API_KEY ANTHROPIC_API_KEY; do
|
|
85
|
+
if security find-generic-password -a "$USER" -s "$key" -w >/dev/null 2>&1; then
|
|
86
|
+
echo "[OK] $key"
|
|
87
|
+
else
|
|
88
|
+
echo "[MISSING] $key"
|
|
89
|
+
fi
|
|
90
|
+
done
|
|
91
|
+
```
|
|
92
|
+
</secure_patterns>
|
|
93
|
+
|
|
94
|
+
<environment_integration>
|
|
95
|
+
## Using with Environment Variables
|
|
96
|
+
|
|
97
|
+
### Export to Shell
|
|
98
|
+
```bash
|
|
99
|
+
export OPENAI_API_KEY=$(security find-generic-password -a "$USER" -s "OPENAI_API_KEY" -w)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### In .zshrc or .bashrc (loads on shell start)
|
|
103
|
+
```bash
|
|
104
|
+
# ~/.zshrc
|
|
105
|
+
export OPENAI_API_KEY=$(security find-generic-password -a "$USER" -s "OPENAI_API_KEY" -w 2>/dev/null)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Generate .env File
|
|
109
|
+
```bash
|
|
110
|
+
echo "OPENAI_API_KEY=$(security find-generic-password -a "$USER" -s "OPENAI_API_KEY" -w)" >> .env
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Use in Scripts (Python)
|
|
114
|
+
```python
|
|
115
|
+
import subprocess
|
|
116
|
+
import os
|
|
117
|
+
|
|
118
|
+
def get_keychain_secret(key_name):
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
["security", "find-generic-password", "-a", os.environ["USER"], "-s", key_name, "-w"],
|
|
121
|
+
capture_output=True, text=True
|
|
122
|
+
)
|
|
123
|
+
return result.stdout.strip() if result.returncode == 0 else None
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Use in Scripts (Node.js)
|
|
127
|
+
```javascript
|
|
128
|
+
const { execSync } = require('child_process');
|
|
129
|
+
|
|
130
|
+
function getKeychainSecret(keyName) {
|
|
131
|
+
try {
|
|
132
|
+
return execSync(`security find-generic-password -a "$USER" -s "${keyName}" -w`)
|
|
133
|
+
.toString().trim();
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
</environment_integration>
|
|
140
|
+
|
|
141
|
+
<troubleshooting>
|
|
142
|
+
## Common Issues
|
|
143
|
+
|
|
144
|
+
### "security: SecKeychainSearchCopyNext: The specified item could not be found"
|
|
145
|
+
Key doesn't exist. Check the exact name with `security dump-keychain`.
|
|
146
|
+
|
|
147
|
+
### "already has an entry"
|
|
148
|
+
Key already exists. Delete first, then add:
|
|
149
|
+
```bash
|
|
150
|
+
security delete-generic-password -a "$USER" -s "KEY_NAME"
|
|
151
|
+
security add-generic-password -a "$USER" -s "KEY_NAME" -w "value"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### "User interaction is not allowed"
|
|
155
|
+
Running in a context without Keychain access (e.g., some CI environments). Not applicable for local development.
|
|
156
|
+
|
|
157
|
+
### Password Prompt on Access
|
|
158
|
+
macOS may prompt for your login password the first time a new application accesses a Keychain item. Click "Always Allow" to prevent future prompts.
|
|
159
|
+
</troubleshooting>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Workflow: Add Key to Keychain Securely
|
|
2
|
+
|
|
3
|
+
<required_reading>
|
|
4
|
+
**Read these reference files NOW:**
|
|
5
|
+
1. references/keychain-commands.md
|
|
6
|
+
</required_reading>
|
|
7
|
+
|
|
8
|
+
<process>
|
|
9
|
+
## Step 1: Get Key Name
|
|
10
|
+
|
|
11
|
+
Ask the user what key they want to add. Suggest common options:
|
|
12
|
+
- `OPENAI_API_KEY`
|
|
13
|
+
- `ANTHROPIC_API_KEY`
|
|
14
|
+
- `STRIPE_SECRET_KEY`
|
|
15
|
+
- `DATABASE_URL`
|
|
16
|
+
- Or let them specify a custom name (SCREAMING_SNAKE_CASE)
|
|
17
|
+
|
|
18
|
+
## Step 2: Check if Key Already Exists and Handle It
|
|
19
|
+
|
|
20
|
+
Run this check:
|
|
21
|
+
```bash
|
|
22
|
+
security find-generic-password -a "$USER" -s "KEY_NAME" -w >/dev/null 2>&1 && echo "EXISTS" || echo "NEW"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If it exists, ask: "This key already exists. Replace it with a new value?"
|
|
26
|
+
- If yes, delete the old one first: `security delete-generic-password -a "$USER" -s "KEY_NAME" 2>/dev/null`
|
|
27
|
+
- If no, cancel and offer to add a different key
|
|
28
|
+
|
|
29
|
+
## Step 3: Open Terminal Window for Secure Input
|
|
30
|
+
|
|
31
|
+
**Claude opens a new Terminal window where the user pastes their key.**
|
|
32
|
+
|
|
33
|
+
Use this AppleScript command to open Terminal with the secure input prompt:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
osascript -e 'tell application "Terminal"
|
|
37
|
+
activate
|
|
38
|
+
do script "clear && echo \"\" && echo \"====================================\" && echo \" SECURE API KEY INPUT\" && echo \"====================================\" && echo \"\" && echo \"Paste your key below (it will be invisible)\" && echo \"Then press ENTER\" && echo \"\" && echo -n \"KEY_NAME: \" && read -s K && security add-generic-password -a \"$USER\" -s \"KEY_NAME\" -w \"$K\" && unset K && pbcopy < /dev/null && echo \"\" && echo \"\" && echo \"[SUCCESS] KEY_NAME stored in Keychain\" && echo \"[SUCCESS] Clipboard cleared\" && echo \"\" && echo \"You can close this window now.\" && echo \"\""
|
|
39
|
+
end tell'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**What happens:**
|
|
43
|
+
1. A new Terminal window opens and comes to front
|
|
44
|
+
2. User sees clear instructions
|
|
45
|
+
3. User pastes (Cmd+V) - nothing appears on screen
|
|
46
|
+
4. User presses Enter
|
|
47
|
+
5. Key is stored in Keychain (encrypted)
|
|
48
|
+
6. Clipboard is cleared
|
|
49
|
+
7. Success message shown
|
|
50
|
+
8. User closes the window
|
|
51
|
+
|
|
52
|
+
**After running:** Wait for user to confirm they completed it, then verify storage.
|
|
53
|
+
|
|
54
|
+
## Step 4: Verify Storage
|
|
55
|
+
|
|
56
|
+
Immediately verify the key was stored:
|
|
57
|
+
```bash
|
|
58
|
+
security find-generic-password -a "$USER" -s "KEY_NAME" -w >/dev/null 2>&1 && \
|
|
59
|
+
echo "[VERIFIED] KEY_NAME is now stored in Keychain" || \
|
|
60
|
+
echo "[ERROR] KEY_NAME was not stored - please try again"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Step 5: Offer Next Steps
|
|
64
|
+
|
|
65
|
+
Ask the user:
|
|
66
|
+
1. **Add another key** - Repeat this workflow
|
|
67
|
+
2. **Populate .env** - Switch to populate-env.md workflow
|
|
68
|
+
3. **Done** - Exit skill
|
|
69
|
+
</process>
|
|
70
|
+
|
|
71
|
+
<terminal_commands>
|
|
72
|
+
## Terminal Window Commands (Claude runs these via osascript)
|
|
73
|
+
|
|
74
|
+
**Template - replace KEY_NAME:**
|
|
75
|
+
```bash
|
|
76
|
+
osascript -e 'tell application "Terminal"
|
|
77
|
+
activate
|
|
78
|
+
do script "clear && echo \"\" && echo \"====================================\" && echo \" SECURE API KEY INPUT\" && echo \"====================================\" && echo \"\" && echo \"Paste your key below (it will be invisible)\" && echo \"Then press ENTER\" && echo \"\" && echo -n \"KEY_NAME: \" && read -s K && security add-generic-password -a \"$USER\" -s \"KEY_NAME\" -w \"$K\" && unset K && pbcopy < /dev/null && echo \"\" && echo \"\" && echo \"[SUCCESS] KEY_NAME stored in Keychain\" && echo \"[SUCCESS] Clipboard cleared\" && echo \"\" && echo \"You can close this window now.\""
|
|
79
|
+
end tell'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**For updating existing key (delete first):**
|
|
83
|
+
```bash
|
|
84
|
+
osascript -e 'tell application "Terminal"
|
|
85
|
+
activate
|
|
86
|
+
do script "clear && echo \"\" && echo \"====================================\" && echo \" UPDATE API KEY\" && echo \"====================================\" && echo \"\" && security delete-generic-password -a \"$USER\" -s \"KEY_NAME\" 2>/dev/null && echo \"Paste your NEW key below (invisible)\" && echo \"Then press ENTER\" && echo \"\" && echo -n \"KEY_NAME: \" && read -s K && security add-generic-password -a \"$USER\" -s \"KEY_NAME\" -w \"$K\" && unset K && pbcopy < /dev/null && echo \"\" && echo \"\" && echo \"[SUCCESS] KEY_NAME updated in Keychain\" && echo \"[SUCCESS] Clipboard cleared\" && echo \"\" && echo \"You can close this window now.\""
|
|
87
|
+
end tell'
|
|
88
|
+
```
|
|
89
|
+
</terminal_commands>
|
|
90
|
+
|
|
91
|
+
<success_criteria>
|
|
92
|
+
This workflow is complete when:
|
|
93
|
+
- [ ] Key name confirmed with user
|
|
94
|
+
- [ ] Existing key check performed
|
|
95
|
+
- [ ] User provided with secure input command
|
|
96
|
+
- [ ] User confirmed they ran the command
|
|
97
|
+
- [ ] Key storage verified
|
|
98
|
+
- [ ] User offered next steps
|
|
99
|
+
</success_criteria>
|