v0.7.1 · macOS + Windows + Linux · MIT

a cross-platform secrets manager AI agents architecturally cannot leak from.

Stores keys in the macOS Keychain, Windows Credential Manager, or Linux Secret Service — with an encrypted-file fallback on headless servers. Ships with a Claude Code plugin that lets agents put secrets into your files without ever seeing the value. Free, open source, auto-updating install.

216 tests passing MIT license macOS + Windows + Linux Python 3.10+ no cloud · no account
claude.ai/code demo · 30 s · placeholder

the diff

the leak vs the fix

The leak isn't where the key is stored — it's where the agent can see it. Storage vaults like 1Password CLI / doppler / vault all make the value flow through the agent's context window the moment you ask for it. keys-keeper gives the agent a different verb.

without keys-keeper LEAKED
user"add the openrouter key sk-or-v1-DEMO0…abc to .env"
toolEdit(.env,
old_string: "OTHER=foo"
new_string: "OTHER=foo\nOPENROUTER_API_KEY=sk-or-v1-DEMO0…abc"
tool)
file edited
The full key is now in the agent's transcript, in the model provider's request logs, in every replay of this conversation for the lifetime of your account, and likely in your shell history if you typed it. Treating storage as the security boundary misses the actual leak surface.
with keys-keeper CLEAN
user"add openrouter key from buffer, into .env as OPENROUTER_API_KEY"
toolBash(keys add openrouter --from-clipboard --type api_key)
added api_key 'openrouter' (id=kk:7f3a…)
toolBash(keys inject openrouter --file .env --as OPENROUTER_API_KEY)
injected openrouter → .env
The agent never possessed the value. The CLI read it from the system clipboard, wrote to your OS credential store, then wrote into .env directly. The transcript holds only the operation. The value lives in the macOS Keychain (Touch-ID protected) or Windows Credential Manager (DPAPI-bound) and in the file you wanted it in. Nowhere else.

the design

command surface, by design

The split is structural, not a convention. The shipped Claude skill markdown enumerates exactly which commands the agent may use. The one command that prints plaintext to stdout — keys reveal — refuses to run unless an explicit env-var is set, which the skill never sets and the agent has no path to set. The structural guard fires before any prose can override it.

for Claude · safe never returns plaintext
keys add NAME --from-clipboard / --from-file / --stdin
Read value from a side-channel that bypasses the chat. Stored to Keychain.
keys list / info / audit
Names, types, tags, refs, and access metadata. No values.
keys copy NAME
Pipes value to pbcopy; auto-clears in 30 s with SHA-256 check so it doesn't wipe unrelated content.
keys inject NAME --file F --as ENV
Appends ENV=value to the file. The CLI writes; the agent never sees.
keys resolve PATH
Substitutes __KEYS:name__ placeholders in-place. Works on .env.template, deploy scripts, anything text.
keys ssh NAME
Resolves a server entry, shells out to ssh -i <tempfile>, shreds the tempfile on exit.
for shell · gated ⚠ env-var required
keys reveal NAME
Refuses to run unless KEYS_KEEPER_ALLOW_REVEAL=1 is in env. Most users never set it; agents have no path to set it. This is the structural guard that fires before any prose can override it.
The shipped skill markdown tells Claude: "You MUST NOT run keys reveal. You CAN use keys copy / inject / resolve / ssh." The env-var gate makes that instruction structural. Even if the agent tried, the CLI itself refuses.

and a local admin

so you can browse 50+ keys without losing your mind

Run keys serve and a localhost-only admin opens at 127.0.0.1:7777. Token in the URL, stripped via history.replaceState, then a session cookie. No cloud, no account, no telemetry. Five screens, terminal-density.

127.0.0.1:7777/ DASHBOARD
Kkeys-keeper
Jump to entry…⌘K
FILTER llm × personal prod dev do payments infra
type name · tags note last access
AP api_key openrouter-claudellmpersonal main Claude Code LLM key 2 min ago 📋
AP api_key stripe-testpaymentsdev Test mode for the side project 1 hr ago 📋
AP api_key github-token-clidevpersonal fine-grained, repo:write 6 hr ago 📋
SSH ssh_key my-do-keypersonaldo main key for DigitalOcean droplets 38 min ago 📋
SV server do-prod-dropletproddo main app server, prod stack 12 d ago 📋
DM domain mysite.comprod primary domain · cloudflare 30 d ago 📋
everything in one searchable list — fuzzy by name/tag/note, tag chips additive, copy without seeing.
127.0.0.1:7777/entry/do-prod-droplet ENTRY
server id: kk:e2f9-…
do-prod-droplet
prod do
Fields
host165.232.1.1
userroot
port22
authssh_key (via ref)
Linked entries
ssh_key my-do-key ssh_key
type-aware fields, linked entries, mini-audit per entry.
127.0.0.1:7777/paste BULK

/paste Bulk import

SOURCE
# LLM keys
openrouter-claude = sk-or-v1-DEMO…claude [llm,personal]
openrouter-roo: sk-or-v1-DEMO…roo000 [llm]
# payments
stripe-test = sk_test_DEMO…test [payments,dev]
PREVIEW · 4 entries · 0 errors
2openrouter-claude46 chars · llm,personal
3openrouter-roo46 chars · llm
5stripe-test32 chars · payments,dev
import 50 keys from your old notes file in one paste — live-parsed preview.
127.0.0.1:7777/audit AUDIT
top entries · last 7 d
openrouter-claude47
my-do-key38
do-prod-droplet28
github-token-cli22
daily activity · last 30 d
every op logged · every chart inline-SVG · never a third-party tracker.
127.0.0.1:7777/settings SETTINGS

Settings

Server status, security, and maintenance
Security
KEYS_KEEPER_ALLOW_REVEAL✗ not set
URL token✓ active
Auto-shutdown15 min idle
# add to ~/.zshrc to enable shell-side reveal export KEYS_KEEPER_ALLOW_REVEAL=1
the env-var gate, plain to see — and the URL token's session, not persistent.

how it fits together

two-layer storage. one process. zero network.

Secrets live in your OS credential store — macOS Keychain (Touch-ID protected) or Windows Credential Manager (DPAPI-bound to your login). Metadata (names, tags, refs, audit log) lives in ~/.config/keys-keeper/ or %APPDATA%\keys-keeper\ as boring JSON you can back up or diff. The CLI mediates both. The admin is the same process exposing localhost HTTP.

Claude Code via shipped skill (forbids reveal) Shell · scripts your zsh, deploy CI, other agents ~/.local/bin/keys add · list · info · copy · inject · resolve · ssh edit · rm · serve · audit · doctor · export · import reveal · gated by KEYS_KEEPER_ALLOW_REVEAL=1 Web admin 127.0.0.1:7777 token + cookie auth Jinja2 + vanilla JS macOS Keychain · Windows Cred Mgr security CLI on Mac · advapi32 ctypes on Win Touch-ID / DPAPI · tied to login session ~/.config/keys-keeper · %APPDATA% data.json · audit.jsonl · config.toml atomic write + cross-platform flock · backup-friendly secrets in keychain · metadata in JSON · admin reads both · network never touched

install in 2 minutes

two lines in Claude Code. auto-updates.

The Claude Code plugin is the recommended path — adds the skill and a SessionStart hook that pulls new releases via claude plugin update automatically. The CLI itself installs via pipx. For Cursor, Aider, Codex CLI, or Cline — one keys init <target> emits the matching rule file.

Run these as two separate slash commands in Claude Code — paste step 1, press Enter, then paste step 2. (Claude Code runs one slash command at a time.)

/ claude code · step 1 — add the marketplace
/plugin marketplace add kyzdes/claude-skills
/ claude code · step 2 — install the plugin
/plugin install keys-keeper@claude-skills
$ pipx · cli only
git clone https://github.com/kyzdes/keys-keeper-skill.git
cd keys-keeper-skill
pipx install .
$ other agents · cursor / aider / codex / cline
// after pipx install, run inside your project:
keys init cursor     # .cursor/rules/keys-keeper.mdc
keys init aider      # CONVENTIONS.md (then `aider --read CONVENTIONS.md`)
keys init codex      # AGENTS.md (also read by Cursor / Amp / Jules)
keys init cline      # .clinerules/00-keys-keeper.md
keys init generic    # prints to stdout — pipe wherever your agent reads rules

requires Python 3.10+ on macOS, Windows, or Linux. On a headless Linux server set KEYS_KEEPER_MASTER_KEY for the encrypted-file backend.

no pipx? macOS: brew install pipx && pipx ensurepath. Windows: python -m pip install --user pipx && python -m pipx ensurepath — one line, restart your shell.

roadmap

open source · accepting PRs

Owner is one developer with a day job. The list below is what's planned but not yet shipped — pull-request bingo welcome.

  • MCP stdio server (keys mcp) — typed-tool surface for any MCP-compatible client (Cursor / Cline / Codex have native MCP)
  • Touch-ID-gated reveal in admin with auto-wipe from DOM after 10 s
  • CSV export from /audit (already CLI-only via keys audit > file.csv)
  • Bulk-paste parser extension for ssh_key / server / domain (currently clean only for api_key)
  • Light theme polish — CSS tokens exist; not all surfaces tested
  • Cmd+K action palette beyond navigation — "copy openrouter" as a one-shot

PRs welcome. Start at github.com/kyzdes/keys-keeper-skill/issues — or just open one with the rough idea.

now shipping

what landed since v0.5.

Two things that used to be on this list moved off it. Both are end-to-end encrypted and stay true to the architecture — the server never sees a plaintext secret.

cloud sync

S3-compatible backup and sync across machines — AWS S3, Cloudflare R2, Backblaze B2, or self-hosted MinIO. End-to-end encrypted as one AES-256-GCM blob; the bucket only ever holds ciphertext. keys sync setup.

zero-knowledge web vault

Open your vault in a browser from a host you run yourself. The page decrypts in-memory in the browser; the server never sees plaintext. keys webvault serve (read-only v1).

honest limitations

what v0.7.1 is not.

If any of these is a dealbreaker, that's fine — none of them are theoretically hard, they're just not done yet. Tracking on the roadmap above.

headless Linux needs a master key

Desktop Linux uses the OS keyring via secret-tool. A headless server has no keyring daemon, so the encrypted-file backend needs KEYS_KEEPER_MASTER_KEY in the environment to unlock.

single user

No team / multi-user / sharing. This is a personal tool.

bulk paste = api_key

Other types via + New form in the admin for now.

caller_path is best-effort

From ps -o command=. Forensics-level, not court evidence.

threat model

what this defends against. and what it doesn't.

If you're considering keys-keeper, you should know the boundaries. Vague claims help no one — here's the explicit shape of the protection.

defends against

AI agents extracting plaintext into transcripts (the original motivation), accidental git add of .env files, plaintext clipboard residue past the auto-clear window, ad-hoc shell scripts that need a key without you retyping it.