From skill-issues
Securely store and manage secrets (API tokens, PATs, passwords, credentials) in macOS Keychain for local development tools — MCP servers, CLI tools, wrapper scripts, .env files, and more. Use this skill whenever the user asks to "secure a token", "store a secret", "set up keychain", "remove plaintext credentials", "secure my .env", or mentions wanting to avoid secrets in plaintext on their Mac. Also trigger when discussing MCP server configuration that involves tokens or credentials on macOS, or when the user asks how to safely manage API keys locally. Trigger PROACTIVELY when you detect a secret being passed as plaintext in a command, env var, or config file — don't wait for the user to ask.
How this skill is triggered — by the user, by Claude, or both
Slash command
/skill-issues:mac-secretsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Migrate secrets from plaintext files into macOS Keychain and wire up tooling to read them at runtime. This is a rigid process skill — follow the steps in order.
Migrate secrets from plaintext files into macOS Keychain and wire up tooling to read them at runtime. This is a rigid process skill — follow the steps in order.
macOS Keychain encrypts secrets with the user's login session key. Secrets are only accessible when the session is unlocked and never sit in plaintext on disk. This is the native macOS equivalent of tools like pass, 1password-cli, or vault — no extra dependencies required.
The two key commands:
security add-generic-password -s '<service>' -a '<account>' -w '<secret>' -Usecurity find-generic-password -s '<service>' -a '<account>' -wThe -U flag updates the entry if it already exists (upsert behavior), though on some macOS versions it may fail — see Step 2 for workaround.
Each entry has three fields — think of them as a filing system:
| Field | Purpose | Analogy |
|---|---|---|
-s (service) | What the secret is — a label describing the credential type | The folder label |
-a (account) | Who uses it — which tool or scope needs this secret | The drawer it goes in |
-w (password) | The actual secret value — this is the only sensitive part | The document inside |
-s and -a are just names you choose for finding the secret later. The secret itself goes in -w.
| Scope | -s (service) | -a (account) | Example |
|---|---|---|---|
| Cross-project (used by Claude Code itself, glab, gh, etc.) | <provider>-<type> | claude-code | -s 'gitlab-pat' -a 'claude-code' |
| Project-specific (only used by one project's MCP/tool) | <provider>-<type> | <project-dir-name> | -s 'confluence-pat' -a 'wiki-lda-mcp' |
Use claude-code as account for tokens that work across repos (GitLab PAT, GitHub token, OpenAI key). Use the project directory name for tokens scoped to a single project.
Determine (ask the user or infer from context):
| Field | What it is | Example |
|---|---|---|
| Secret value | The actual token/password | glpat-abc123... |
| Service name | Descriptive label for Keychain (kebab-case) | gitlab-pat, confluence-pat, openai-api-key |
| Account name | claude-code if cross-project, project dir name if scoped | claude-code, wiki-lda-mcp |
IMPORTANT: Never run the secret as a CLI argument in Claude Code's shell — it would appear in the conversation. Instead, tell the user to run the store command in a separate terminal.
Give the user this command to run in another terminal:
security add-generic-password -s '<service>' -a '<account>' -w
The -w flag without a value prompts interactively for the password — the secret never appears in shell history or on screen.
If -U (upsert) fails with "item already exists", tell the user to delete first:
security delete-generic-password -s '<service>' -a '<account>'
security add-generic-password -s '<service>' -a '<account>' -w
Do NOT use read -s -p — it does not work in zsh (-p means coprocess, not prompt).
Once the user confirms, verify from Claude Code's shell:
security find-generic-password -s '<service>' -a '<account>' -w | head -c 10 && echo "...(ok)"
Only needed when a tool requires the secret as an environment variable at startup (e.g., MCP servers). Skip this step for secrets read on demand (e.g., curl API calls with $(security ...)).
Template — save as scripts/run-<tool>.sh:
#!/usr/bin/env bash
set -euo pipefail
export <ENV_VAR_NAME>
<ENV_VAR_NAME>="$(security find-generic-password -s '<service>' -a '<account>' -w)"
exec <original-command> "$@"
Key details:
set -euo pipefail — fail fast on any errorexport and assignment on separate lines — if they're combined (export VAR=$(cmd)), a failing command won't trigger set -eexec replaces the shell process with the actual command — no leftover wrapper process"$@" passes through any additional argumentsMake the script executable:
chmod +x scripts/run-<tool>.sh
Update the relevant configuration files to use the wrapper script instead of the original command.
For MCP servers (.mcp.json):
Before:
{
"mcpServers": {
"my-server": {
"command": "uv",
"args": ["run", "--directory", ".", "my-server"]
}
}
}
After:
{
"mcpServers": {
"my-server": {
"command": "bash",
"args": ["scripts/run-my-server.sh"]
}
}
}
For on-demand API calls (no wrapper needed):
curl -H "PRIVATE-TOKEN: $(security find-generic-password -s 'gitlab-pat' -a 'claude-code' -w)" \
https://gitlab.example.com/api/v4/...
Remove the secret from any plaintext file where it previously lived:
.env files — comment out the line and add a pointer to the Keychain entry:
# Stored in macOS Keychain (service: <service>, account: <account>)
# <ENV_VAR_NAME>=
Never delete the entire variable reference — the comment serves as documentation for other developers or future-you about where the secret lives.
.gitignoreCheck that these files are in .gitignore:
.env (should already be there — verify).mcp.json (contains local paths and tool config)Add missing entries. Don't duplicate existing ones.
Run the wrapper script or the tool that consumes the secret and confirm it works. For MCP servers, a quick smoke test:
# Inline Python test — adapt imports to the project
from dotenv import load_dotenv
load_dotenv()
# ... initialize client, make a simple API call, confirm success
Or just launch the tool and check it connects successfully.
Report the final state to the user:
| Component | Status |
|---|---|
| Keychain entry | <service> / <account> |
| Wrapper script | scripts/run-<tool>.sh (or N/A if on-demand) |
| Config updated | <which file> |
| Plaintext removed | <which file> |
.gitignore | Updated |
| Verification | Pass/Fail |
security find-generic-password -a '<account>' 2>&1 | grep "svce"
security find-generic-password -a 'claude-code' 2>&1 | grep "svce"
Delete and re-add (the -U flag is unreliable on some macOS versions):
security delete-generic-password -s '<service>' -a '<account>'
security add-generic-password -s '<service>' -a '<account>' -w
security delete-generic-password -s '<service>' -a '<account>'
When a project needs several secrets (e.g., an API token and a database password), create one Keychain entry per secret and extend the wrapper script:
#!/usr/bin/env bash
set -euo pipefail
export API_TOKEN DB_PASSWORD
API_TOKEN="$(security find-generic-password -s 'myapp-api-token' -a 'my-project' -w)"
DB_PASSWORD="$(security find-generic-password -s 'myapp-db-password' -a 'my-project' -w)"
exec my-command "$@"
security(1) which is a macOS-specific tool. For Linux, suggest secret-tool (libsecret) or pass. For cross-platform, suggest 1password-cli or environment-specific vaults.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub vasallo94/skill-issue --plugin skill-issues