关于 Keystash
Keystash 是面向 AI Agent 和命令行工作流的 macOS 密钥管理工具:密钥值保存在登录钥匙串中,Agent 通过元数据和获准操作进行管理。其 MCP 接口不提供读取或返回密钥值的工具。
主要功能
- 01将密钥值存入 macOS 登录钥匙串
- 02管理密钥元数据与轮换提醒
- 03生成和替换密钥
- 04使用凭据访问允许列表中的 URL
- 05审计凭据使用请求
- 06通过 stdio 提供六个 MCP 工具
适用场景
- 01让 Agent 调用获准的 API 而不暴露密钥
- 02在聊天和文件之外存储、轮换凭据
- 03查看已到轮换期限的条目
- 04审计使用已存凭据发起的请求
MCP 相关
`keystash mcp` 通过 stdio 提供六个工具,用于列出、存储、轮换、生成、使用和删除密钥。MCP 工具不会返回密钥值;`secret_use` 只向每个条目允许的 URL 发送凭据。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
6收录的工具列表 · 更新于 2 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| tag | string | 否 | 仅返回带有此标签的条目。 |
| name | string | 否 | 仅返回此条目。 |
详细介绍
keystash 🔑
值留在钥匙串中,AI 只拿引用名。 钥匙串保存值;你的 AI Agent 只会获得引用。
keystash 让 AI 助手(MCP、Claude Code 或任何 Agent)帮你管理 API key、token、密码,
而不让它看见这些内容。
从 v0.4.0 起:值保存在 macOS 登录钥匙串中,磁盘上只留有元数据,而且全库没有 read 工具—— 没有任何接口会把密钥交给 AI。
一句话:这一版的安全性来自「没有 read 工具」+「全库没有明文出口」,而不是来自「用了什么加密」。
English
keystash 的优势
常见的建议——“不要把 API key 粘贴到聊天中”——从 Agent 变得有用的那天起就不再管用了。无法读取 OPENAI_API_KEY 的 Agent,就无法轮换它、无法代表你调用 API,也无法告诉你哪个 key 下个月就会过期。
因此,目标不是“Agent 从不接触密钥”,而是Agent 管理它看不见的密钥:
- 值会写入 macOS 登录钥匙串。它绝不会写入此工具拥有的文件、绝不会打印,也绝不会由任何 API 返回。
- 磁盘上以普通的 0600 JSON 格式保存的只有元数据(名称、标签、轮换计划、允许的 URL)。元数据本来就不是秘密——因此列出和编辑它无需解锁。
- 没有
secret_read工具,也没有会将值回显到 stdout 的 CLI 命令。这不是通过禁用标志实现的——对应的代码路径根本不存在。 - 唯一能访问外部世界的途径
secret_use,只能访问你为每个条目预先批准的 URL。
快速开始
pip install keystash # or: uv tool install keystash
keystash doctor # check Keychain / osascript / security / pbcopy
# Store a key. The value is typed into a native hidden dialog — never argv, never the shell history.
keystash add OPENAI_PROD_KEY -a https://api.openai.com/ -r 90
# Let keystash invent one instead; it prints a fingerprint, never the value.
keystash generate STRIPE_PROD_SECRET -a https://api.stripe.com/ -r 180
# generated STRIPE_PROD_SECRET sha256:1f3a9c02 (43 chars)
keystash list # metadata only
keystash list --json
keystash use OPENAI_PROD_KEY https://api.openai.com/v1/models
keystash rm OPENAI_PROD_KEY
通过管道传入值(CI、另一个保险库、密码管理器导出)可跳过对话框:
printf '%s' "$VALUE" | keystash add MY_SERVICE_PROD_KEY --stdin -a https://api.myservice.com/
命名约定:SERVICE_ENV_PURPOSE——OPENAI_PROD_KEY、ALIYUN_OSS_STAGING_SECRET。
强制规则:^[A-Z][A-Z0-9_]{0,63}$。
Touch ID 解锁
值保存在登录钥匙串中。keystash copy NAME 会将值放入剪贴板,并在 30 秒后清除(--clear-after 0 可禁用清除)。不会打印任何内容。
只有在实际需要某个值时才会出现 Touch ID 提示——执行 list、show、doctor 或编辑元数据时都不会出现。
keystash doctor——清理已有的杂乱情况
keystash doctor 会报告平台、钥匙串后端是否可用、osascript / security / pbcopy 的路径、元数据文件、审计日志以及旧版保险库的路径。
如果元数据文件存在,它还会打印条目数量,以及哪些条目已超过轮换间隔。
AI Agent,零明文:keystash mcp
keystash mcp 仅通过 stdio 使用 MCP——它不会打开任何套接字,连 127.0.0.1 上的也不会。
它只提供六个工具。没有 secret_read,也没有 run_command:
| 工具 | 功能 | AI 能看到值吗? |
|---|---|---|
secret_list | 列出条目:名称、标签、轮换日期、允许的 URL | 不能——只能看到元数据 |
secret_store | 存储由用户在对话框中输入的值 | 不能 |
secret_rotate | 以相同方式替换值 | 不能 |
secret_generate | 生成并存储强值 | 不能——会返回指纹 |
secret_use | 使用凭据调用允许的 URL,并返回响应 | 不能——密钥留在请求头中 |
secret_delete | 在你确认的原生对话框中确认后,删除条目及其 Keychain 项目 | 不能 |
将其接入 MCP 客户端:
{
"mcpServers": {
"keystash": {
"command": "keystash",
"args": ["mcp"],
"env": { "KEYSTASH_META": "/Users/you/.keystash/entries.json" }
}
}
}
关于 secret_use,有三点值得了解:
- 每个条目都有允许列表。 每个条目都包含
allowed_urls,URL 必须与其中一项前缀匹配。https://api.openai.com.evil.com/不匹配https://api.openai.com/, 而前缀匹配也是条目必须以/结尾的原因。允许列表为空时会拒绝所有请求——AI 无法选择目标主机,因此secret_use无法被用作数据外泄工具。 - 重定向会被拒绝,而非跟随(
allow_redirects=False),因此允许的主机无法将凭据转发到其他位置。 - 在响应正文到达模型之前,会根据该值及其编码形式(原始值、base64、URL 编码、hex)进行清理,并截断至 20 000 个字符。
每次调用 secret_use 都会向审计日志(~/.keystash/audit.log,模式 0600)追加一行:
时间戳、条目名称、方法、URL、HTTP 状态。记录谁调用了什么,但绝不记录值。
命令
| 命令 | 用途 |
|---|---|
keystash list [-t TAG] [--json] | 列出条目(仅元数据) |
keystash show NAME | 查看单个条目的元数据 |
keystash add NAME [-t TAG] [-r DAYS] [-a URL] [--auth-header H] [--auth-prefix P] [--stdin] [--force] | 存储新密钥 |
keystash rotate NAME [--stdin] | 替换值,保留元数据 |
keystash generate NAME [-t TAG] [-r DAYS] [-a URL] [-l LEN] | 生成并存储(16–128 个字符,默认 43) |
keystash edit NAME [-t TAG] [-r DAYS] [-a URL] | 仅修改元数据——绝不读取或重写值 |
keystash copy NAME [--clear-after SECONDS] | 复制到剪贴板,自动清除(默认 30 秒) |
keystash use NAME URL [-X METHOD] [-d BODY] [--timeout SECONDS] | 调用允许列表中的 URL |
keystash rm NAME [--yes] | 删除条目及其 Keychain 项目 |
keystash migrate [-s VAULT] [--verify/--no-verify] [--retire-vault] | 将 v0.3 加密保管库迁移到 Keychain |
keystash doctor | 检查此机器的前置条件 |
keystash mcp | 提供 MCP stdio 接口 |
从 v0.3 迁移
v0.3 将所有内容保存在一个 PBKDF2 + Fernet 保险库文件中。migrate 会读取该保险库一次,然后将每个值写入 Keychain:
keystash migrate -s ~/keystash-vault.json --retire-vault
旧主密码的获取方式与 v0.3 相同:如果设置了 KEYSTASH_MASTER_PASSWORD,则使用它;接着从登录 Keychain 获取(服务为 keystash,账户为保险库的绝对路径——这是 v0.3 写入的条目,因此从未手动输入过该密码的人仍然无需输入);最后才显示隐藏式提示。值只会经过内存;
唯一的输出是旧名称 / 新名称 / 标签 / 已验证组成的表格。--verify(默认开启)会从 Keychain 中读回每个值并进行比较,之后才会将条目标记为已迁移,因此 Keychain 静默失败不会导致密钥丢失。--retire-vault 会在整个运行完全成功后,将旧文件重命名为 *.migrated——不会删除它。在确认每个条目都迁移成功之前,请保留该文件。
迁移会有意将每个条目的 allowed_urls 留空,因此在允许某个主机之前,secret_use 会拒绝所有请求:keystash edit NAME --allow https://api.example.com/(必须带末尾斜杠)。只有你添加的前缀才能访问。
多设备同步
元数据文件是纯 JSON;你可以随意同步它。值不会同步——它们保存在这台 Mac 的登录 Keychain 中,这正是设计目的。在第二台设备上,重新输入每个值一次,或使用 keystash add --stdin,然后复制元数据文件。
安全模型
在信任此工具之前,请先阅读这一部分。
keystash 消除了什么。 明文清单。以前,你的密钥可能在 ~/.zshrc、.env 文件、Shell 历史记录、聊天记录、MCP 转录记录、剪贴板缓冲区的历史内容中。现在,它们集中在一个 AI 没有工具读取的位置,而且没有任何命令会打印它们。
它不会做到什么。 使用 Keychain 存储时,静态加密由操作系统负责,而其下层是你的登录会话。具体来说:
- 任何以你的身份运行的进程都可以读取相同的值。
security find-generic-password -s keystash -a OPENAI_PROD_KEY -w可以在任何同一用户的 Shell 中运行。keystash 无法防御已被入侵的账户,也不声称能做到。 - 边界是同一用户的进程隔离,而不是加密。 keystash 会从产物——文件、日志、转录记录、argv、环境变量——中移除明文,而不是从受信任进程的内存中移除。
- 元数据并非秘密(纯 0600 JSON):名称、标签、轮换日期和允许的 URL,任何能读取你主目录的人都能读取。名称会泄露意图(“ALIYUN_PROD_KEY”会告诉攻击者该去哪里寻找)。不要在标签中放入秘密。
- 清理采用固定编码集的字面替换(原始格式、base64、urlsafe-base64、百分号编码、十六进制)。以其他方式重新编码的值——加密、拆分为多行、字符移位——可能逃过清理。长度少于 8 个字符的值仍会按原样遮盖,但其编码变体不会,因此短秘密只能得到部分保护。
- 轮换只是提醒,不是强制执行。
-r DAYS会让doctor和 MCPsecret_list标记已过期的条目。不会自动轮换任何内容。 - 注入请求的秘密原则上对同一用户的进程可见; 这与上述边界相同。
AI 仍然可以做什么。 它可以列出条目名称、删除条目、使用存储的凭据调用允许列表中的 URL(并读取响应),还可以存储新值——包括它编造的值。请将 secret_delete 和允许列表视为需要审核的内容,而不是设置一次就不再查看的内容。请阅读审计日志。
keystash 消除的是明文,而不是信任。
环境变量
| 变量 | 含义 | 默认值 |
|---|---|---|
KEYSTASH_META | 元数据文件路径 | ~/.keystash/entries.json |
KEYSTASH_VAULT | 旧版 v0.3 保险库路径——仅由 migrate 使用 | ~/.keystash/vault.json |
--meta/-m 和 --vault/-V 标志会覆盖这些变量。
Development
git clone https://github.com/thu-lawyer/keystash.git
cd keystash
pip install -e ".[dev]"
pytest -q # the real Keychain is never touched
ruff check src tests
The test suite drives the CLI as a subprocess and injects an in-memory store, so it passes on
Linux and Windows CI with no Keychain. Set KEYSTASH_TEST_KEYCHAIN=1 to opt into the one test
that touches the real login Keychain.
License
MIT
中文
为什么是 keystash
「别把 API key 贴进对话」这条建议,在 agent 变得有用的那天就失效了。
一个读不到 OPENAI_API_KEY 的 agent,没法帮你轮换它、没法代你调接口、也说不清下个月哪个 key 过期。
所以目标不是「agent 永不接触密钥」,而是让 agent 管理它看不见的密钥:
- 值进 macOS 登录钥匙串,不写进本工具拥有的任何文件、不打印、不被任何接口返回。
- 磁盘上只有元数据(名字、标签、轮换周期、允许的 URL),0600 明文 JSON。 元数据故意不算秘密 —— 所以列出和编辑它不需要解锁。
- 没有
secret_read工具,也没有任何把值打到 stdout 的 CLI 命令。 不是「关掉的开关」,是这条代码路径根本不存在。 - 唯一通往外面的路
secret_use,只能打你按条目预先批准的 URL。
快速上手
pip install keystash # 或:uv tool install keystash
keystash doctor # 检查 Keychain / osascript / security / pbcopy
# 存一个 key。值在系统隐藏输入框里手打 —— 不走 argv,不进 shell 历史。
keystash add OPENAI_PROD_KEY -a https://api.openai.com/ -r 90
# 也可以让 keystash 生成;它只打印指纹,不打印值。
keystash generate STRIPE_PROD_SECRET -a https://api.stripe.com/ -r 180
# generated STRIPE_PROD_SECRET sha256:1f3a9c02 (43 chars)
keystash list # 只有元数据
keystash list --json
keystash use OPENAI_PROD_KEY https://api.openai.com/v1/models
keystash rm OPENAI_PROD_KEY
从管道喂值(CI、别的密码库导出)可以跳过弹窗:
printf '%s' "$VALUE" | keystash add MY_SERVICE_PROD_KEY --stdin -a https://api.myservice.com/
命名规范:SERVICE_ENV_PURPOSE —— OPENAI_PROD_KEY、ALIYUN_OSS_STAGING_SECRET。
已强制校验:^[A-Z][A-Z0-9_]{0,63}$。
Touch ID 解锁
值在登录钥匙串里。keystash copy NAME 把值放进剪贴板,30 秒后自动清除(--clear-after 0 关闭清除)。
不打印任何东西。 真正取值时才出现 Touch ID 提示 —— list、show、doctor、
改元数据都不会触发。
keystash doctor —— 主动清剿你已经撒出去的明文
keystash doctor 会报出平台、Keychain 后端是否可用、osascript / security / pbcopy 的路径、
元数据文件、审计日志、旧版 vault 路径。元数据文件存在时,还会给出条目总数和已过轮换期的条目。
AI 助手零明文管理:keystash mcp
keystash mcp 只走 stdio,不开任何端口(连 127.0.0.1 都不开)。
它恰好暴露六个工具,没有 secret_read,也没有 run_command:
| 工具 | 做什么 | AI 会看见值吗 |
|---|---|---|
secret_list | 列出条目:名字、标签、轮换日期、允许的 URL | 不会 —— 只有元数据 |
secret_store | 存一个用户在弹窗里亲手输入的值 | 不会 |
secret_rotate | 用同样方式替换值 | 不会 |
secret_generate | 生成强随机值并存入 | 不会 —— 只返回指纹 |
secret_use | 带凭据调一个被允许的 URL,返回响应 | 不会 —— key 只在请求头里 |
secret_delete | 你在原生弹窗里确认后,删除条目及其钥匙串项 | 不会 |
接到 MCP 客户端:
{
"mcpServers": {
"keystash": {
"command": "keystash",
"args": ["mcp"],
"env": { "KEYSTASH_META": "/Users/you/.keystash/entries.json" }
}
}
}
关于 secret_use,有三点值得知道:
- 按条目的白名单。 每个条目带
allowed_urls,URL 必须与其中一条前缀匹配。https://api.openai.com.evil.com/匹配不上https://api.openai.com/—— 前缀匹配正是要求白名单项必须以/结尾的原因。白名单为空 = 拒绝一切。 AI 无法自选目标主机,所以secret_use变不成外送通道。 - 拒绝跟随重定向(
allow_redirects=False),被允许的主机无法把凭据弹去别处。 - 响应体先擦洗再回给模型(值本身及其 raw / base64 / URL 编码 / hex 变体), 并在 20 000 字符处截断。
每次 secret_use 往审计日志(~/.keystash/audit.log,0600)追加一行:
时间戳、条目名、方法、URL、HTTP 状态。只记谁在何时用了哪个 key,永不记值。
命令一览
| 命令 | 作用 |
|---|---|
keystash list [-t TAG] [--json] | 列出条目(仅元数据) |
keystash show NAME | 单个条目的元数据 |
keystash add NAME [-t TAG] [-r DAYS] [-a URL] [--auth-header H] [--auth-prefix P] [--stdin] [--force] | 存入新密钥 |
keystash rotate NAME [--stdin] | 替换值,保留元数据 |
keystash generate NAME [-t TAG] [-r DAYS] [-a URL] [-l LEN] | 生成并存入(16–128 字符,默认 43) |
keystash edit NAME [-t TAG] [-r DAYS] [-a URL] | 只改元数据 —— 值不被读、不被重写 |
keystash copy NAME [--clear-after SECONDS] | 复制到剪贴板并自动清除(默认 30 秒) |
keystash use NAME URL [-X METHOD] [-d BODY] [--timeout SECONDS] | 调一个白名单内的 URL |
keystash rm NAME [--yes] | 删除条目及其钥匙串项 |
keystash migrate [-s VAULT] [--verify/--no-verify] [--retire-vault] | 把 v0.3 加密 vault 迁进钥匙串 |
keystash doctor | 检查本机前置条件 |
keystash mcp | 提供 MCP stdio 接口 |
从 v0.3 迁移
v0.3 把所有东西放在一个 PBKDF2 + Fernet 的 vault 文件里。migrate 读一次那个 vault,
把每个值写进钥匙串:
keystash migrate -s ~/keystash-vault.json --retire-vault
旧主密码的取法与 v0.3 一致:先看 KEYSTASH_MASTER_PASSWORD,再查登录钥匙串
(service keystash、account = 旧 vault 的绝对路径,就是 v0.3 当年写进去的那条 ——
所以从没手打过这个密码的人,现在也不必打),最后才弹隐藏窗要你手输。值只经过内存;
输出只有一张 旧名/新名/标签/已校验 的表。
--verify(默认开)会把每个值从钥匙串读回来比对,确认无误才算迁移成功 ——
这样钥匙串静默失败也不会悄悄丢 key。--retire-vault 在整轮成功后才把旧文件改名为
*.migrated,不删除。确认所有条目之前,先留着那个文件。
迁移后每条条目的 allowed_urls 都是空的(故意的),所以在放行主机之前 secret_use 一律拒绝:
keystash edit NAME --allow https://api.example.com/(结尾的 / 必须有)。只有你加进去的前缀才可达。
多机同步
元数据是明文 JSON,随便你怎么同步。值不同步 —— 它们在这台 Mac 的登录钥匙串里,
而这正是重点。第二台机器上重新录入(或用 keystash add --stdin 喂一次),再把元数据文件拷过去。
安全模型
信这个工具之前,请读完这一段。
keystash 消掉了什么。 明文清单。过去你的 key 散在 ~/.zshrc、.env、shell 历史、
对话记录、MCP 转录、剪贴板的历史里。现在它们在一个地方,AI 没有工具能读,也没有命令会打印。
它不做什么。 值放在钥匙串里,静态加密是操作系统的活,而它下面那一层是你的登录会话。具体说:
- 任何以你的身份运行的进程都能读到同样的值。 在同一个用户下的任意 shell 里,
security find-generic-password -s keystash -a OPENAI_PROD_KEY -w就能取出来。 keystash 不防「账号已被攻破」,也不声称能防。 - 边界是「同用户进程隔离」,不是「加密」。 keystash 把明文从产物里去掉 —— 文件、日志、转录、argv、环境变量 —— 而不是从一个受信任进程的内存里去掉。
- 元数据不是秘密(0600 明文 JSON):名字、标签、轮换日期、允许的 URL,
能读你家目录的人都能读。名字本身就泄露意图(
ALIYUN_PROD_KEY直接告诉攻击者去哪儿找)。 不要把秘密写进标签。 - 擦洗是「固定编码集合上的字面替换」(raw、base64、urlsafe-base64、百分号编码、hex)。 值若被换成别的编码形态 —— 加密、跨行拆开、字符位移 —— 可能活下来。 短于 8 个字符的值仍会被原样擦除,但不做编码变体扩展,所以短密钥只被部分覆盖。
- 轮换是提醒,不是强制。
-r DAYS只让doctor和 MCP 的secret_list标出逾期条目, 没有任何自动轮换。 - 注入请求的密钥原则上对同用户进程可见,与上面同一条边界。
AI 仍然能做什么。 它能列出你的条目名、删除条目、用存好的凭据调白名单内的 URL
(并读到响应)、以及存新值(包括它自己生成的)。
把 secret_delete 和白名单当成需要你复核的东西,而不是设一次就完事的东西。审计日志要读。
keystash 消掉的是明文,不是信任。
环境变量
| 变量 | 含义 | 默认 |
|---|---|---|
KEYSTASH_META | 元数据文件路径 | ~/.keystash/entries.json |
KEYSTASH_VAULT | 旧版 v0.3 vault 路径 —— 只有 migrate 会读 | ~/.keystash/vault.json |
--meta/-m 与 --vault/-V 可覆盖它们。
参与开发
git clone https://github.com/thu-lawyer/keystash.git
cd keystash
pip install -e ".[dev]"
pytest -q # 不会碰真实钥匙串
ruff check src tests
测试以子进程方式驱动 CLI,并把存储换成内存实现,所以在无钥匙串的 Linux / Windows CI 上也能过。
设 KEYSTASH_TEST_KEYCHAIN=1 才会启用唯一那个会碰真实登录钥匙串的测试。
License
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。