A trust boundary for LLM agents. Your agent never holds your real credentials.
Agent-Keyhole runs a trusted sidecar process that holds your real API keys (from OS keychain or encrypted vault) while your agent process only sees dummy placeholders. All upstream responses are automatically scrubbed so credentials never leak back to the agent, even if the API echoes them.
npm install agent-keyhole
npx keyhole init
npx keyhole add github # stores your token in OS keychainimport { createKeyhole } from 'agent-keyhole';
const kh = await createKeyhole();
const github = kh.createClient('github');
const res = await github('/repos/octocat/hello-world');
const repo = await res.json();
// Your real token was injected by the sidecar.
// The agent process never saw it.
await kh.shutdown();AGENT PROCESS (untrusted) KEYHOLE SIDECAR (trusted)
βββ HTTP Interceptor βββ Secret Store (keychain or vault)
β patches http/https/fetch βββ Request Builder (injects real creds)
βββ Keyhole Client βββ Response Masker (4-layer redaction)
β fetch-like per-service API βββ Audit Logger
βββ IPC Client ββββ Unix Socket βββ βββ OTT Authentication
The sidecar is a separate OS process. Memory isolation is enforced even if the agent runs arbitrary code. Communication uses Unix domain sockets (0600 permissions) with a one-time token on every request.
Response masking is zero-config. The 4-layer pipeline:
- Header scrub β strips
Authorization,Set-Cookie,X-API-Key, etc. - Known-secret scan β detects your exact secrets (plain, base64, URL-encoded) via structural JSON walk or raw text fallback
- Heuristic engine β catches unknown credentials (rotated keys, OAuth grants) using dual-signal detection: suspicious key name + high Shannon entropy
- User overrides β optional regex patterns and JSON paths for edge cases
Layers 2 and 3 require no configuration. They work out of the box.
The agent only sees a scoped fetch-like function. No URLs, no domains, no headers.
const kh = await createKeyhole();
const github = kh.createClient('github');
const res = await github('/user/repos', {
method: 'POST',
body: JSON.stringify({ name: 'new-repo', private: true })
});Patches http.request, https.request, and globalThis.fetch. Existing code and SDKs work unchanged.
const kh = await createKeyhole({ autoPatch: true });
// If you have a .env (post-migration): SDKs read placeholders automatically.
// If not: generate placeholder env vars from sdk_env mappings in keyhole.yaml:
// const env = kh.getSafeEnv();
// Object.assign(process.env, env);
const res = await fetch('https://api.github.com/user/repos');
// Real token injected by the sidecar β agent process never sees it.On servers without an OS keychain, use the encrypted vault:
npx keyhole vault create # AES-256-GCM with scrypt KDF
npx keyhole vault add github # prompts for secret + passphraseconst kh = await createKeyhole({
store: 'vault',
vaultPassphrase: process.env.VAULT_PASSPHRASE
});If no passphrase is provided, the sidecar enters PENDING_UNLOCK state. Unlock at runtime:
const kh = await createKeyhole({ store: 'vault' });
// kh.state === 'pending_unlock'
await kh.unlock(passphrase);
// kh.state === 'ready'Services are defined in keyhole.yaml:
services:
github:
domains:
- api.github.com
auth:
type: bearer # bearer | basic | query_param | custom_header
secret_ref: github-token
headers:
Accept: application/vnd.github+jsonIf you ran npx keyhole migrate, your .env already has placeholder values.
SDKs read these automatically β just configure service domains and auth in keyhole.yaml.
No sdk_env or placeholder fields needed.
If you don't have a .env file, use sdk_env to generate placeholder env vars:
sdk_env:
GITHUB_TOKEN: "{{placeholder}}"const kh = await createKeyhole({ autoPatch: true });
const env = kh.getSafeEnv(); // reads sdk_env mappings from yaml
Object.assign(process.env, env); // SDKs see placeholder valuesIf an SDK validates key format (e.g., OpenAI checks for sk- prefix), set a format-aware placeholder:
placeholder: "sk-keyhole-000000000000000000000000000000000000000000000000"Response masking is automatic. For edge cases, add optional overrides:
response_masking:
patterns:
- "ghp_[A-Za-z0-9_]{36}"
json_paths:
- "$.credentials.access_token"
heuristic:
enabled: true # default
min_length: 16 # default
min_entropy: 3.5 # default
additional_key_names: [my_key] # merged with built-in listMulti-agent access control restricts which agents can reach which services:
agents:
content-bot:
services: [github, openai]
deploy-bot:
services: [github]| Command | Description |
|---|---|
npx keyhole init |
Create a keyhole.yaml config file |
npx keyhole add <service> |
Store a secret in the OS keychain |
npx keyhole remove <service> |
Remove a secret from the OS keychain |
npx keyhole list |
List configured services and secret status |
npx keyhole test [service] |
Test connectivity through the sidecar |
npx keyhole vault create |
Create an encrypted vault file |
npx keyhole vault add <service> |
Add/update a secret in the vault |
npx keyhole vault remove <service> |
Remove a secret from the vault |
npx keyhole vault list |
List secrets in the vault (names only) |
All commands accept --config <path> to specify a custom config file.
What it protects against:
- Agent code reading API keys from memory or environment variables
- Credential leakage via API response echo (tokens returned in JSON bodies)
- Accidental logging of secrets by agent frameworks
- Redirect-based credential exfiltration (credentials stripped on untrusted domains)
What it does not protect against:
- A compromised OS or root-level attacker (the sidecar runs as the same user)
- Exfiltration of data obtained using the credentials (Keyhole controls auth, not authorization)
- Side-channel attacks on the sidecar process memory
MIT