# Mac Secrets

> 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.

- Skill: `vasallo94/mac-secrets` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vasallo94/mac-secrets`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vasallo94/mac-secrets/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: Vasallo94 (https://skillmd.com/u/vasallo94)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vasallo94/mac-secrets

---


# mac-secrets

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.

## Why macOS Keychain

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:
- **Store**: `security add-generic-password -s '<service>' -a '<account>' -w '<secret>' -U`
- **Read**: `security find-generic-password -s '<service>' -a '<account>' -w`

The `-U` flag updates the entry if it already exists (upsert behavior), though on some macOS versions it may fail — see Step 2 for workaround.

## How Keychain entries are organized

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`.

## Naming convention

| 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.

## Process

### Step 1: Identify the secret

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` |

### Step 2: Store in Keychain

**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:

```bash
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:

```bash
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:

```bash
security find-generic-password -s '<service>' -a '<account>' -w | head -c 10 && echo "...(ok)"
```

### Step 3: Create a wrapper script (if needed)

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`:

```bash
#!/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 error
- Declare `export` and assignment on separate lines — if they're combined (`export VAR=$(cmd)`), a failing command won't trigger `set -e`
- `exec` replaces the shell process with the actual command — no leftover wrapper process
- `"$@"` passes through any additional arguments

Make the script executable:

```bash
chmod +x scripts/run-<tool>.sh
```

### Step 4: Update configuration

Update the relevant configuration files to use the wrapper script instead of the original command.

**For MCP servers (`.mcp.json`):**

Before:
```json
{
  "mcpServers": {
    "my-server": {
      "command": "uv",
      "args": ["run", "--directory", ".", "my-server"]
    }
  }
}
```

After:
```json
{
  "mcpServers": {
    "my-server": {
      "command": "bash",
      "args": ["scripts/run-my-server.sh"]
    }
  }
}
```

**For on-demand API calls** (no wrapper needed):

```bash
curl -H "PRIVATE-TOKEN: $(security find-generic-password -s 'gitlab-pat' -a 'claude-code' -w)" \
  https://gitlab.example.com/api/v4/...
```

### Step 5: Remove plaintext secrets

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>=
  ```
- **Config files** — same pattern: remove the value, leave a comment

Never delete the entire variable reference — the comment serves as documentation for other developers or future-you about where the secret lives.

### Step 6: Secure `.gitignore`

Check that these files are in `.gitignore`:

- `.env` (should already be there — verify)
- `.mcp.json` (contains local paths and tool config)
- Any other file that held the secret

Add missing entries. Don't duplicate existing ones.

### Step 7: Verify end-to-end

Run the wrapper script or the tool that consumes the secret and confirm it works. For MCP servers, a quick smoke test:

```python
# 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 |

## Managing existing secrets

### List Keychain entries for a project

```bash
security find-generic-password -a '<account>' 2>&1 | grep "svce"
```

### List all Claude Code secrets

```bash
security find-generic-password -a 'claude-code' 2>&1 | grep "svce"
```

### Update a secret

Delete and re-add (the `-U` flag is unreliable on some macOS versions):

```bash
security delete-generic-password -s '<service>' -a '<account>'
security add-generic-password -s '<service>' -a '<account>' -w
```

### Delete a secret

```bash
security delete-generic-password -s '<service>' -a '<account>'
```

## Multiple secrets in one project

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:

```bash
#!/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 "$@"
```

## Constraints

- **macOS only** — this skill uses `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.
- **Local development only** — CI/CD and production environments should use their platform's native secret management (GitHub Secrets, GitLab CI variables, AWS Secrets Manager, etc.).
- **One secret per Keychain entry** — don't try to store JSON blobs or multiple values in a single entry.

