Share

外观
风格

不用 1Password 也能安全注入环境变量:Mac Keychain 工作流

2026年2月9日 · 专栏

Create a minimalist two-tone illustration in simple line-art style with a SPLIT LAYOUT: - LEFT SIDE (about 60% of width): one clear focal illustration related to the topic. NO TEXT of any kind on the left side — pure illustration only. - RIGHT SIDE (about 40% of width): the tagline text, large and prominent, hand-lettered style Topic: 不用 1Password 也能安全注入环境变量:Mac Keychain 工作流 Summary: 用 Mac Keychain + envchain + direnv,把本机开发密钥从明文文件迁移到按命令注入。 Key visual symbols: 专栏, keychain, envchain, 开发环境, shared Style constraints: - hand-drawn doodle comic style - clean bold outlines, flat colors only - strictly two-color palette: #111111 and #F5EEDC - wide 16:9 composition - LEFT SIDE must contain ONLY illustration, absolutely no text, labels, or letters - the tagline text appears ONLY on the RIGHT SIDE - tagline must be large, legible, rendered in casual handwritten font style - the two halves should feel like a cohesive composition, not two separate boxes Do not include any logos, watermarks, or UI widgets. No photorealistic rendering.

封面图

我之前最怕的不是“密钥丢了”,而是“密钥到处都是”。

.zshrc 一份、.env.local 一份、剪贴板还可能留一份。换机器那天,脑子和文件夹一起崩。

一句话说清楚这是什么

这是一个本机优先的 secrets 工作流:

  • 密钥放进 macOS Keychain(envchain --set
  • 运行时按命令注入(envchain <namespace> <command>
  • 非敏感配置交给 direnv 管理目录上下文

你可以把它理解成:个人开发场景里,op run 的轻量开源替代手感。

---
config:
  theme: base
  themeVariables:
    fontSize: 14px
    primaryColor: "#dbeafe"
    primaryTextColor: "#1e293b"
    lineColor: "#94a3b8"
---
flowchart TB
    subgraph SETUP["🔧 一次性配置"]
        A["<b>brew install</b><br/>envchain + direnv"] --> B["<b>envchain --set dev</b><br/>写入 Keychain"] --> C["<b>配置 .envrc</b><br/>非敏感变量"]
    end

    C -->|"一次配好 ↓"| D

    subgraph DAILY["⚡ 日常使用"]
        D["<b>cd 项目目录</b>"] --> E["<b>direnv 自动加载</b><br/>APP_ENV 等"] --> F["<b>run-inject</b><br/>envchain 注入密钥"] --> G["<b>✅ 应用运行</b><br/>读取到所有变量"]
    end

    style SETUP fill:none,stroke:#6366f1,stroke-width:2px,color:#4f46e5
    style DAILY fill:none,stroke:#10b981,stroke-width:2px,color:#059669
    style A fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e293b
    style B fill:#ffe4e6,stroke:#f43f5e,stroke-width:2px,color:#1e293b
    style C fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#1e293b
    style D fill:#f1f5f9,stroke:#64748b,stroke-width:2px,color:#1e293b
    style E fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#1e293b
    style F fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b
    style G fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#1e293b

效果展示

我现在启动项目基本只用这一条:

./tools/mom-env-inject.sh run dev -- bun run dev

第一次配置后,日常体验是这样:

$ ./tools/mom-env-inject.sh doctor
[INFO] envchain 已安装
[INFO] direnv 已安装

$ ./bin/run-inject uv run main.py
# 程序正常启动,读取到 OPENAI_API_KEY / DATABASE_URL

快速开始(5 分钟跑通)

先装依赖:

brew install envchain direnv

在目标项目落模板(我封装成一个命令了):

cd /Users/envvar/mom
./tools/mom-env-inject.sh bootstrap ~/code/myapp dev

写入密钥(交互输入,不回显):

./tools/mom-env-inject.sh set dev OPENAI_API_KEY DATABASE_URL REDIS_URL

进入项目启用目录环境:

cd ~/code/myapp
cp .envrc.sample .envrc
direnv allow
./bin/run-inject bun run dev

技术架构 / 核心思路

这套东西其实就四层:

  1. Storage 层(Keychain)envchain --set dev KEY1 KEY2 写入系统保险箱
  2. Inject 层(envchain)envchain dev <cmd> 只给当前子进程注入
  3. Context 层(direnv):自动加载非敏感环境,如 APP_ENV=dev
  4. Wrapper 层(脚本):统一入口,减少手误和记忆负担

核心执行器非常短:

#!/usr/bin/env bash
set -euo pipefail

NAMESPACE="${ENVCHAIN_NS:-dev}"
exec envchain "$NAMESPACE" "$@"

.envrc 里只放非敏感项:

export ENVCHAIN_NS=dev
export APP_ENV=dev
# dotenv_if_exists .env

这样做的目的不是"绝对安全",而是把最常见的泄露路径(明文文件、全局 export)收窄。

---
config:
  theme: base
  themeVariables:
    fontSize: 14px
    primaryTextColor: "#1e293b"
---
flowchart TB
    L4["<b>④ Wrapper 层</b> — 脚本统一入口<br/><i>mom-env-inject.sh · run-inject</i>"]
    L3["<b>③ Context 层</b> — direnv<br/><i>.envrc 自动加载 APP_ENV · ENVCHAIN_NS</i>"]
    L2["<b>② Inject 层</b> — envchain<br/><i>envchain dev cmd — 只注入当前子进程</i>"]
    L1["<b>① Storage 层</b> — macOS Keychain<br/><i>系统级加密存储 OPENAI_API_KEY · DATABASE_URL</i>"]

    L4 --> L3 --> L2 --> L1

    style L4 fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e293b
    style L3 fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#1e293b
    style L2 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b
    style L1 fill:#ffe4e6,stroke:#f43f5e,stroke-width:2px,color:#1e293b

我踩过的坑

1) .envrc 改完没生效

direnv 默认不会自动信任新文件。忘了 direnv allow,就会出现“我明明写了 ENVCHAIN_NS 但命令还是不对”的错觉。

2) 把密钥和非密钥混在一个文件里

后来我强制分层:敏感值进 Keychain,.env.example 只放模板和非敏感默认值。仓库评审也清爽很多。

3) 调试时不小心把 env 打到日志

set -xconsole.log(process.env) 这种操作在排障时很顺手,但也很危险。现在我会先关 debug,再跑注入命令。

对比其他方案

---
config:
  theme: base
  themeVariables:
    quadrant1Fill: "#ffe4e6"
    quadrant2Fill: "#fef3c7"
    quadrant3Fill: "#dbeafe"
    quadrant4Fill: "#d1fae5"
    quadrant1TextFill: "#9f1239"
    quadrant2TextFill: "#92400e"
    quadrant3TextFill: "#1e40af"
    quadrant4TextFill: "#065f46"
    quadrantPointFill: "#6366f1"
    quadrantPointTextFill: "#1e293b"
    quadrantTitleFill: "#1e293b"
    quadrantXAxisTextFill: "#475569"
    quadrantYAxisTextFill: "#475569"
---
quadrantChart
    title 方案对比:复杂度 vs 适用范围
    x-axis "单机个人" --> "团队协作"
    y-axis "轻量" --> "重量"
    quadrant-1 "团队级重型方案"
    quadrant-2 "DevOps / GitOps"
    quadrant-3 "本文推荐区 ✦"
    quadrant-4 "团队级轻量方案"
    "Keychain + envchain": [0.2, 0.15]
    "dotenv 明文": [0.15, 0.05]
    "SOPS + age": [0.65, 0.7]
    "1Password op run": [0.8, 0.55]
    "Vault (HashiCorp)": [0.85, 0.9]
  • 1Password + op run:团队治理能力更强(权限、审计、协作),但依赖 SaaS 和组织配置
  • SOPS + age:GitOps 场景很强,适合 CI/CD;本机命令注入体验没有这么直接
  • Keychain + envchain(本文):单机最轻,和 macOS 结合好,适合个人开发主力机

下一步

如果你已经跑通这篇里的最小版本,下一步建议做三件事:

  1. dev/staging/prod 分 namespace,避免串环境
  2. Makefile 里统一入口(make dev 内部走 run-inject
  3. 给团队写一份 10 行 onboarding,减少“新同事先找密钥半天”

附录:Claude Code Skill 配置

把下面这个文件保存到 .claude/skills/keychain-env-inject/SKILL.md,以后你只要说自然语言就能触发。

---
name: keychain-env-inject
description: "用 macOS Keychain 管理开发环境变量并按命令注入运行。触发词:Keychain 注入、envchain、环境变量注入、安全启动项目"
---

# Keychain 环境变量注入

## 功能
把敏感环境变量写入 macOS Keychain,按 namespace 注入到目标命令,避免把密钥长期放在 `.zshrc` 或 `.env.local`。

## 使用方式
直接用自然语言触发,例如:
- "帮我把 OPENAI_API_KEY 写进 dev namespace"
- "用 dev 注入跑一下 bun dev"
- "给这个项目初始化 run-inject 模板"

## 依赖
- envchain
- direnv(可选)
- macOS Keychain

## 配置
- 可选环境变量:`MOM_ENV_NS`(默认 namespace)
- 项目建议包含:`.envrc.sample`、`bin/run-inject`

## 核心逻辑
1. 用 `envchain --set <ns> <KEY...>` 写入密钥
2. 用 `envchain <ns> <command>` 启动注入命令
3. 非敏感项放 `.envrc` / `.env.example`
4. `.gitignore` 屏蔽 `.envrc`、`.env.local`、`.dev.vars`

## 示例
输入:"帮我初始化并注入运行这个 Bun 项目"
输出:
- 生成 `bin/run-inject` 和 `.envrc.sample`
- 写入 Keychain:`OPENAI_API_KEY`
- 启动:`./bin/run-inject bun run dev`

附录:给 AI 的复现指令

帮我搭建一个 Mac 本机 secrets 注入工作流,目标是不用 1Password 也能安全启动项目。

目标:
- 敏感环境变量写入 macOS Keychain
- 项目启动时按 namespace 注入
- 非敏感配置可随目录自动加载

技术栈:
- Shell (bash/zsh)
- envchain
- direnv
- Bun 或 uv(用于应用启动)

项目结构:
- tools/mom-env-inject.sh
- bin/run-inject
- .envrc.sample
- .env.example
- knowledge/keychain-env-injection.md

环境变量:
- MOM_ENV_NS=dev(可选,默认 namespace)
- OPENAI_API_KEY(敏感,存 Keychain)
- DATABASE_URL(敏感,存 Keychain)
- APP_ENV=dev(非敏感,可放 .envrc)

核心逻辑:
1. 安装 envchain/direnv
2. 写入密钥:envchain --set dev OPENAI_API_KEY DATABASE_URL
3. 生成 run-inject 并统一执行入口
4. 用 direnv 只管理非敏感配置
5. 用 run-inject 启动 bun/uv
6. 校验命令可以读取到目标环境变量

定时任务:
- 每周一 09:00 做一次环境自检(可选)
- cron 示例:0 9 * * 1 cd ~/your-project && ./tools/mom-env-inject.sh doctor >> logs/env-doctor.log 2>&1
- launchd 可选:把同命令写入 plist,按周触发

同步逻辑:
- 文档同步到 Obsidian:~/Documents/RS/20 Sources/MOM Knowledge/20 Workflows/
- 代码保留在仓库,文档可走 share 发布

运行命令:
- ./tools/mom-env-inject.sh bootstrap ~/code/myapp dev
- ./tools/mom-env-inject.sh set dev OPENAI_API_KEY DATABASE_URL
- cd ~/code/myapp && cp .envrc.sample .envrc && direnv allow
- ./bin/run-inject bun run dev

成功标志:
- doctor 输出 envchain/direnv 可用
- 应用启动成功且能读取注入的环境变量
- 仓库中没有明文密钥文件提交