|
2 | 2 |
|
3 | 3 | [](https://github.com/daslabhq/kern/actions/workflows/test.yml) |
4 | 4 |
|
5 | | -The agent credential manager. Your agents use APIs without ever seeing the keys. |
| 5 | +Encrypted secrets in git with a TypeScript SDK. Agents manage your credentials — you just approve. |
6 | 6 |
|
7 | 7 | ```bash |
8 | | -# store a credential |
9 | | -kern secret add GITHUB_TOKEN |
10 | | - |
11 | | -# start the proxy — agents connect via MCP |
12 | | -kern proxy |
| 8 | +npm install kern |
13 | 9 | ``` |
14 | 10 |
|
15 | | -An AI agent in Claude Code, Cursor, or any MCP client calls `kern_fetch("https://api.github.com/user/repos")`. Kern matches the URL to a stored credential, injects the token, makes the request, returns the response. **The credential never enters the LLM's context.** |
16 | | - |
17 | | -## The problem |
| 11 | +## 30-second setup |
18 | 12 |
|
19 | | -Today you paste API keys into `.env` files and hope the LLM doesn't leak them. It does — GitGuardian found 29 million hardcoded secrets in public GitHub in 2025. AI-assisted commits leak at double the base rate. |
20 | | - |
21 | | -Even if you're careful, every `process.env.GITHUB_TOKEN` in your agent's code means the raw credential flows through the LLM's context window. One prompt injection and it's gone. |
22 | | - |
23 | | -## How kern solves it |
| 13 | +```bash |
| 14 | +kern identity init # create your age keypair |
| 15 | +mkdir secrets |
| 16 | +kern identity pubkey >> secrets/.recipients |
| 17 | +kern secret add GITHUB_TOKEN # paste into terminal |
| 18 | +``` |
24 | 19 |
|
25 | | -Kern sits between your agent and the APIs it calls. Credentials are encrypted in an age vault. The proxy injects them at request time. The agent sees results, never keys. |
| 20 | +Your secret is age-encrypted, committed to git, useless to anyone not in `.recipients`. |
26 | 21 |
|
27 | | -``` |
28 | | -Agent: "fetch my GitHub repos" |
29 | | - ↓ |
30 | | -kern_fetch("https://api.github.com/user/repos") |
31 | | - ↓ |
32 | | -kern matches api.github.com → GITHUB_TOKEN |
33 | | -kern injects: Authorization: Bearer ghp_... |
34 | | -kern makes the HTTP request |
35 | | - ↓ |
36 | | -Agent gets: [{ "name": "my-repo", ... }] |
37 | | - (no token in the response, no token in context) |
38 | | -``` |
| 22 | +## Use from code |
39 | 23 |
|
40 | | -### Configuration |
| 24 | +```typescript |
| 25 | +import { loadIdentityFromHost, openVault } from "kern"; |
41 | 26 |
|
42 | | -```yaml |
43 | | -# kern.yaml |
44 | | -credentials: |
45 | | - - secret: github_token |
46 | | - match: "https://api.github.com/**" |
47 | | - header: "Authorization: Bearer {value}" |
48 | | - |
49 | | - - secret: openai_key |
50 | | - match: "https://api.openai.com/**" |
51 | | - header: "Authorization: Bearer {value}" |
52 | | - |
53 | | - - secret: slack_token |
54 | | - match: "https://slack.com/api/**" |
55 | | - header: "Authorization: Bearer {value}" |
56 | | - |
57 | | - - secret: postgres_url |
58 | | - match: "postgres://**" |
59 | | - connection: true |
| 27 | +const vault = openVault({ identity: await loadIdentityFromHost() }); |
| 28 | +const token = await vault.get("github_token"); |
60 | 29 | ``` |
61 | 30 |
|
62 | | -No per-API code. kern matches URLs and injects headers. Add a new service in one line. |
| 31 | +One dependency. Works on Bun, Node 20+, Deno. |
63 | 32 |
|
64 | | -## MCP server |
| 33 | +## Use from Claude Code |
65 | 34 |
|
66 | | -Add kern to Claude Code, Cursor, or any MCP client: |
| 35 | +Add kern as an MCP server: |
67 | 36 |
|
68 | 37 | ```json |
69 | 38 | { |
70 | 39 | "mcpServers": { |
71 | 40 | "kern": { |
72 | 41 | "command": "npx", |
73 | | - "args": ["kern", "proxy"] |
| 42 | + "args": ["kern", "mcp"] |
74 | 43 | } |
75 | 44 | } |
76 | 45 | } |
77 | 46 | ``` |
78 | 47 |
|
79 | | -Kern exposes two tools: |
| 48 | +Then: |
80 | 49 |
|
81 | | -- **`kern_services`** — list available services (names + URL patterns, no credentials) |
82 | | -- **`kern_fetch`** — make an authenticated HTTP request (kern injects the credential) |
| 50 | +``` |
| 51 | +You: "Add my GitHub and OpenAI keys to this project" |
83 | 52 |
|
84 | | -The agent sees what services are available, makes requests through kern, and gets results. Zero credentials in the conversation. |
| 53 | +Claude → kern_add("github_token") |
| 54 | + → browser opens kern's local form |
| 55 | + → you paste the token there (not in chat) |
| 56 | + → kern encrypts it into the vault |
| 57 | + → "github_token added ✓" |
85 | 58 |
|
86 | | -## Vault |
| 59 | +Claude → kern_add("openai_key") |
| 60 | + → same flow |
| 61 | + → "openai_key added ✓" |
87 | 62 |
|
88 | | -Credentials are stored in an [age](https://age-encryption.org/)-encrypted vault committed to git. Useless without the private key. One environment variable (`KERN_AGE_KEY`) unlocks everything in production. |
| 63 | +You: "What secrets do we have?" |
89 | 64 |
|
90 | | -``` |
91 | | -secrets/ |
92 | | -├── .recipients who can decrypt (age public keys) |
93 | | -├── github_token.age encrypted, safe to commit |
94 | | -├── openai_key.age |
95 | | -└── slack_token.age |
| 65 | +Claude → kern_list |
| 66 | + → "2 secrets: github_token, openai_key" |
96 | 67 | ``` |
97 | 68 |
|
98 | | -### CLI |
| 69 | +The credential goes: your browser → kern's local server → encrypted vault. The LLM never sees it. This uses MCP's URL-mode elicitation — the standard way to handle sensitive input. |
99 | 70 |
|
100 | | -```bash |
101 | | -kern identity init # create an age keypair |
102 | | -kern identity pubkey # print your public key |
| 71 | +## Local form server |
103 | 72 |
|
104 | | -kern secret add GITHUB_TOKEN # encrypt and store |
105 | | -kern secret list # show available secrets |
106 | | -kern secret rotate SLACK_TOKEN # replace a value |
107 | | -kern secret rewrap # re-encrypt for current recipients |
108 | | -``` |
| 73 | +For adding secrets through a browser form (standalone or via MCP): |
109 | 74 |
|
110 | | -### SDK |
| 75 | +```bash |
| 76 | +kern serve |
| 77 | +# → http://localhost:9271 |
| 78 | +# → open /add?name=GITHUB_TOKEN to add a secret |
| 79 | +``` |
111 | 80 |
|
112 | | -```ts |
113 | | -import { loadIdentityFromHost, openVault } from "kern"; |
| 81 | +Slick dark form, paste your credential, click encrypt. Done. |
114 | 82 |
|
115 | | -const id = await loadIdentityFromHost(); |
116 | | -const vault = openVault({ identity: id }); |
| 83 | +## CI — one secret unlocks everything |
117 | 84 |
|
118 | | -const key = await vault.get("github_token"); |
| 85 | +```yaml |
| 86 | +# GitHub Actions |
| 87 | +env: |
| 88 | + KERN_AGE_KEY: ${{ secrets.KERN_AGE_KEY }} |
119 | 89 |
|
120 | | -vault.list(); |
121 | | -await vault.put("new_key", "value"); |
122 | | -await vault.rewrap(); |
| 90 | +steps: |
| 91 | + - run: | |
| 92 | + TOKEN=$(npx kern secret get github_token) |
123 | 93 | ``` |
124 | 94 |
|
125 | | -## Why not SOPS / Vault / 1Password? |
| 95 | +One env var in CI. One `.recipients` entry for your CI's public key. Every secret available. |
126 | 96 |
|
127 | | -SOPS encrypts secrets but has no proxy — the agent still sees the credential at runtime. |
| 97 | +## Team — add a member in one command |
128 | 98 |
|
129 | | -HashiCorp Vault has a proxy but requires a server. |
| 99 | +```bash |
| 100 | +# Andre generates his keypair |
| 101 | +kern identity init |
| 102 | +kern identity pubkey # → age1abc... |
130 | 103 |
|
131 | | -1Password has agent support but it's SaaS — your credentials live on someone else's infrastructure. |
| 104 | +# You add him |
| 105 | +echo "age1abc..." >> secrets/.recipients |
| 106 | +kern secret rewrap # re-encrypts everything for the new set |
| 107 | +git commit -am "add Andre" |
132 | 108 |
|
133 | | -Kern is the only tool that does all three: encrypts secrets in git, proxies API calls without exposing credentials, and runs locally with zero infrastructure. |
| 109 | +# Andre clones, everything works |
| 110 | +``` |
134 | 111 |
|
135 | | -| | SOPS | Vault | 1Password | kern | |
136 | | -|---|---|---|---|---| |
137 | | -| Encrypted in git | yes | no | no | yes | |
138 | | -| No server required | yes | no | no | yes | |
139 | | -| Credential proxy | no | yes | yes | yes | |
140 | | -| TypeScript SDK | no | no | no | yes | |
141 | | -| Runs locally | yes | no | no | yes | |
142 | | -| Open source | yes | yes | no | yes | |
| 112 | +Remove someone: delete their key from `.recipients`, rewrap, rotate any secrets they had. |
143 | 113 |
|
144 | | -## How it works under the hood |
| 114 | +## CLI |
145 | 115 |
|
146 | | -1. **Identity** — every machine gets an age keypair (`kern identity init`). The public key goes in `.recipients`. Anyone in the list can decrypt. |
| 116 | +```bash |
| 117 | +kern identity init # create age keypair |
| 118 | +kern identity pubkey # print public key |
| 119 | +
|
| 120 | +kern secret add NAME # encrypt and store |
| 121 | +kern secret get NAME # decrypt to stdout |
| 122 | +kern secret list # show names (no values) |
| 123 | +kern secret rotate NAME # replace a value |
| 124 | +kern secret delete NAME # remove |
| 125 | +kern secret rewrap # re-encrypt for current recipients |
147 | 126 |
|
148 | | -2. **Vault** — secrets are age-encrypted files. `kern secret add` encrypts to all recipients. Committed to git, auditable in history, useless to non-recipients. |
| 127 | +kern mcp # start MCP server (Claude Code, Cursor) |
| 128 | +kern serve # start local form server |
| 129 | +``` |
149 | 130 |
|
150 | | -3. **Proxy** — `kern proxy` starts a local MCP server. When an agent calls `kern_fetch(url)`, kern: |
151 | | - - Matches the URL against `kern.yaml` patterns |
152 | | - - Decrypts the matching credential from the vault |
153 | | - - Injects it as an HTTP header |
154 | | - - Makes the request |
155 | | - - Returns the response (credential stripped) |
| 131 | +## How it works |
156 | 132 |
|
157 | | -4. **Signing** (remote mode) — when the vault and proxy are on different machines, kern signs authorization claims instead of proxying directly. The remote executor verifies the signature and injects the credential. Same age keypair, same trust model. |
| 133 | +Secrets are [age](https://age-encryption.org/)-encrypted files in a `secrets/` directory. Each file is one secret. A `.recipients` file lists the age public keys that can decrypt. |
| 134 | + |
| 135 | +``` |
| 136 | +secrets/ |
| 137 | +├── .recipients age1... (one per line) |
| 138 | +├── github_token.age |
| 139 | +├── openai_key.age |
| 140 | +└── prod/ |
| 141 | + ├── .recipients narrower set |
| 142 | + └── database_url.age |
| 143 | +``` |
| 144 | +
|
| 145 | +Standard age v1 format. Interoperable with the `age` CLI, `rage`, any age library. |
158 | 146 |
|
159 | 147 | ## Environment |
160 | 148 |
|
161 | 149 | | Variable | Purpose | Default | |
162 | 150 | |---|---|---| |
163 | | -| `KERN_AGE_KEY` | age private key for decryption | `~/.kern/key` | |
164 | | -| `KERN_VAULT_DIR` | vault root directory | `./secrets` | |
165 | | - |
166 | | -One env var in production. Local dev reads from `~/.kern/key`. |
167 | | - |
168 | | -## Install |
169 | | - |
170 | | -```bash |
171 | | -npm install kern |
172 | | -``` |
173 | | - |
174 | | -One dependency (`age-encryption`). ESM-only. Works on Bun, Node 20+, Deno. |
| 151 | +| `KERN_AGE_KEY` | age private key | `~/.kern/key` | |
| 152 | +| `KERN_VAULT_DIR` | vault directory | `./secrets` | |
175 | 153 |
|
176 | | -## Roadmap |
| 154 | +## Why kern |
177 | 155 |
|
178 | | -- **Audit ledger** — append-only log of every proxied request |
179 | | -- **Approval flow** — push notification before high-risk API calls |
180 | | -- **Remote signing** — kern on your phone signs, executor in the cloud acts |
181 | | -- **OS Keychain** — store the age key in macOS Keychain / Windows Credential Manager |
| 156 | +SOPS does encrypted secrets in git, but it's a Go CLI — you shell out to it. No TypeScript SDK, no MCP server, no local form. |
182 | 157 |
|
183 | | -## Testing |
| 158 | +Vault and 1Password have SDKs, but they need a server or SaaS account. |
184 | 159 |
|
185 | | -```bash |
186 | | -bun test ./test/smoke.ts |
187 | | -``` |
| 160 | +kern is the only tool that does encrypted-secrets-in-git with a native TypeScript SDK and agent integration. 424 lines of vault code, one dependency, MIT. |
188 | 161 |
|
189 | 162 | ## License |
190 | 163 |
|
|
0 commit comments