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


我之前最怕的不是“密钥丢了”,而是“密钥到处都是”。
.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
技术架构 / 核心思路
这套东西其实就四层:
- Storage 层(Keychain):
envchain --set dev KEY1 KEY2写入系统保险箱 - Inject 层(envchain):
envchain dev <cmd>只给当前子进程注入 - Context 层(direnv):自动加载非敏感环境,如
APP_ENV=dev - 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 -x、console.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 结合好,适合个人开发主力机
下一步
如果你已经跑通这篇里的最小版本,下一步建议做三件事:
- 给
dev/staging/prod分 namespace,避免串环境 - 在
Makefile里统一入口(make dev内部走run-inject) - 给团队写一份 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 可用
- 应用启动成功且能读取注入的环境变量
- 仓库中没有明文密钥文件提交