From 131d0bcb5dfa3892ff8f232ef89e171ae64c9776 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Sun, 21 Jun 2026 11:44:47 +0200 Subject: [PATCH] feat(secrets): .env source-of-truth in ~/.claude + repo symlink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 6 ++++-- install-plugins.sh | 2 +- lib/design-gate.md | 4 ++-- lib/design-tool-gate.sh | 2 +- lib/toggle-external.sh | 2 +- link.sh | 26 ++++++++++++++++++++++++++ 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 5d71cef..0d78a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -98,9 +98,11 @@ HANDOVER.pdf # Install logs 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.local +.env.* +!.env.example # OS .DS_Store diff --git a/install-plugins.sh b/install-plugins.sh index e5f3161..6e8c572 100644 --- a/install-plugins.sh +++ b/install-plugins.sh @@ -618,7 +618,7 @@ if [ -x "$REPO/lib/toggle-external.sh" ]; then ok "magic MCP disabled (default)" fi 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 else warn "lib/toggle-external.sh not found or not executable — skipping" diff --git a/lib/design-gate.md b/lib/design-gate.md index 915bcc0..85e43d1 100644 --- a/lib/design-gate.md +++ b/lib/design-gate.md @@ -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. **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, - 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. - **`unverified` line** (claude CLI absent) → the state of a plugin/mcp couldn't be checked; it does not block. Mention it, proceed. @@ -92,7 +92,7 @@ remedy is always `/profile ` — a profile, never a lone tool. - 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. - 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 `# GATE-BLOCK:` line(s) — edit there to add/remove a blocking design tool, not in the script. diff --git a/lib/design-tool-gate.sh b/lib/design-tool-gate.sh index e9732d7..e31f011 100755 --- a/lib/design-tool-gate.sh +++ b/lib/design-tool-gate.sh @@ -131,7 +131,7 @@ fi if [ "${#manual[@]}" -gt 0 ]; then echo " required + manual step (API key / external install): ${manual[*]}" 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 fi if [ "${#unverified[@]}" -gt 0 ]; then diff --git a/lib/toggle-external.sh b/lib/toggle-external.sh index 29a5f6a..44e5023 100755 --- a/lib/toggle-external.sh +++ b/lib/toggle-external.sh @@ -181,7 +181,7 @@ enable_tool() { magic) load_env 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 fi if [ "$(status_tool magic)" = "enabled" ]; then diff --git a/link.sh b/link.sh index 882d362..0e9e035 100644 --- a/link.sh +++ b/link.sh @@ -106,6 +106,32 @@ for _ext in "${NPX_EXTERNAL_SKILLS[@]}"; do CHANGED=$((CHANGED + 1)) 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 echo "✅ All symlinks already up to date." else