Hermes 学会用你的密码库了:替你登录、填卡、收 2FA 验证码


你让 Hermes 帮你登录公司后台改个配置,它在登录页前停下了。不是因为它不会点,而是因为密码只有你知道。于是你把它复制进聊天框——从这一刻起,这段明文躺在会话记录里、日志里,以及下一轮要发给模型的上下文里。为了省一次点击,你付出的代价是把密码交给了整条链路。

Hermes 刚合并的浏览器凭据库(credential vault)就是冲这件事来的:Agent 自己走到登录页,标识符(邮箱/用户名)它自己敲,密码由 Hermes 在服务端注入页面,模型看到的结果只有一行 {filled_fields: 1, origin: "https://github.com"}。密码不进对话、不进日志、不进会话数据库,也不出现在任何进程的命令行参数里。

它是怎么分工的

整套东西分成两层:一层是存登录信息的库,一层是浏览器里干活的工具。Agent 拿到的始终只是句柄和可见的标识符元数据,真正的密码在填充那一刻才由后端解出来,直接写进页面。

工具 干什么
browser_vault_list 列出可用的登录项(只有标签、标识符、站点,没有密码)
browser_vault_save_login 在登录页上就地保存一条新登录
browser_vault_fill 把某条登录注入当前页面(校验 origin)
browser_vault_enter_code 处理二次验证码那一格
browser_vault_unlock 解锁外部密码管理器(1Password / Bitwarden)

存储落在 ~/.hermes/vault/ 下(Fernet 密钥 + vault 文件,权限都是 0600),并且按 profile 隔离——如果你在用多 profile 干活,每个 profile 的凭据是分开的,这一点和真实 profile 浏览是同一套思路。

第一次:它在登录页当场问你

这里最值得称道的是“零设置”。你不需要先学 hermes vault add,也不用去设置里找个开关——直接说“登录 GitHub”,Agent 走到登录页发现这个站点没有存过登录,就会在你的界面里弹一个掩码提示:

  • CLI / TUI:sudo 弹窗那种两步面板,先显示邮箱/用户名(明文可见),再输入密码(不回显)。
  • 桌面端:一张“要保存你的 github.com 登录吗?“的卡片,带标注清楚的 Email or username 字段和掩码密码字段,两个按钮:Don’t save / Save & sign in。

你说保存,Hermes 就把它加密、绑定到页面 origin、立刻填进去;你说不保存,返回 save_declined,Agent 这一轮不再追问。密码从始至终没有经过聊天内容——它是弹窗里输入的,不是在对话里打的。

注意这里有个设计细节:这些工具是跟着浏览器工具集走的,而不是“等库里有了东西才出现”。因为库空的时候恰恰是最需要“保存登录”的时候——早先的版本就是把这个功能藏在“库里有项目”后面,结果没人发现它。

2FA:三种情况,三种处理

密码过了还有验证码这道坎。之前 Agent 走到这是真的没辙:登录分类器故意排除了验证码输入框(密码绝不能落进 OTP 框),而第二步又没有对应工具,唯一能做的就是在聊天里问你。

现在按情况分三路:

站点要什么 Hermes 怎么做
验证码,且保存的登录里带了 authenticator key(本地库,或 1Password / Bitwarden 条目里的 TOTP 种子) 自己算出当前验证码并填入,不问你
短信 / 邮件 / App 收到的一次性码,没存 key 在你的界面弹出小提示(“github.com 的验证码”),你输入,它填入;验证码同样不进对话
Passkey、硬件密钥、App 里点批准 没有东西可输;Agent 让你在自己设备上完成,然后等页面继续

authenticator key 就是网站开启 2FA 时给的那个“设置密钥”或 otpauth:// 链接。加进去的地方在设置 → Passwords & Logins → Add(可选字段),或者 hermes vault add 里那一步;带 key 的条目会显示 2FA auto 徽章。

填写验证码时它还挺谨慎:只有在页面上的控件确实是“刚好等于验证码位数、每个都是 maxlength=1、在同一个表单里且相邻”时才会逐位输入;否则整串一次性填进去——这样页面上那些促销码、邮编、推荐码之类的输入框不会被数字喷一脸。

已经在用 1Password 或 Bitwarden?什么都不用开

如果你装了 op 或 bw 命令行工具并且已经登录,Hermes 会自动识别它,里面的网站登录项直接变成可填充的句柄,和本地库里的条目并列。句柄按后端加前缀区分(本地是 vault_…,1Password 是 op:…,Bitwarden 是 bw:…),所以工具侧不需要为不同来源改 schema。

第一次用到里面的登录时,它会让你输入主密码解锁——掩码提示,每个会话一次,空闲 30 分钟过期。主密码是交给密码管理器自己的 CLI 的:1Password 走 op signin 的标准输入,Bitwarden 走 bw unlock --passwordenv 的子进程环境变量。Hermes 只在内存里留会话 token,既不落盘也不进环境变量备份,Agent 看不到主密码、token 和任何一条登录。

想干脆不用某个被识别到的管理器:hermes vault sources --disable bitwarden,或者去设置里那个开关。

付款和地址:每一笔刷卡都要你点头

卡和地址的存储方式跟登录一样,也是保存一次、绑定到结账站点、只在该站点填充。区别在确认环节:每一次填卡都会先问你,走的是和危险命令同一套审批提示,你拒绝就什么都不写。地址填充不需要确认。

另外一条边界值得记住:无人值守的会话填不了卡。cron、webhook、API server 这些地方没人能点“确认”,所以这类填充会被直接拒绝——也就是说,一段提示注入哪怕真把 Agent 带到了结账页,它能“问”,但花不出去钱。这套思路和无人值守审批是同一个原则:没人应答的时候,默认拒绝而不是默认放行。

命令行速查

桌面端在设置 → Passwords & Logins里管理一切;命令行这边是四个子命令:

# 交互式添加一条登录 / 卡 / 地址(密码走 getpass,永远不作为命令行参数)
hermes vault add
hermes vault add --kind payment     # 支付卡
hermes vault add --kind address     # 收货地址

# 列出条目:只有句柄、来源、类型、标签、标识符、origin,没有密码
hermes vault list

# 按句柄删除
hermes vault rm vault_3f9c1a

# 看看哪些密码管理器被识别到了;关闭或重新启用其中一个
hermes vault sources
hermes vault sources --disable onepassword
hermes vault sources --enable onepassword

新登录项的名字(label)、站点 origin、登录标识符是可见元数据——Agent 能读到、能自己敲进页面。密码和卡号不是。

顺带说清一个容易混淆的点:这里的 vault 和 v0.19 那个外部 SecretSource不是一回事。SecretSource 管的是给 Hermes 自己用的 API Key(从 Bitwarden Secrets Manager / 1Password 读进环境变量),配置在 secrets 段;浏览器凭据库管的是网站上的登录、银行卡、地址,配置在 vault 段。名字像,用途完全不同。

没人应答的会话(cron / webhook / API)

这类会话没人能回答提示,行为是明确区分过的:

  • 本地已保存的登录:照常可用,不需要提示。
  • 被锁住的外部密码管理器:返回 unavailable_in_this_session(这个会话解锁不了)。
  • 这个站点没存过登录:返回 prompt_unavailable(没人能回答)。

想让无人值守场景也能用 1Password,可以给它一个 service account token——把它放进 OP_SERVICE_ACCOUNT_TOKEN,就不用弹主密码了:

vault:
  onepassword:
    enabled: true                      # 想让检测到的管理器停用就设 false
    account: ""                        # 传给 `op --account` 的账号简写;留空用默认
    service_account_token_env: OP_SERVICE_ACCOUNT_TOKEN
  bitwarden:
    enabled: true
    binary_path: ""                    # bw 的绝对路径;留空走 PATH

它保证什么,不保证什么

值得说清楚,因为“模型看不到密码”这句话很容易被过度解读。

保证:密码不通过 Hermes 进入模型上下文——不在工具结果里、不在日志里、不在会话数据库里,也不在任何进程的 argv 里。填充走的是受管浏览器会话的直接 CDP socket,写之前还会在页面里再校验一遍:页面 origin 必须和保存时完全一致,否则拒绝。

不保证:它防不了页面自己。密码一旦被填进某个网站,那个网站(和它跑的任何脚本)就拿到了它——和你自己手打进去完全一样。用云浏览器后端的话,厂商的浏览器照常能看到页面。origin 绑定防的是“填错站”,不是“防住被攻陷的对站”。

存储本身是本机加密的,这和opt-in 的系统钥匙串加密是两套东西:凭据库用 Fernet 密钥 + 0600 文件,不依赖系统钥匙串。

版本状态:目前只在 main 上

截至写下这篇,这套功能还没有进入任何 tagged 版本:最新的 v0.21.1(tag v2026.9.7,9 月 7 日发布)里没有 agent/vault_backends/、tools/browser_vault_tool.py 和 hermes_cli/vault.py 这些文件,它们是在 9 月 10 日合并进 main 的。

所以:

  • 用 hermes update 升到 v0.21.1,还拿不到这套能力,要等下一个 tagged 版本把它带上。
  • 想现在就用,需要跟踪 main(git 安装方式),并接受主干代码的日常波动。

不管哪种情况,重装或升级之后的第一步都是同一句话:让 Agent 去登录某个网站,然后看它会不会在登录页前,第一次问你要不要把这个登录存下来。