{"id":51145571,"url":"https://github.com/askalf/keeper","last_synced_at":"2026-06-26T02:30:29.254Z","repository":{"id":365113483,"uuid":"1270605364","full_name":"askalf/keeper","owner":"askalf","description":"own your agent secrets — an encrypted vault that hands agents scoped, short-lived, single-use leases instead of raw keys, and audits every access. Completes the agent-security stack (warden · canon · keeper). Part of Own Your Stack.","archived":false,"fork":false,"pushed_at":"2026-06-15T23:13:57.000Z","size":42,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-16T00:19:44.961Z","etag":null,"topics":["agent-security","ai-agents","credentials","least-privilege","own-your-stack","secrets","security","vault"],"latest_commit_sha":null,"homepage":"https://sprayberrylabs.com/own-your-stack","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/askalf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-15T22:01:39.000Z","updated_at":"2026-06-15T23:14:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/askalf/keeper","commit_stats":null,"previous_names":["askalf/keeper"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/askalf/keeper","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/askalf%2Fkeeper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/askalf%2Fkeeper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/askalf%2Fkeeper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/askalf%2Fkeeper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/askalf","download_url":"https://codeload.github.com/askalf/keeper/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/askalf%2Fkeeper/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34801014,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-26T02:00:06.560Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-security","ai-agents","credentials","least-privilege","own-your-stack","secrets","security","vault"],"created_at":"2026-06-26T02:30:23.805Z","updated_at":"2026-06-26T02:30:29.224Z","avatar_url":"https://github.com/askalf.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# keeper\n\n\u003e _keeper — **own your agent secrets**. An encrypted vault that hands agents scoped, short-lived, single-use leases instead of raw keys. Part of **[Own Your Stack](https://github.com/askalf)** — own your AI infrastructure instead of renting it by the token._\n\nAgents need credentials — API keys, tokens, passwords — to do anything useful. Today they get them the worst possible way: a long-lived key stuffed into an environment variable or, worse, into the prompt. OpenClaw leaked the keys of ~135k exposed instances exactly this way. A key in the model's context is a key in every log, every trace, and every place a poisoned tool can read.\n\n**keeper holds the keys so the agent doesn't.** The raw secret stays encrypted in the vault; the agent only ever holds a **lease** — a scoped, short-lived, use-limited handle — and the real key is revealed **only at the egress point**, only while the lease is valid:\n\n- **vault** — secrets encrypted at rest (AES-256-GCM, key in `~/.keeper`, `0600`). Never a plaintext env var, never in a prompt.\n- **lease** — `grant` mints an opaque handle bound to a **TTL**, a **use count**, and (optionally) a **destination host**. The agent's context holds the lease, not the secret.\n- **redeem** — exchange a lease for the secret at the point of use, *iff* it's still valid (not expired, uses remaining, host in scope). A denial is audited and never burns a use.\n- **audit** — every grant / redeem / deny / revoke is **hash-chained** (shared with [warden](https://github.com/askalf/warden)) — editing or deleting a past access breaks `keeper audit --verify`.\n\nCompletes the agent-security stack: **warden** contains the call · **canon** vets the tool · **keeper** holds the keys.\n\n## Quick start\n\n```bash\necho \"sk-live-…\" | keeper add OPENAI_API_KEY          # stored encrypted\n\nLEASE=$(keeper grant OPENAI_API_KEY --ttl 300 --uses 1 --host api.openai.com)\n# → the agent gets $LEASE — not the key\n\n# at the egress point, run the call with the key in the child's env only:\nkeeper exec \"$LEASE\" --as OPENAI_API_KEY -- \\\n  curl https://api.openai.com/v1/models -H \"Authorization: Bearer $OPENAI_API_KEY\"\n\nkeeper audit --verify                                 # tamper-evident access log\n```\n\nThe agent dispatched `keeper exec \u003clease\u003e …`; the key was decrypted inside keeper and handed to the subprocess's environment — it never entered the agent's context, stdout, or logs. Run the whole story: `npm run demo`.\n\n## Egress broker — the agent just swaps a base URL\n\nRun the broker and the agent needs no key, no `exec`, no redeem — only a base-URL swap:\n\n```bash\n# bind a lease to ONE upstream, how to inject, which endpoints, and a rate cap\nLEASE=$(keeper grant OPENAI_API_KEY \\\n  --upstream https://api.openai.com --inject bearer \\\n  --paths \"/v1/chat/*,/v1/models\" --rate 60 --ttl 600 --uses 100)\nkeeper broker --port 8771 \u0026\n```\n\nPoint the agent's client at the broker:\n\n```js\nconst openai = new OpenAI({ baseURL: `http://127.0.0.1:8771/${LEASE}`, apiKey: 'unused' });\nawait openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [/* … */] });\n```\n\nFor each call the broker redeems the lease (atomic + audited), makes the **real** upstream request itself with the secret injected (`Authorization: Bearer …`), and streams the response back. The key is injected at the network boundary — it never enters the agent's context, env, or logs. And because the lease is **bound to one upstream**, the secret can only ever go to that host; the agent can't redirect it. `--inject`: `bearer` (default) · `x-api-key` (Anthropic) · `Header-Name` (custom).\n\n**Scope it down further:**\n- `--paths \"/v1/chat/*,/v1/models\"` — restrict the lease to specific endpoints (glob; a chat lease can't reach billing or admin).\n- `--rate 60` — cap it at 60 requests/min.\n\nBoth are enforced **before** the secret is redeemed — an out-of-scope or over-rate request gets `403` / `429`, consumes no use, and is audited.\n\n\u003e **Windows / Git Bash:** MSYS auto-rewrites an argument that looks like a Unix absolute path, so a bare `--paths \"/v1/models\"` reaches keeper as `C:/Program Files/Git/v1/models` and silently never matches (every call then `403`s on `path`). A comma-list like `\"/v1/chat/*,/v1/models\"` is left alone, which is why it works. Prefix the run with `MSYS_NO_PATHCONV=1` (use drive-letter paths for any file args), or call keeper from PowerShell/cmd. Not a keeper bug — it mangles the arg before keeper sees it.\n\n## Why a lease, not the key\n\n| | a raw key in env / prompt | a keeper lease |\n|---|---|---|\n| in the model's context | **yes** — leaks to logs, traces, poisoned tools | no — only an opaque handle |\n| lifetime | until you rotate it | seconds (TTL) |\n| blast radius | every call, every host | one use, one host |\n| revocable | rotate everywhere | `keeper revoke \u003clease\u003e` |\n| audited | no | every access, tamper-evident |\n\n## Dispatching to a fleet\n\nA platform that runs agents on remote devices shouldn't ship a long-lived key to each one — that's how OpenClaw leaked ~135k of them. Ship a **lease** instead:\n\n- the **control plane** stores the secret in keeper and grants a scoped, short-lived lease per task (`--upstream`, `--paths`, `--rate`, `--ttl`, `--uses`);\n- the **device** receives only the lease id and runs through `keeper broker` — the key is injected at egress, never written to the device;\n- a compromised device yields a *lease* (scoped, expiring, revocable), not a key. `keeper revoke \u003clease\u003e` kills it instantly — no production-key rotation.\n\nSee it end to end: `npm run demo:platform`.\n\n## Security model\n\nkeeper is a vault, so its own security is the point:\n\n- **Encrypted at rest** — AES-256-GCM, with the secret *name* bound in as AAD, so a ciphertext can't be swapped between names.\n- **Master key** — three options, in priority order:\n  - `KEEPER_PASSPHRASE` — derived with **scrypt**; never on disk (only a salt is).\n  - `KEEPER_KEYCHAIN=1` — held by the **OS keychain**: macOS Keychain · Linux Secret Service · Windows DPAPI (user scope). Never plaintext on disk, and it **fails closed** if no keychain is available (no silent downgrade). `keeper keychain` shows the active backend.\n  - else — a random key file in `~/.keeper` (`0600` + a restrictive ACL on Windows).\n\n  Use the passphrase or the keychain for anything that matters.\n- **Leases are bearer tokens** — only `sha256(id)` is stored; the raw id is returned once, to you. Reading `leases.json` therefore can't redeem anything.\n- **Single-use is atomic** — redeem is a check-and-consume under a cross-process lock, so concurrent redeems can't double-spend a one-use lease.\n- **Fail-closed** — a tampered, swapped, or wrong-key entry returns null and denies; it never throws or leaks garbage.\n- **Tamper-evident audit** — every access is hash-chained (shared with warden) and logged by lease *fingerprint*, never the raw id.\n\nWhat it is **not**: a defense against an attacker who already has your passphrase / master key or full process memory — at that point they have the vault. keeper shrinks the *agent's* exposure (a lease, not the key; short-lived; scoped; audited); it doesn't replace OS-level isolation.\n\n## Commands\n\n```\nkeeper add \u003cname\u003e                  store a secret (stdin, or --value=)\nkeeper ls                          list secret names (never values)\nkeeper grant \u003cname\u003e [--ttl --uses --host --upstream --inject]   mint a lease\nkeeper redeem \u003clease\u003e [--host]     exchange a valid lease for the secret (egress side)\nkeeper exec \u003clease\u003e --as \u003cENV\u003e -- \u003ccmd...\u003e  redeem + run \u003ccmd\u003e with the secret in its env only\nkeeper broker [--port 8771]        egress-injection proxy (base-URL swap, zero key in the agent)\nkeeper leases · keeper revoke \u003clease\u003e · keeper rm \u003cname\u003e\nkeeper audit [--verify]            the access log, optionally chain-verified\nkeeper keychain                    master-key backend status (KEEPER_KEYCHAIN=1 to use the OS keychain)\n```\n\n## Library\n\n```js\nimport { addSecret, grant, redeem } from '@askalf/keeper';\n\naddSecret('STRIPE_KEY', process.env.STRIPE_KEY);\nconst lease = grant('STRIPE_KEY', { ttlS: 60, uses: 1, host: 'api.stripe.com' });\n// hand `lease.id` to the agent; at egress:\nconst { ok, value } = redeem(lease.id, { host: 'api.stripe.com' });\n```\n\n## The agent-security stack\n\nThree composable layers, one defense: **[warden](https://github.com/askalf/warden)** contains the call · **[canon](https://github.com/askalf/canon)** vets the tool · **[keeper](https://github.com/askalf/keeper)** holds the keys *(you are here)*. Run all three together → **[agent-security-stack](https://github.com/askalf/agent-security-stack)**.\n\n---\nPart of **[Own Your Stack](https://github.com/askalf)** — own your AI infrastructure instead of renting it. Built by Thomas Sprayberry.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faskalf%2Fkeeper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faskalf%2Fkeeper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faskalf%2Fkeeper/lists"}