v0.1 · macOS · MIT

a macOS secrets manager AI agents architecturally cannot leak from.

Stores keys in macOS Keychain. Ships with a Claude skill that lets agents put secrets into your files without ever seeing the value. Free, open source, single-binary install.

read on github
103 tests passing MIT license macOS 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 from pbpaste, wrote to keychain, then wrote into .env directly. The transcript holds only the operation. The value lives in macOS Keychain (Touch-ID protected) 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 the macOS Keychain — Touch-ID protected, tied to your login session. Metadata (names, tags, refs, audit log) lives in ~/.config/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 via security CLI · service "keys-keeper" Touch-ID protected · tied to login session ~/.config/keys-keeper/ data.json · audit.jsonl · config.toml atomic write + fcntl flock · Time Machine friendly secrets in keychain · metadata in JSON · admin reads both · network never touched

install in 5 minutes

five lines of bash. zero account.

Single binary on PATH after pipx. The skill install is a separate command so you can say no to it (e.g. you're trying it from the CLI before adding the Claude integration).

$ install
git clone https://github.com/kyzdes/keys-keeper.git
cd keys-keeper
pipx install .
./scripts/install_skill.sh

requires Python 3.10+ and macOS. Linux/Windows backends are on the roadmap below.

no pipx? brew install pipx && 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.

  • Linux backend via secret-tool (libsecret) — KeychainBackend interface already abstracted
  • Windows backend via Credential Manager (with chunking for SSH keys; CredMan has a 2560-byte cap)
  • Touch-ID-gated reveal in admin with auto-wipe from DOM after 10 s
  • Cursor / Aider / Cline rule-file generators beyond the Claude skill format
  • 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/issues — or just open one with the rough idea.

honest limitations

what v0.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.

macOS only

Keychain backend is the only one shipped. Linux/Windows are on the roadmap.

single user

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

no cloud sync

Use keys export + your encrypted-file sync route.

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.