Claude Code on alex-laptop wants to POST to api.stripe.com/v1/refunds
payment_intent=pi_3Mt7XoLkdIwHu7ix0kjN2wKg
amount=4200
reason=requested_by_customer
metadata[ticket]=SUP-1187
mehen.dev
A credential vault and proxy for AI agents. Agents get capabilities, never secrets.
Mehen makes the call for the agent, returns the response scrubbed, and asks a human before anything risky goes out.
$ curl -fsSL mehen.dev/install | sh
$ mehen ls
name provider bound to mode
openai-personal OpenAI api.openai.com placeholder
stripe-live Stripe api.stripe.com relay
gmail-alex Google gmail.googleapis.com oauth
$ mehen run -- bun dev
mehen OPENAI_API_KEY=mehen://openai-personal/4f91c2
mehen allowed POST api.openai.com/v1/chat/completions 212 ms
mehen held POST api.stripe.com/v1/refunds waiting for Alex
mehen allowed POST api.stripe.com/v1/refunds approved with passkey
The problem
Once the model has seen the value, masking it in the output changes nothing. It is already in too many places.
.envRead by every tool with file access.the transcriptStored with the provider, forever.the logsRequest bodies, tool results, stack traces.shell historyOne export, kept for years.prompt injectionA page that asks nicely gets the key.How it works
The agent needs a key, so it asks. A human gets an intake link and types the key into the vault's page, never into the chat.
The vault makes the call and returns the response, scrubbed. Or mehen run hands your program a placeholder that becomes the real key only on the way to the right host.
OPENAI_API_KEY=mehen://openai-personal/4f91c2
STRIPE_SECRET_KEY=mehen://stripe-live/b07e1a
api.openai.com and api.stripe.com only.A write, or anything in production, waits for a person who sees the exact request and approves it with a passkey. Agents cannot approve.
Approvals
The exact request, the binding it runs under, and why it waited. One passkey tap, and the agent carries on. Nothing is sent until then.
payment_intent=pi_3Mt7XoLkdIwHu7ix0kjN2wKg
amount=4200
reason=requested_by_customer
metadata[ticket]=SUP-1187
The vault
Each credential is bound to hosts, paths and methods. Sent anywhere else, it is refused, and the refusal says the fix.
| Name | Provider | Bound hosts | Methods | Mode |
|---|---|---|---|---|
| openai-personal | OpenAI | api.openai.com | POST /v1/chat/* POST /v1/embeddings | placeholder |
| stripe-live | Stripe | api.stripe.com | GET /v1/* POST needs approval | relay |
| gmail-alex | gmail.googleapis.com | read send | oauth | |
| github-ci | GitHub | api.github.com | GET /repos/* POST /repos/*/statuses | relay |
| Time | Agent | Credential | Destination | Decision |
|---|---|---|---|---|
| 23:41:07 | claude-code@studio | openai-personal | POST api.openai.com/v1/chat/completions | Allowed |
| 23:40:52 | cron/nightly-report | stripe-live | GET api.stripe.com/v1/balance | Allowed |
| 23:39:18 | codex@laptop | stripe-live | POST api.stripe.com/v1/refunds | Held for approvalwaiting for Alex's passkey |
| 23:37:03 | claude-code@studio | openai-personal | POST api.openai.com/v1/files | Refusedoutside the binding. Fix: mehen.dev/a/7KQ2 |
| 23:36:21 | agent/inbox | gmail-alex | gmail send | Allowed |
| 23:35:40 | codex@laptop | openai-personal | POST evil-proxy.example/v1/chat | Refusednot a bound host. Nothing was sent. Fix: ask a human at mehen.dev/a/7KQ2 |
The CLI
Run your program under mehen run and SDKs work unchanged. Over MCP, the agent names the capability, never the value.
$ mehen request stripe
mehen intake link sent to Alex: mehen.dev/i/7h3q expires in 15 min
mehen the key is typed into the vault's page, never into this chat
$ mehen connect gmail
mehen opening Google sign-in for Alex
mehen connected gmail-alex read, send tokens stay in the vault
$ mehen run -- bun dev
mehen OPENAI_API_KEY=mehen://openai-personal/4f91c2
ready on http://localhost:3000
mehen allowed POST api.openai.com/v1/chat/completions 212 ms
mehen refused openai-personal is bound to api.openai.com. Nothing was sent.
fix: ask a human at mehen.dev/a/7KQ2
// the agent names the capability, never the value
{
"tool": "vault_fetch",
"arguments": {
"item": "openai-personal",
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"body": { "model": "gpt-5", "input": "Summarise the log" }
}
}
// the response comes back scrubbed
{
"status": 200, "scrubbed": true,
"body": { "output": "Six crossings, two refused, one waiting." }
}
$ mehen enrol
mehen this machine is quiet-heron-lamp
mehen match the words at mehen.dev/machines to approve it
Everything in the box
The vault makes the HTTP call and returns the response, scrubbed.
mehen run gives programs a stand-in that becomes real only at the right host.
Hosts, paths and methods per credential. Anywhere else is refused.
Risky calls wait for a passkey. Agents cannot approve.
The human types the key into the vault's page, never into the chat.
"Connect my Gmail": tokens go to the vault, the agent gets the capability.
Each device holds its own key, approved by matching fingerprint words.
Who, which credential, where, when, what was decided. Every time.
No dead ends: each "no" carries the link to open or the command to run.
Works with
mehen CLIInstall in a minute. Free for one person; a link covers the rest.
The serpent coils round the sun, and nothing gets past.