Back to Browse

Keystash MCP Server

Developer ToolsUse Caution4.6MCP RegistryLocal
Free

Server data from the Official MCP Registry

Keychain-backed secret broker: an AI agent can use your keys, but no tool ever returns one.

About

Keychain-backed secret broker: an AI agent can use your keys, but no tool ever returns one.

Security Report

4.6
Use Caution4.6High Risk

keystash is a local secret broker that keeps values in the macOS Keychain, exposes no read tool, and limits outbound credential use to per-entry URL allow-lists, with redirects refused and responses scrubbed. The main limitations are that the core source modules (broker, keychain, mcp_server, cli) were not included, so the central claims are verified only from docs and tests, and that the URL allow-list is a string prefix check that does not normalize dot-segments, which could let an AI reach other paths on an allowed host with the stored credential. No malicious or exfiltration patterns were found in the provided material. Supply chain analysis found 5 known vulnerabilities in dependencies (0 critical, 3 high severity). Package verification found 1 issue.

3 files analyzed · 13 issues found

Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.

Permissions Required

This plugin requests these system permissions. Most are normal for its category.

env_vars

Check that this permission is expected for this type of plugin.

File System Read

Reads files on your machine. Normal for tools that analyze or process local data.

File System Write

Writes or modifies files on your machine. Check that this is expected for the tool.

process_spawn

Check that this permission is expected for this type of plugin.

clipboard

Check that this permission is expected for this type of plugin.

HTTP Network Access

Connects to external APIs or services over the internet.

How to Install

Add this to your MCP configuration file:

{
  "mcpServers": {
    "io-github-thu-lawyer-keystash": {
      "args": [
        "keystash"
      ],
      "command": "uvx"
    }
  }
}

Documentation

View on GitHub

From the project's GitHub README.

keystash 🔑

值留给钥匙串,AI 只拿引用名。 The Keychain keeps the values; your AI agent only ever gets references.

keystash 让 AI 助手(MCP、Claude Code、任何 agent)帮你管理 API key、token、密码, 而不让它看见它们。

v0.4.0 起:值存在 macOS 登录钥匙串,磁盘上只剩元数据,而且全库没有 read 工具 —— 没有任何一个接口会把密钥交给 AI。

一句话:这一版的安全性来自「没有 read 工具」+「全库没有明文出口」,不是来自「用了什么加密」。


English

Why keystash

The usual advice — "don't paste your API key into the chat" — stopped working the day agents became useful. An agent that cannot read OPENAI_API_KEY cannot rotate it, cannot call the API on your behalf, cannot tell you which key expires next month.

So the goal is not "the agent never touches keys". It is the agent manages keys it cannot see:

  • The value goes into the macOS login Keychain. It is never written to a file this tool owns, never printed, never returned by any API.
  • Only metadata (name, tags, rotation schedule, allowed URLs) lives on disk, in plain 0600 JSON. Metadata is deliberately not secret — so listing and editing it needs no unlock.
  • There is no secret_read tool, and no CLI command that echoes a value to stdout. Not a disabled flag — the code path does not exist.
  • The one route to the outside world, secret_use, can only reach URLs you pre-approved per entry.

Quick start

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

Piping a value in (CI, another vault, a password manager export) skips the dialog:

printf '%s' "$VALUE" | keystash add MY_SERVICE_PROD_KEY --stdin -a https://api.myservice.com/

Naming convention: SERVICE_ENV_PURPOSE — OPENAI_PROD_KEY, ALIYUN_OSS_STAGING_SECRET. Enforced: ^[A-Z][A-Z0-9_]{0,63}$.

Touch ID unlock

Values live in the login Keychain. keystash copy NAME puts the value on the clipboard and clears it again after 30 seconds (--clear-after 0 disables the clear). Nothing is printed. The Touch ID prompt appears when a value is actually needed — not on list, not on show, not on doctor, not on metadata edits.

keystash doctor — clean up the mess you already have

keystash doctor reports the platform, whether the Keychain backend is available, the paths of osascript / security / pbcopy, your metadata file, the audit log, and the legacy vault path. If the metadata file exists it also prints the entry count and which entries are past their rotation interval.

AI agents, zero plaintext: keystash mcp

keystash mcp speaks MCP over stdio only — it opens no socket, not even on 127.0.0.1. It exposes exactly six tools. There is no secret_read, and no run_command:

ToolWhat it doesDoes the AI see the value?
secret_listList entries: name, tags, rotation dates, allowed URLsNo — metadata only
secret_storeStore a value the user types into a dialogNo
secret_rotateReplace a value the same wayNo
secret_generateGenerate a strong value and store itNo — a fingerprint is returned
secret_useCall an allowed URL with the credential, return the replyNo — the key stays in the request header
secret_deleteErase the entry and its Keychain item, after a native dialog you confirmNo

Wire it into an MCP client:

{
  "mcpServers": {
    "keystash": {
      "command": "keystash",
      "args": ["mcp"],
      "env": { "KEYSTASH_META": "/Users/you/.keystash/entries.json" }
    }
  }
}

Three things worth knowing about secret_use:

  1. Per-entry allow-list. Every entry carries allowed_urls, and a URL must be a prefix match of one of them. https://api.openai.com.evil.com/ does not match https://api.openai.com/, and prefix matching is why the entries must end in /. An empty allow-list refuses everything — the AI cannot pick the destination host, so secret_use cannot be turned into an exfiltration primitive.
  2. Redirects are refused, not followed (allow_redirects=False), so an allowed host cannot bounce the credential somewhere else.
  3. The response body is scrubbed against the value and its encodings (raw, base64, URL-encoded, hex) before it reaches the model, and truncated at 20 000 characters.

Every secret_use call appends one line to the audit log (~/.keystash/audit.log, mode 0600): timestamp, entry name, method, URL, HTTP status. Who called what, never the value.

Commands

CommandPurpose
keystash list [-t TAG] [--json]List entries (metadata only)
keystash show NAMEOne entry's metadata
keystash add NAME [-t TAG] [-r DAYS] [-a URL] [--auth-header H] [--auth-prefix P] [--stdin] [--force]Store a new secret
keystash rotate NAME [--stdin]Replace the value, keep the metadata
keystash generate NAME [-t TAG] [-r DAYS] [-a URL] [-l LEN]Generate and store (16–128 chars, default 43)
keystash edit NAME [-t TAG] [-r DAYS] [-a URL]Metadata only — the value is never read or rewritten
keystash copy NAME [--clear-after SECONDS]Clipboard, auto-cleared (default 30)
keystash use NAME URL [-X METHOD] [-d BODY] [--timeout SECONDS]Call an allow-listed URL
keystash rm NAME [--yes]Delete the entry and its Keychain item
keystash migrate [-s VAULT] [--verify/--no-verify] [--retire-vault]Move a v0.3 encrypted vault into the Keychain
keystash doctorCheck this machine's prerequisites
keystash mcpServe the MCP stdio interface

Migrating from v0.3

v0.3 kept everything in one PBKDF2 + Fernet vault file. migrate reads that vault once and writes each value into the Keychain:

keystash migrate -s ~/keystash-vault.json --retire-vault

The old master password is fetched the way v0.3 fetched it: KEYSTASH_MASTER_PASSWORD if set, then the login Keychain (service keystash, account = the vault's absolute path — the item v0.3 wrote, so anyone who never typed that password by hand still does not have to), and only then a hidden prompt. Values pass through memory only; the only output is a table of old name / new name / tags / verified. --verify (on by default) reads each value back out of the Keychain and compares it before the entry is considered migrated, so a silent Keychain failure cannot lose a key. --retire-vault renames the old file to *.migrated after a fully successful run — it is not deleted. Keep that file until you have confirmed every entry.

Migration leaves every entry's allowed_urls empty on purpose, so secret_use refuses everything until you allow a host: keystash edit NAME --allow https://api.example.com/ (the trailing slash is required). Only the prefixes you add are reachable.

Multi-machine sync

The metadata file is plain JSON; sync it however you like. The values do not sync — they are in this Mac's login Keychain, and that is the point. On a second machine, re-enter or keystash add --stdin each value once, then copy the metadata file over.

Security model

Read this part before trusting the tool.

What keystash does eliminate. The plaintext inventory. Before, your keys were in ~/.zshrc, in .env files, in shell history, in chat logs, in MCP transcripts, in your clipboard buffer's past. Now they are in one place the AI has no tool to read, and no command prints them.

What it does not do. With Keychain storage, encryption at rest is the OS's job, and the layer beneath is your login session. Concretely:

  • Any process running as you can read the same values. security find-generic-password -s keystash -a OPENAI_PROD_KEY -w works from any same-user shell. keystash does not defend against a compromised account, and does not claim to.
  • The boundary is same-user process isolation, not encryption. keystash removes plaintext from the artifacts — files, logs, transcripts, argv, env — not from a trusted process's memory.
  • Metadata is not secret (plain 0600 JSON): names, tags, rotation dates and allowed URLs are readable by anyone who can read your home directory. Names leak intent ("ALIYUN_PROD_KEY" tells an attacker where to look). Do not put secrets in tags.
  • Scrubbing is literal replacement over a fixed encoding set (raw, base64, urlsafe-base64, percent-encoded, hex). A value re-encoded some other way — encrypted, split across lines, character-shifted — can survive it. Values shorter than 8 characters are still redacted verbatim, but their encoding variants are not, so a short secret is only partially covered.
  • Rotation is a reminder, not an enforcement. -r DAYS makes doctor and the MCP secret_list flag overdue entries. Nothing rotates automatically.
  • Secrets injected into a request are visible to same-user processes in principle; that is the same boundary as above.

What the AI can still do. It can list your entry names, delete entries, call allow-listed URLs with stored credentials (and read the responses), and store new values — including ones it invents. Treat secret_delete and the allow-lists as things you review, not things you set once. Read the audit log.

keystash 消掉的是明文,不是信任。

Environment variables

VariableMeaningDefault
KEYSTASH_METAMetadata file path~/.keystash/entries.json
KEYSTASH_VAULTLegacy v0.3 vault path — used only by migrate~/.keystash/vault.json

The --meta/-m and --vault/-V flags override them.

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

Reviews

No reviews yet

Be the first to review this server!