Extracts hardcoded secrets from CLAUDE.md, .mcp.json, and project config into gitignored .env file, wires SessionStart hook for auto-loading. Use for 'separate secrets' or 'extract API keys'.
From toolboxnpx claudepluginhub leejuoh/claude-code-zero --plugin toolboxThis skill is limited to using the following tools:
assets/load-secrets.sh.templatereferences/detection-patterns.mdDesigns and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Implements structured self-debugging workflow for AI agent failures: capture errors, diagnose patterns like loops or context overflow, apply contained recoveries, and generate introspection reports.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Extract hardcoded secrets from CLAUDE.md, .mcp.json, and project config into a gitignored env file, then wire up a SessionStart hook to load them automatically via CLAUDE_ENV_FILE.
Secrets in CLAUDE.md and .mcp.json get committed to git, shared with collaborators, and cached in Claude's context. This skill moves them to a gitignored file and loads them through a hook or shell profile.
If the user passes scan-only, stop after Phase 1 (report findings without modifying anything).
Read references/detection-patterns.md for the full list of regex patterns and scan targets.
Run Grep with each pattern against the scan targets. Present findings in a table:
| # | File | Line | Type | Value (masked) |
|---|------|------|------|----------------|
| 1 | CLAUDE.md:42 | API Key | sk-...abc1 |
| 2 | CLAUDE.md:55 | DB URL | postgres://...@host/db |
Ask the user to confirm which items are actual secrets to extract. Some may be intentional examples or documentation — do not force extraction.
For each confirmed secret, propose an environment variable name:
DATABASE_URL in code), reuse that name.Present the mapping and ask for confirmation. Mark the source so MCP secrets get handled differently in Phase 4:
| # | Source | Current value (masked) | Proposed env var |
|---|--------|----------------------|-----------------|
| 1 | CLAUDE.md | sk-...abc1 | OPENAI_API_KEY |
| 2 | CLAUDE.md | postgres://...@host/db | DATABASE_URL |
| 3 | .mcp.json | xoxb-...token | SLACK_TOKEN |
The user may rename variables or skip items. Wait for approval before proceeding.
Each step checks for existing infrastructure and merges rather than overwrites.
Check if the project already has a gitignored env file:
for f in .env.local .env.secret .env .env.development.local; do
git check-ignore "$f" 2>/dev/null && echo "FOUND: $f"
done
.env.local).git check-ignore -q "<chosen-file>" 2>/dev/null
echo $? # 0 = ignored, 1 = NOT ignored
If not ignored, propose adding the filename to .gitignore. Show the exact line and ask for confirmation.
Format:
# Claude Code secrets — loaded via SessionStart hook
# DO NOT commit this file to git
OPENAI_API_KEY=<paste-your-key-here>
DATABASE_URL=<paste-your-connection-string-here>
Tell the user to fill in actual values. Do NOT write real secret values — always use placeholders.
Read the template at assets/load-secrets.sh.template. Copy it to .claude/hooks/load-secrets.sh, replacing {{ENV_FILE_PATH}} with the actual relative path from Step 3.1.
Make it executable:
chmod +x .claude/hooks/load-secrets.sh
Read .claude/settings.local.json (create if missing). Merge the SessionStart hook entry:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/load-secrets.sh"
}
]
}
]
}
}
Merging rules:
hooks key → add it.SessionStart → add the array.SessionStart → append the new entry.Add deny rules to .claude/settings.local.json to prevent Claude from reading the env file directly. Adjust paths to match the chosen env file (example uses .env.local):
{
"permissions": {
"deny": [
"Read(.env.local)",
"Bash(cat .env.local*)",
"Bash(head .env.local*)",
"Bash(tail .env.local*)",
"Bash(less .env.local*)",
"Bash(more .env.local*)"
]
}
}
Merge with existing deny rules — do not remove existing entries.
Replace each hardcoded value with the $ENV_VAR_NAME reference.
Before:
API_KEY: sk-1234567890abcdef
After:
API_KEY: $OPENAI_API_KEY (loaded via SessionStart hook)
Add a brief note near the top of CLAUDE.md:
## Secrets
Environment variables are loaded automatically via SessionStart hook.
See `.claude/hooks/load-secrets.sh` for the loading mechanism.
Do not hardcode secrets in this file — use `$VAR_NAME` references.
MCP servers are spawned as separate processes when Claude Code starts — before SessionStart hooks run. This means MCP servers cannot receive env vars set via the SessionStart hook. They only inherit env vars already present in the parent shell environment.
Present the user with two options:
Option A (recommended for shared repos): Remove the hardcoded value from the env field. The user sets the env var in their shell profile (~/.zshrc or ~/.bashrc) instead. The MCP server inherits it from the parent process.
Before:
{
"env": {
"SLACK_TOKEN": "xoxb-actual-token"
}
}
After:
{
"env": {}
}
User adds to ~/.zshrc:
export SLACK_TOKEN="xoxb-actual-token"
Option B (simpler): Gitignore .mcp.json entirely. Secrets stay in the file but are not committed. Downside: MCP server configuration is no longer shared with collaborators.
Ask the user which option they prefer. If Option A, also add the env var to the .env.local file (as a reference, even though it won't be loaded via hook for MCP). Tell the user they must restart their shell and Claude Code for changes to take effect.
Test the hook script:
MOCK_ENV=$(mktemp)
CLAUDE_ENV_FILE="$MOCK_ENV" CLAUDE_PROJECT_DIR="$(pwd)" .claude/hooks/load-secrets.sh
echo "=== Loaded variables ==="
cat "$MOCK_ENV"
rm "$MOCK_ENV"
Expected output: one export VAR="value" line per secret (or placeholder).
After verification, inform the user:
/hooks to confirm the SessionStart hook appears..env.local with other variables.export lines to it, and Claude Code sources that file before each Bash command in the session.<paste-your-key-here> style placeholders so the user fills them in manually.command\r: not found errors.export KEY="value") to handle spaces and special characters. If a value itself contains double quotes, the user must escape them in the env file (KEY=value with \"quotes\").~/.zshrc) or the .mcp.json must be gitignored.env field in .mcp.json is empty or missing a key, the MCP server inherits that env var from the parent shell. This is standard Unix process behavior — removing a key from env does NOT block inheritance.