2026-05-15
凭据放 Keychain,不放 .env
230 行 Bash 把 macOS Keychain 包成一个手感顺的 CLI:用 $(secret get foo) 把 API key 注入到一条命令里,用完即逝,不进 shell 环境、不进 history、不进 .env。
问题
只要你跟云 API、数据库或 AI agent 打交道,手里总会攒一堆长期 凭据。它们倾向于沉淀在最不该出现的地方:
.env和~/.zshrc,一个手抖git add就上 GitHub 了。export PGPASSWORD=...,一旦执行就驻留在 shell 环境里,到 session 结束前每个子进程都能读。- shell history,
mysql -p'…'这类调用以明文躺在那里。
更深一层:把一个凭据只交给某一条命令、用完即释放,没有
低摩擦的办法。AI agent 让事情更糟——它频繁 shell out,你
export 出去的任何值都进入了 agent 的 blast radius。
真正想要的
只有两件事:
- 凭据存在某个加密位置,登录系统时解锁一次。
mycommand --token=$(secret get foo) ...能跑,值只流进 那一条命令,然后消失。
仅此而已。不要 daemon,不要 master password,不要新的信任根。
现有方案为什么不够
一些常见的选项,各自的短板:
- dotenv 文件。 加密策略就是”别 commit”。即便不 commit,
加载后还是
export到 shell。 pass/gopass。 工具本身很扎实,但要带上 GPG 密钥、 agent、独立的信任根。如果笔记本已经有一个 OS keychain,就 是过重。- 1Password CLI。 已经付费用户很顺手;但引入网络依赖和 需要保活的 session token。
- 直接调
security。 macOS 内置就有 Keychain。命令也能用 (security find-generic-password -a foo -s bar -w),但难打、 key 不存在时静默返回空、还会把你的条目和系统里所有 Wi-Fi 密码、Safari 登录混在一起。
解法
230 行 Bash 包一层 security。四个设计选择把原始 Keychain
访问变成我真的会去用的东西。
- OS 级加密存储。 值写进
~/Library/Keychains/login.keychain-db,磁盘上加密,登录密码 解锁。不引入新的 daemon,不要 master password。 - 现场注入。 推荐用法是
$(secret get name)——值流进某一 条命令、随它一起结束。不进 shell 环境,不进 history,不被 任何兄弟进程窥视。 - 从其余 Keychain 内容里隔离出来。 所有条目都用同一个
固定的
account=agent-secrets字段。secret list按这个字段 过滤,所以你只看到自己放进去的东西——不是 OS 的杂物。 - 失败显式。
secret get在条目缺失、值为空、keychain 锁定 时 exit 1 + stderr 报错。绝不静默返回空字符串、让你的脚本 把一个未鉴权的请求打到 prod。
实际长这样:
# 录入一次,带描述
secret add aliyun-prod-access-key "阿里云 prod root AK"
# 现场注入,不外泄
ALIBABA_CLOUD_ACCESS_KEY_ID=$(secret get aliyun-prod-access-key) \
aliyun ecs DescribeInstances
# 看看都存了什么
secret list -l
代码开源:zhaidewei/secret-cli。
为什么对 AI agent 特别友好
这套形状对 coding agent 和其他 shell-out 工具尤其合身:
- 凭据从不进 agent 的对话上下文。 Agent 跑
cmd --token=$(secret get foo)时,shell 在exec之前做替换, 明文只落在子进程的 argv 里。Agent 自己看到、log 下来、replay 的 字符串永远是字面量$(secret get foo)——原始值从不出现在对话 transcript 里。 - shell-out 的最小 blast radius。 Agent 频繁 shell out。你
export出去的任何东西在整个 session 里都活着,被它后续 spawn 的每一个 sibling tool call 继承。$(secret get …)只活一条 命令、随它死。即便 agent 后面跑偏了,凭据也不在环境里供它 泄漏。 - 显式失败正是 agent 需要的。 Agent 没有”咦怎么是空的”那种 人类直觉。Exit 1 + stderr 让 agent 立刻发现问题、回头求助; 而不是静默地把一个未鉴权请求打到 prod。
- 自描述的清单。
secret list -l让 agent 自己浏览有哪些 凭据、每条是干嘛的——描述字段同时也是 agent 可读的元数据, 不用单独维护一份credentials.md让 agent 跟它对齐。 - 天生非交互。
secret get在无 tty 下直接返值;secret add接受 stdin 管道。不会卡在”按任意键继续”。
经验总结
绝大多数”重型” secret manager 默认你没有可以倚靠的操作系统。 在 Mac 上你有,而且早就用登录密码付过这个成本了。真正有意思 的部分是上面那一层薄薄的 ergonomic 包装——命名空间、显式失败、 一次性注入——而不是存储本身。便宜的做法是别和 OS 打架,把它 包起来用。