mcpField
K
Keystash

Keystash

本地服务
pypi.org安全03 周前更新

关于 Keystash

Keystash 是面向 AI Agent 和命令行工作流的 macOS 密钥管理工具:密钥值保存在登录钥匙串中,Agent 通过元数据和获准操作进行管理。其 MCP 接口不提供读取或返回密钥值的工具。

主要功能

  1. 01将密钥值存入 macOS 登录钥匙串
  2. 02管理密钥元数据与轮换提醒
  3. 03生成和替换密钥
  4. 04使用凭据访问允许列表中的 URL
  5. 05审计凭据使用请求
  6. 06通过 stdio 提供六个 MCP 工具

适用场景

  1. 01让 Agent 调用获准的 API 而不暴露密钥
  2. 02在聊天和文件之外存储、轮换凭据
  3. 03查看已到轮换期限的条目
  4. 04审计使用已存凭据发起的请求

MCP 相关

`keystash mcp` 通过 stdio 提供六个工具,用于列出、存储、轮换、生成、使用和删除密钥。MCP 工具不会返回密钥值;`secret_use` 只向每个条目允许的 URL 发送凭据。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"thu-lawyer-keystash": {
"command": "uvx",
"args": [
"keystash"
]
}
}
}
启动命令uvx keystash

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypikeystash0.4.2

工具

6

收录的工具列表 · 更新于 2 周前

参数类型必填说明
tagstring否仅返回带有此标签的条目。
namestring否仅返回此条目。

详细介绍

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,有三点值得了解:

  1. 每个条目都有允许列表。 每个条目都包含 allowed_urls,URL 必须与其中一项前缀匹配。 https://api.openai.com.evil.com/ 不匹配 https://api.openai.com/, 而前缀匹配也是条目必须以 / 结尾的原因。允许列表为空时会拒绝所有请求——AI 无法选择目标主机,因此 secret_use 无法被用作数据外泄工具。
  2. 重定向会被拒绝,而非跟随(allow_redirects=False),因此允许的主机无法将凭据转发到其他位置。
  3. 在响应正文到达模型之前,会根据该值及其编码形式(原始值、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 和 MCP secret_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,有三点值得知道:

  1. 按条目的白名单。 每个条目带 allowed_urls,URL 必须与其中一条前缀匹配。 https://api.openai.com.evil.com/ 匹配不上 https://api.openai.com/ —— 前缀匹配正是要求白名单项必须以 / 结尾的原因。白名单为空 = 拒绝一切。 AI 无法自选目标主机,所以 secret_use 变不成外送通道。
  2. 拒绝跟随重定向(allow_redirects=False),被允许的主机无法把凭据弹去别处。
  3. 响应体先擦洗再回给模型(值本身及其 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

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。