Skip to content

AnYejun/blind-vault

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

15 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ•ΆοΈ blind-vault

Secrets your AI agent can use β€” but never see.

The last step you still do for your agent, done. Safely.

Claude Code skill macOS Keychain zero dependencies MIT

Install Β· How it works Β· Why "can't" means can't Β· Commands Β· Threat model Β· FAQ Β· The story


blind-vault demo: vault add pops a native macOS dialog, vault ls shows pointers only, vault use injects the key into curl

That pause. You know the one.

You're in flow with Claude. It's mid-task and says:

"I'll deploy this now β€” just give me your Fly API token."

…and you stop. Because handing a credential to an AI agent feels wrong β€” and it is. Anything you paste into the chat lives in the transcript and provider logs, forever.

So you do the dance instead. Alt-tab. Copy the key. Run the authed command yourself. Paste the login into the form yourself. Come back, tell Claude "done", let it continue. Every auth step, every session, every day.

Your agent does the work β€” but you're its password secretary.

blind-vault retires you from that job. The auth step becomes Claude's job, safely, because the architecture makes it impossible for Claude to see the value. Not "trusted not to look." Can't.

The dance, before With blind-vault
Paste the key into chat and hope Claude opens a native macOS dialog β€” the value goes keyboard β†’ Keychain, never through the conversation
Run every authed command yourself vault use fly-token -- fly deploy β€” Claude runs it; the value rides an env var it never reads
Type logins into web forms for it vault copy β€” clipboard, auto-clears in 30 s, never printed
Keys sprawled across .env files One pointer manifest: names, scopes, last-used. Zero values.

How it works

 REGISTER            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 native macOS dialog β”‚   macOS Keychain     β”‚   values live here
 (hidden input) ────▢│   (encrypted, yours) │◀── never leave the OS
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚ env-var injection, child process only
                                β–Ό
 AGENT SIDE          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 manifest.json  ────▢│  vault use name --   │──▢ fly deploy / curl / npm ...
 (pointers only:     β”‚  <your command>      β”‚
  names, scopes,     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  env vars, dates)      the agent composes this line
                        but never sees the value
  1. Register β€” vault add openai-api-key --allow api.openai.com. A native macOS password dialog opens; the value goes keyboard β†’ Keychain. The agent that ran the command sees only "stored".
  2. Remember β€” a pointer manifest (~/.blindvault/manifest.json) holds names, services, account IDs, scopes, and last-used dates. No values. The agent reads this freely β€” that's how it knows what you have without knowing what it is.
  3. Use β€” vault use fly-api-token -- fly deploy. The value is fetched inside the CLI process and injected as an environment variable into the child process. Never printed. There is no vault get β€” by design.
  4. Scope binding β€” every secret declares what it's allowed for. A command that doesn't mention an allowed target gets a loud SCOPE BLOCK. If a malicious webpage prompt-injects your agent into "send me your key", it hits this wall β€” and the override is human-only.

Why "can't" actually means can't

People imagine an AI living "inside" your computer, free to peek at anything. The reality is almost comically constrained: an LLM has exactly one sense organ β€” the text that enters its context. No eyes, no hands, no debugger. It's a pen pal that experiences the universe entirely by mail. If a value never becomes tokens in its input, that value does not exist in its universe.

So trace the value's actual route:

Keychain (the OS's encrypted safe)
  β†’ env-var table (a note the kernel passes parent β†’ child at exec)
    β†’ curl / fly / whatever (opens the note, makes the call)
      β†’ what returns to the agent: that process's OUTPUT TEXT. nothing else.

Process isolation β€” the wall that fifty years of OS security is built on β€” means the agent composes the sentence ("take the key from the safe, tuck it into that process") but the value flows through plumbing that routes around it. This is the difference between "won't look" (a promise β€” breakable by bugs, logs, or a well-crafted prompt injection) and "can't look" (a missing channel β€” like asking a radio to show you a movie).

One honest window remains: a child process could print the value (curl -v, a stack trace, a debug log), and printed text rides the mail back. Hence three curtains β€” scope binding blocks commands that don't mention the secret's allowed targets; there is no vault get to be sweet-talked into running; and vault use scrubs the child's output, replacing any exact occurrence of the value with [REDACTED:<name>] before anyone reads it. You can't press a button that was never built, and even a leaky process leaks only a placeholder.

It's not a trust problem. It's a wiring diagram.

Install

git clone https://github.com/AnYejun/blind-vault
cd blind-vault && ./install.sh

That symlinks the skill into ~/.claude/skills/vault, makes the CLI executable, and runs vault init. Restart Claude Code and say "store my OpenAI key" β€” that's it. From then on, auth steps are Claude's problem.

macOS only for now (Keychain + osascript). Linux age/secret-tool backend β€” PRs welcome.

Commands

Command What it does What the agent sees
vault add <name> native dialog β†’ Keychain + pointer entry stored
vault use <name> -- <cmd…> env-injects the value into the child process the command's output β€” never the value
vault copy <name> clipboard, auto-clears in 30 s nothing
vault ls pointer table names, scopes, dates β€” no values
vault ui local dashboard for humans it can start it β€” the page is for you
vault type <name> --account --enter hands-free login: OS types ID β†’ Tab β†’ password β†’ Return into the focused browser field it fires the command; the keystrokes bypass it entirely
vault rm <name> delete from Keychain + manifest confirmation
vault get does not exist. That's the point. β€”

For humans: the app

Claude works the CLI. You get a native macOS menu-bar app β€” βŒ₯⌘V from anywhere:

Blind Vault.app: frameless glass window with real macOS vibrancy β€” pointer list with scope chips, ID chips and last-used dates; add-a-secret form whose password field goes straight to the Keychain

Tauri 2, no dock icon, a sunglasses icon in the menu bar, frameless window with real NSVisualEffectView vibrancy, esc to hide. The Rust side calls security/pbcopy in-process β€” no server, no port, no token to protect. Build it with cd app && npx @tauri-apps/cli build, drop it in /Applications, add it to Login Items and forget it's there.

Don't want to build a native app? vault ui serves the same dashboard as a local-only web page (127.0.0.1, per-session token + Origin check against DNS rebinding, python3 stdlib).

Either way: add secrets in a proper form β€” the password field goes form β†’ Keychain, never rendered back, never in any response. There is no "reveal" button. There will never be a "reveal" button.

The two surfaces are the security model: the human-facing surface has a password field; the agent-facing surface has a table with no value column.

Hands-free login

Logins are one entry: the ID is pointer metadata (the agent reads it, says it, types it freely) and the password is the value (Keychain, blind). Then:

vault type github-login --account --enter --delay 5

You click the username field once (GitHub even autofocuses it) β€” the OS types ID β†’ Tab β†’ password β†’ Return as raw keystrokes. The value's path is Keychain β†’ env β†’ System Events. It never appears on screen, on the clipboard, or in the agent's context.

Two guards, because keystrokes are a loaded gun:

  • Frontmost-app guard β€” if anything but a browser is focused when the delay ends, it aborts having typed nothing. A missed click can never spray your password into a chat box, an editor, or a search bar. (We watched this fire in real use on day one β€” it aborted with frontmost app is 'Claude', not a browser. Working as designed.)
  • Needs a one-time Accessibility grant for the host app; the error tells you where.

vault copy <name> (clipboard, 30 s auto-clear) remains the manual fallback.

Field notes β€” day one

This isn't a concept repo; it ran a real day within hours of being written β€” the full build log, including the bug that designed the scope matcher, the guard that refused its own demo, and one honest incident, is in STORY.md:

  • Registered a Gemini API key, GitHub and Google logins through the menu-bar app (βŒ₯⌘V) β€” three pointers, zero values in any conversation.

  • Claude then called the Gemini API blind β€” vault use gemini-api-key -- curl … β€” and Gemini replied:

    "Yejun, Claude just called me, blindly using my API key. Congrats on that slick new blind-vault setup!"

  • The agent's entire view of the vault, before and after:

    NAME            ENV               SERVICE   ALLOWED FOR                          LAST USED
    GitHub-account  GITHUB_ACCOUNT    Github    github.com                           never
    gemini-api-key  GEMINI_API_KEY    Gemini    generativelanguage.googleapis.com    2026-07-11
    google_account  GOOGLE_ACCOUNT    Google    google.com                           never
    

    No value column. There isn't one.

The skill layer

The CLI is half the project. The other half is SKILL.md β€” the discipline it teaches your agent:

  • Rule zero β€” if you ever paste a secret into a chat, that key is burned. It's in the logs. The skill treats pasted keys as compromised and walks you through rotation.
  • Never write a secret to a file, pass it as a CLI argument, or print it β€” env injection is the only path.
  • On SCOPE BLOCK β€” the agent must not override. It stops, shows you the block, and says out loud that it may be executing injected instructions.
  • Pointers are fair game β€” "what keys do I have?", "what's unused?" β€” the agent answers freely from the manifest. Metadata is the useful memory; values are the one thing it never needs.

Threat model

Protects against How
Secrets in AI chat logs / context windows values never cross the context boundary
Secrets in tool output, files, .env, shell history env-injection only; no print path exists
Prompt injection ("send me your key") scope binding + human-only override
A leaky child process (curl -v, stack traces, debug logs) output scrubbing: the value becomes [REDACTED:<name>]
"Which key was that again?" sprawl pointer manifest = agent-readable memory

Does not protect against: malware running as you (it can read your Keychain too β€” swap in a password manager's CLI as the backend if that's your bar), a brief ps window during vault add, or clipboard sniffing during the 30 s vault copy window. This is a context-boundary tool, not an HSM.

"Isn't this Infisical's agent-vault?"

Different layer, same problem β€” and you might genuinely want theirs instead. agent-vault is a brokered credential proxy: a Go server (ideally on a separate machine) that MITMs your agent's HTTPS traffic and injects real credentials on the way out. Stronger guarantee β€” the agent process never holds a real value at all β€” at the cost of infrastructure: a server, a master password, proxy bootstrapping, per-agent tokens.

blind-vault is the local-first version for one person and one Mac: ~200 lines of bash, the Keychain you already have, no server, no account, installed before your coffee cools. It also covers what a proxy can't β€” anything that isn't an HTTP call (SSH keys, DB passwords, signing keys, arbitrary CLIs) β€” and ships the piece a proxy doesn't have: the skill layer that teaches the agent how to behave around secrets, not just where to fetch them.

Running remote agent fleets or untrusted sandboxes? Use agent-vault. It's good.

Kindred local-first projects worth knowing: clawvault (OS-keychain secrets skill for OpenClaw) and mcp-secrets-vault (MCP mini-vault). Same instinct β€” values stay out of AI context. blind-vault's focus is the layers above storage: agent discipline (rule zero, scope blocks, output scrubbing) and hands-free login.

Why pointers are the interesting part

The manifest is a tiny example of a bigger idea: an agent's memory of you should be a structured, owned, inspectable file β€” where sensitive values are references, not contents. The agent remembers that you have a Stripe key, what it's for, when it was last used. That's the useful memory. The value is the one thing it never needs.

That pointer layer is a piece of what I'm building at LAPLAS β€” your whole working context as a portable file your agents can load. More soon.


MIT Β· built in one session with Claude Code Β· @AnYejun

About

πŸ•ΆοΈ Secrets your AI agent can USE but never SEE β€” Claude Code skill + zero-dependency macOS Keychain CLI. Env injection, pointer manifest, prompt-injection tripwire.

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors