Dewei Zhai

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。

真正想要的

只有两件事:

  1. 凭据存在某个加密位置,登录系统时解锁一次。
  2. 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 打架,把它 包起来用。


想聊聊?和我的助理辩一辩,或者给我留个言