Pasted into a chat
A key dropped into a chat window to "just get unblocked" now lives in someone's model logs, forever, searchable.
Blind keys for fast agents. Store API keys encrypted, hand agents scoped tokens, and let every call run through a proxy that injects the secret server-side.
The problem
A key dropped into a chat window to "just get unblocked" now lives in someone's model logs, forever, searchable.
One printenv or cat .env inside an agent's tool call and the secret is sitting in the transcript.
An agent that "helpfully" writes a config file commits your key to the repo — then to every clone, fork, and log.
The one-line answer: the key never leaves the Worker. The agent gets a result, not a string.
How it works
1 · Agent
Bearer tv_live_…Scoped token. No key on disk, in env, or in chat.
2 · KeyVeil Worker
3 · Provider
OpenAI, GitHub…
Agent sees: result JSON only.
Before / after
Before — agent can read this
OPENAI_API_KEY=sk-proj-abc123... # agent can read this
$ printenv | grep OPENAI # it's in the transcript nowAfter — agent gets a result, not a string
curl -H "Authorization: Bearer $VEIL_AGENT_TOKEN" \
-d '{"model":"gpt-4o-mini","input":"summarize this"}' \
"https://keyveil-api.parithosh.workers.dev/v1/proxy/openai/chat"Storage
Keys encrypted with AES-256-GCM before they touch the database, wrapped by per-account keys. Dashboards and APIs list names only — values are never returned by default.
Control
Agent tokens limited to named actions, IP ranges, and expiry dates. Each token is shown once; only its hash is stored. Revocation lands on the very next request.
Execution
Calls arrive with a token, never a key. The gateway checks scope and identity, injects the secret, and returns only the result. Reading a raw value needs the separate secrets:reveal scope — and gets logged.
One blind call, simulated in your browser. Nothing leaves this page.
CLI
npm i -g keyveilCreate an agent key in the dashboard, then store it (mode 600).
$ keyveil login --token tv_live_…
saved to ~/.keyveil/config.json (api: https://keyveil-api.parithosh.workers.dev)
$ keyveil login --status
ok as u_498975c1719ba1d6903734 via agent (key: opencode)
scopes: openai:chat, github:create-repo
api: https://keyveil-api.parithosh.workers.devList shows masked previews and last use — never values.
$ keyveil secrets add OPENAI_API_KEY
Value for OPENAI_API_KEY (hidden):
stored OPENAI_API_KEY (encrypted server-side)
$ keyveil secrets list
name preview updated_at last_used
GITHUB_TOKEN ghp_…9f2c 2026-09-10T08:12Z 2026-09-16T12:31Z
OPENAI_API_KEY sk-p…f4a2 2026-09-12T19:44Z 2026-09-16T12:33ZThe full token prints once. Only its hash is stored.
$ keyveil keys create --name opencode \
--scopes openai:chat,github:create-repo --ttl 90
created key k_9f2c41ab (tv_live_9f2c41ab) scopes=openai:chat,github:create-repo
SAVE THIS TOKEN NOW — it is shown only once:
tv_live_9f2c41ab7d…Server injects the secret; you only see the result.
$ keyveil proxy github create-repo -d '{"name":"demo","isPublic":true}'
{
"url": "https://github.com/you/demo",
"full_name": "you/demo",
"private": false
}Agents
Paste this block into your session. Verbatim from SKILL.md.
You have access to a personal blind secrets gateway. Do NOT ask the user for API keys.
Base: $API_BASE_URL (default https://keyveil-api.parithosh.workers.dev)
Auth: Authorization: Bearer $VEIL_AGENT_TOKEN (already in env, never print it)
Rules:
1. Prefer blind proxy. POST /v1/proxy/<provider>/<action> with JSON args. Use the returned result only.
- GitHub repo: POST /v1/proxy/github/create-repo {"name":"foo","isPublic":true}
- OpenAI: POST /v1/proxy/openai/chat {"model":"gpt-4o-mini","input":"..."}
2. Discover first: GET /v1/tools lists what this user allows.
3. Never echo / cat / printenv secrets. Never put secret values in chat, logs, or git. Raw values are only available via GET /v1/secrets/:name with an explicit secrets:reveal scope (terminal use, audit-logged) — proxy calls must never request or expect them.
4. On 401/403/429 stop and say: "veil auth failed / scope denied / IP not allowlisted / rate limited" — do not retry with a different key, do not dump headers.
5. Every proxy call is audit-logged per user. Keep args minimal.
6. Terminal management (human's own terminal): the keyveil CLI (login, secrets, keys, proxy) talks to the same API with $VEIL_AGENT_TOKEN.Same contract, dropped into CLAUDE.md.
# KeyVeil — blind secrets gateway (paste into CLAUDE.md)
You have access to a personal blind secrets gateway. Do NOT ask the user for API keys.
- Base: $API_BASE_URL (default https://keyveil-api.parithosh.workers.dev)
- Auth: Authorization: Bearer $VEIL_AGENT_TOKEN (already in env, never print it)
- Prefer blind proxy: POST /v1/proxy/<provider>/<action> with JSON args; use the returned result only.
- GitHub repo: POST /v1/proxy/github/create-repo {"name":"foo","isPublic":true}
- OpenAI: POST /v1/proxy/openai/chat {"model":"gpt-4o-mini","input":"..."}
- Discover first: GET /v1/tools lists what this user allows.
- Never echo / cat / printenv secrets. Never put secret values in chat, logs, or git. Raw values need secrets:reveal scope (terminal use, audit-logged) — proxy calls must never request them.
- On 401/403/429 stop and report "veil auth failed / scope denied / IP not allowlisted / rate limited". Do not retry with a different key, do not dump headers.Same contract as a .cursorrules entry.
# .cursorrules — KeyVeil blind secrets gateway
- Never ask the user for API keys. A blind secrets gateway is available.
- Base: $API_BASE_URL (default https://keyveil-api.parithosh.workers.dev); Auth: Bearer $VEIL_AGENT_TOKEN (env, never print).
- Use blind proxy only: POST /v1/proxy/github/create-repo {"name","isPublic"} or POST /v1/proxy/openai/chat {"model","input"}. Consume the result JSON; the secret itself is never visible.
- Discover via GET /v1/tools before assuming an action exists.
- Forbidden: echo/cat/printenv of secrets; secrets in chat, logs, git, or generated config files.
- On 401/403/429: stop, report the mapped message (auth failed / scope denied / IP not allowlisted / rate limited), do not retry with another key.Expose the two proxy tools as functions. Raw values are never a tool.
// Tool definitions for any function-calling agent.
// Auth: Authorization: Bearer $VEIL_AGENT_TOKEN. Base: https://keyveil-api.parithosh.workers.dev
{
"tools": [
{
"name": "keyveil_proxy",
"description": "Run an allowed provider action blind. The secret is injected server-side; only the result returns.",
"input_schema": {
"provider": "github | openai",
"action": "create-repo | chat",
"args": "object (e.g. {\"name\": \"demo\", \"isPublic\": true})"
},
"endpoint": "POST /v1/proxy/{provider}/{action}"
},
{
"name": "keyveil_tools",
"description": "List what this user allows. Call first.",
"endpoint": "GET /v1/tools"
}
],
"never": ["request secret values", "echo/printenv/cat secrets", "retry 401/403 with another key"]
}GET /v1/toolsPublic to any bearer token. Agents call it first to learn exactly what this user allows — no guessing, no prompt archaeology.
| Tool | Input |
|---|---|
github/create-repo | { name, isPublic } |
openai/chat | { model, input } |
Agent builders: these are stable. Parse them, don't retry around them.
| Status | Meaning | Do |
|---|---|---|
401 | Missing, invalid, or expired token/grant | Stop, ask the human to re-issue |
403 | Scope denied (need names it) or IP not allowlisted | Stop — never try a different key |
429 | Over 60 req/min on this key (Retry-After: 60) | Back off, then continue |
404 | Unknown tool — response lists the tools you do have | Re-discover via /v1/tools |
Providers
Live — chat completions
POST /v1/proxy/openai/chat
Live — create repos
POST /v1/proxy/github/create-repo
Planned
Messages API through the same blind path.
Planned
Scoped payment intents, never the secret key.
We add what agents actually spend.
Security
AES-256-GCM. Each account gets its own data key, wrapped by a master KEK held only as a Cloudflare secret (ENCRYPTION_KEK). KV holds ciphertext blobs (u:<user>:s:<NAME>); D1 holds names, masked hints, and audit rows — never plaintext.
Agent tokens (tv_live_…) print once; only a SHA-256 hash is stored. Expiry is mandatory thinking (default 90 days), IP allowlists are per key, and revocation lands on the very next request. Proxy calls are capped at 60 req/min per key.
Scopes like openai:chat or github:create-repo bind each token to named actions. Raw-value reads live behind their own scope, granted only to keys in your terminal.
Google sign-in, opaque server-side sessions, one-click logout. Tokens carry expiries and optional IP allowlists; revocation is a single call.
Every store, reveal, proxy call, and revocation records who acted, what ran, and from where — values never included. Read yours any time with audit:read.
KeyVeil removes the secret from the agent's world — it does not remove spend: a compromised agent token can still use your quota within its scopes until you revoke it. Scope stored keys minimally at the provider too, and treat Cloudflare (which operates the infra) as in the trust path. Full notes: docs/SECURITY.md.
Audit
Real rows from this site's production audit log — timestamp, actor, action, result. Values are never recorded. See your own live in the dashboard.

FAQ
Because your agent can read them. One printenv in a tool call and the key is in the transcript, the logs, and possibly the model's training-adjacent data. KeyVeil keeps the key in the Worker and hands the agent a result — there is nothing in the environment worth stealing.
Those are excellent at getting a secret to a process — which then holds it in memory, env, or a file your agent can also read. KeyVeil never hands the secret to the process at all: it injects the key server-side at call time and returns only the upstream result.
Yes — loudly. It's one Worker plus D1 plus KV, no other infrastructure. Clone the repo and:
wrangler d1 create keyveil-db
wrangler kv:namespace create VEIL_KV
wrangler d1 execute keyveil-db --file=db/schema.sql --remote
wrangler secret put ENCRYPTION_KEK # + GOOGLE_CLIENT_ID/SECRET, SESSION_SECRET
npm run deploy:api && npm run deploy:webFull steps: docs/SETUP.md.
Values arrive over TLS, are encrypted with AES-256-GCM under your per-account data key, and stored as ciphertext in KV. The master KEK lives only as a Cloudflare Worker secret. D1 keeps names, masked previews (sk-p…f4a2), and audit rows — never values, never in logs. Honest caveat: Cloudflare operates the infrastructure, so it sits in the trust path; what KeyVeil guarantees is that agents, transcripts, repos, and logs never see your keys.
Free while in beta — hosted, no card. If you outgrow that or want full custody, self-host on Cloudflare's free tier in about ten minutes (above). Paid tiers only arrive with SLAs and team features.
Pricing
Hosted KeyVeil costs nothing right now. When you want custody, the whole gateway is a Worker, a D1, and a KV you can run yourself.