feat(secrets): .env source-of-truth in ~/.claude + repo symlink
Move the real secret out of the git tree: the key lives in ~/.claude/.env (outside the repo), and link.sh symlinks repo/.env -> ~/.claude/.env so `source "$REPO/.env"` resolves transparently. The secret never enters git — not as content (it's a link) and not by accident (gitignored). link.sh: add link_env() — verify ~/.claude/.env exists + has MAGIC_API_KEY (warn, never create/copy the secret), then create repo/.env -> ~/.claude/.env. Defensive + idempotent: links only when repo/.env is absent or already the right symlink; a residual REAL repo/.env is left untouched with a migrate hint (never clobbered, so the secret can't be destroyed). .gitignore: harden .env -> .env + .env.* + !.env.example (covers .env.local, .env.bak, .env.save; keeps the template tracked). Messages point at ~/.claude/.env (the canonical edit location) instead of the ambiguous $REPO/.env: design-tool-gate.sh gate output, design-gate.md (branch 3 + IMPORTANT), toggle-external.sh, install-plugins.sh. Verified: shellcheck clean (link.sh, toggle-external.sh, design-tool-gate.sh); link.sh created the symlink (1 change, idempotent re-run); repo/.env absent from git status; magic-off path still exits 10 with the ~/.claude/.env hint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2e6725e8bb
commit
131d0bcb5d
6
.gitignore
vendored
6
.gitignore
vendored
@ -98,9 +98,11 @@ HANDOVER.pdf
|
|||||||
# Install logs
|
# Install logs
|
||||||
install-*.log
|
install-*.log
|
||||||
|
|
||||||
# Local secrets (MCP API keys etc.) — use .env.example as template
|
# Local secrets (MCP API keys etc.) — real key lives in ~/.claude/.env;
|
||||||
|
# repo/.env is a symlink to it (created by link.sh). Never commit the secret.
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@ -618,7 +618,7 @@ if [ -x "$REPO/lib/toggle-external.sh" ]; then
|
|||||||
ok "magic MCP disabled (default)"
|
ok "magic MCP disabled (default)"
|
||||||
fi
|
fi
|
||||||
if [ ! -f "$REPO/.env" ] || ! grep -q '^MAGIC_API_KEY=' "$REPO/.env" 2>/dev/null; then
|
if [ ! -f "$REPO/.env" ] || ! grep -q '^MAGIC_API_KEY=' "$REPO/.env" 2>/dev/null; then
|
||||||
warn "MAGIC_API_KEY not found in $REPO/.env — copy .env.example and set your key before enabling"
|
warn "MAGIC_API_KEY not found in ~/.claude/.env — copy .env.example there and set your key before enabling"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
warn "lib/toggle-external.sh not found or not executable — skipping"
|
warn "lib/toggle-external.sh not found or not executable — skipping"
|
||||||
|
|||||||
@ -75,7 +75,7 @@ Exit codes: `0` = ready (proceed) · `10` = incomplete (gate trips) · `2` = err
|
|||||||
- **required + manual step** → required tools the profile can't flip silently.
|
- **required + manual step** → required tools the profile can't flip silently.
|
||||||
**magic lands here: it TRIPS the gate** (it's required for Build), it is NOT
|
**magic lands here: it TRIPS the gate** (it's required for Build), it is NOT
|
||||||
a silent "optional". `/profile design` runs `toggle-external.sh` for magic,
|
a silent "optional". `/profile design` runs `toggle-external.sh` for magic,
|
||||||
which needs a valid `MAGIC_API_KEY` in `.env` — tell the user to verify it.
|
which needs a valid `MAGIC_API_KEY` in `~/.claude/.env` — tell the user to verify it.
|
||||||
- Do NOT hand-activate individual tools. The profile is the unit of activation.
|
- Do NOT hand-activate individual tools. The profile is the unit of activation.
|
||||||
- **`unverified` line** (claude CLI absent) → the state of a plugin/mcp couldn't
|
- **`unverified` line** (claude CLI absent) → the state of a plugin/mcp couldn't
|
||||||
be checked; it does not block. Mention it, proceed.
|
be checked; it does not block. Mention it, proceed.
|
||||||
@ -92,7 +92,7 @@ remedy is always `/profile <that>` — a profile, never a lone tool.
|
|||||||
- Remedy is ALWAYS a profile (`/profile design`), never an atomic tool toggle —
|
- Remedy is ALWAYS a profile (`/profile design`), never an atomic tool toggle —
|
||||||
the profile system is the single source of truth for what's active.
|
the profile system is the single source of truth for what's active.
|
||||||
- magic is REQUIRED (it trips the gate), but `/profile design` only enables it
|
- magic is REQUIRED (it trips the gate), but `/profile design` only enables it
|
||||||
if `MAGIC_API_KEY` is in `.env` — the gate says so; surface that to the user.
|
if `MAGIC_API_KEY` is in `~/.claude/.env` — the gate says so; surface that to the user.
|
||||||
- The design-core set (what trips the gate) is declared in `design.profile` on
|
- The design-core set (what trips the gate) is declared in `design.profile` on
|
||||||
the `# GATE-BLOCK:` line(s) — edit there to add/remove a blocking design tool,
|
the `# GATE-BLOCK:` line(s) — edit there to add/remove a blocking design tool,
|
||||||
not in the script.
|
not in the script.
|
||||||
|
|||||||
@ -131,7 +131,7 @@ fi
|
|||||||
if [ "${#manual[@]}" -gt 0 ]; then
|
if [ "${#manual[@]}" -gt 0 ]; then
|
||||||
echo " required + manual step (API key / external install): ${manual[*]}"
|
echo " required + manual step (API key / external install): ${manual[*]}"
|
||||||
case " ${manual[*]} " in
|
case " ${manual[*]} " in
|
||||||
*" magic "*) echo " magic needs MAGIC_API_KEY in .env (/profile $PROFILE runs toggle-external.sh)" ;;
|
*" magic "*) echo " magic needs MAGIC_API_KEY in ~/.claude/.env (/profile $PROFILE runs toggle-external.sh)" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
if [ "${#unverified[@]}" -gt 0 ]; then
|
if [ "${#unverified[@]}" -gt 0 ]; then
|
||||||
|
|||||||
@ -181,7 +181,7 @@ enable_tool() {
|
|||||||
magic)
|
magic)
|
||||||
load_env
|
load_env
|
||||||
if [ -z "${MAGIC_API_KEY:-}" ]; then
|
if [ -z "${MAGIC_API_KEY:-}" ]; then
|
||||||
err "MAGIC_API_KEY not set — add it to $REPO/.env (template: .env.example)"
|
err "MAGIC_API_KEY not set — add it to ~/.claude/.env (template: .env.example)"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if [ "$(status_tool magic)" = "enabled" ]; then
|
if [ "$(status_tool magic)" = "enabled" ]; then
|
||||||
|
|||||||
26
link.sh
26
link.sh
@ -106,6 +106,32 @@ for _ext in "${NPX_EXTERNAL_SKILLS[@]}"; do
|
|||||||
CHANGED=$((CHANGED + 1))
|
CHANGED=$((CHANGED + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# ── Local secrets: repo/.env -> ~/.claude/.env ──────────────
|
||||||
|
# Real key lives in ~/.claude/.env (source of truth, outside the repo so the
|
||||||
|
# secret never enters the git tree). The repo reaches it via a symlink that
|
||||||
|
# `source "$REPO/.env"` follows transparently. Never creates/copies/prints it.
|
||||||
|
link_env() {
|
||||||
|
local home_env="$CLAUDE/.env" repo_env="$REPO/.env"
|
||||||
|
if [ ! -f "$home_env" ]; then
|
||||||
|
echo "⚠️ $home_env missing — create it (the repo never stores the secret):"
|
||||||
|
echo " cp \"$REPO/.env.example\" \"$home_env\" && \"\${EDITOR:-nano}\" \"$home_env\""
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
grep -q '^MAGIC_API_KEY=' "$home_env" 2>/dev/null \
|
||||||
|
|| echo "⚠️ $home_env has no MAGIC_API_KEY line — magic won't enable until added."
|
||||||
|
if [ -L "$repo_env" ]; then
|
||||||
|
[ "$(readlink "$repo_env")" = "$home_env" ] && return
|
||||||
|
ln -sf "$home_env" "$repo_env"; CHANGED=$((CHANGED + 1))
|
||||||
|
elif [ ! -e "$repo_env" ]; then
|
||||||
|
ln -sf "$home_env" "$repo_env"; CHANGED=$((CHANGED + 1))
|
||||||
|
else
|
||||||
|
echo "⚠️ $repo_env is a real file, not a symlink."
|
||||||
|
echo " If it holds your secret: mv \"$repo_env\" \"$home_env\" then re-run link.sh"
|
||||||
|
echo " Otherwise remove it so link.sh can link to $home_env."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
link_env
|
||||||
|
|
||||||
if [ "$CHANGED" -eq 0 ]; then
|
if [ "$CHANGED" -eq 0 ]; then
|
||||||
echo "✅ All symlinks already up to date."
|
echo "✅ All symlinks already up to date."
|
||||||
else
|
else
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user