keychains
v0.0.19
Published
CLI for Keychains.dev — register machines, manage permissions, and make proxied API calls
Readme
keychains
CLI for Keychains.dev — secure credential delegation for AI agents.
Install
npm install -g keychainsQuick Start
# Just run it — no setup needed
keychains curl https://api.lifx.com/v1/lights/all \
-X GET \
-H "Authorization: Bearer {{LIFX_PERSONAL_ACCESS_TOKEN}}"That's it. On first run, Keychains automatically:
- Registers your machine (using your hostname)
- Creates a default wildcard permission
- Returns an authorization URL
Click the URL, sign in, connect your credentials, and re-run the command. After that, every call just works.
You never put real tokens in the command. Use template variables like {{OAUTH2_ACCESS_TOKEN}} or {{LIFX_PERSONAL_ACCESS_TOKEN}} and Keychains swaps them for real credentials at proxy time.
How it works
keychains curl <url>
│
├── Machine not registered? → auto-register using hostname
├── No permission? → auto-create wildcard (active immediately)
├── Make proxied API call
│
└── 403: credentials missing
└── "Authorize at: https://keychains.dev/add-permissions?machineId=...&scopes=..."
└── User clicks → signs in → machine linked → credentials connected
└── Re-run → works!Output Formats
Every command supports three output formats:
| Flag | Format | Use case |
|------|--------|----------|
| --human | Colored table (default) | Reading in terminal |
| --json | Raw JSON | Piping to jq, scripts, CI |
| --env | KEY=VALUE | Appending to .env files |
# Create a permission and append directly to .env
keychains permissions create --name "Dev" --env >> .env
# List permissions as JSON
keychains permissions ls --json
# Get your identity as env vars
keychains whoami --env >> .envInput Variables (Piped JSON)
Use {{$varname}} patterns in your URL, headers, or body to inject values from piped JSON input. This is different from Keychains credential templates ({{OAUTH2_ACCESS_TOKEN}}) which are resolved server-side.
# Pipe JSON into curl — variables get substituted before the request
echo '{"user": "alice", "email": "[email protected]"}' \
| keychains curl https://api.com/users/{{$user}} \
-X POST \
-d '{"user": "{{$user}}", "email": "{{$email}}"}'Supported accessor syntax
| Syntax | Resolves to |
|--------|-------------|
| {{$user}} | params.user |
| {{$user.name}} | params.user.name |
| {{$items[0].id}} | params.items[0].id |
| {{$items[0]['key']}} | params.items[0]["key"] |
| {{$items[0]["key"]}} | params.items[0]["key"] |
| {{$a.b[2].c['d']}} | params.a.b[2].c["d"] |
How it works
- Keychains detects
{{$...}}patterns in the URL, headers, and body - If variables are found, it reads JSON from stdin (Unix pipe)
- Each
{{$path}}is replaced with the resolved value from the piped JSON - The original command (with
{{$...}}patterns) is saved to history - When exported, the generated code is a function accepting
params
# Complex nested data
echo '{"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}' \
| keychains curl https://api.com/users/{{$users[0].id}} \
-X PATCH \
-d '{"name": "{{$users[0].name}}"}'
# Credential templates (server-side) can mix with input variables (client-side)
echo '{"repo": "my-org/my-repo"}' \
| keychains curl https://api.github.com/repos/{{$repo}}/issues \
-X POST \
-H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}" \
-d '{"title": "Bug in {{$repo}}"}'Commands
keychains machine — Machine Identity
Manage your machine's cryptographic identity (Ed25519 SSH keypair).
Most users don't need to register manually — machines are auto-registered on first use. Use these commands for explicit registration or management.
keychains machine register --name "my-laptop" # Register with a custom name
keychains machine register --name "ci" --server https://custom.keychains.dev
keychains machine status # Show machine status & permissions
keychains machine ls # List all machines on your account
keychains machine update --name "new-name" # Rename this machine
keychains machine rotate-keys # Rotate SSH keypair
keychains machine rm <machineId> # Remove a machine from your account
keychains machine rm <machineId> -f # Remove without confirmationkeychains permissions — Permission Requests
Create, list, revoke, and suspend permissions. Use -a to see all permissions across your account (like git branch -a).
# Create a wildcard permission (default)
keychains permissions create --name "Dev machine"
keychains permissions create --name "Dev" --env >> .env # Append to .env
# Create a scoped permission
keychains permissions create --name "GitHub read-only" \
--scopes "github::repo:read,github::read:org"
# List permissions
keychains permissions ls # This machine only
keychains permissions ls -a # All permissions on your account
keychains permissions ls --json # JSON output
keychains permissions ls --status active # Filter by status
# Revoke / suspend
keychains permissions rm <id> # Revoke a permission
keychains permissions rm <id> -f # Skip confirmation
keychains permissions suspend <id> # Suspend (requires re-auth)Aliases: keychains perm create, keychains perm ls, etc.
keychains delegates — Delegate Identities
Give remote systems (sandboxed agents, CI runners) limited access without sharing your machine keys.
# Create a wildcard delegate from a permission
keychains delegates add <permissionId> --wildcard --ttl 3600
# Create a scoped delegate
keychains delegates add <permissionId> --scopes "github::repo"
# List delegate-enabled permissions
keychains delegates ls
keychains delegates ls -a # All on account
keychains delegates ls <permissionId> # For a specific permission
# Revoke a delegate
keychains delegates rm <delegateId>
keychains delegates rm <delegateId> -f # Skip confirmation
# Get delegate credentials as env vars
keychains delegates add <permissionId> --wildcard --env >> .envAliases: keychains delegate add, keychains delegate ls, etc.
keychains connections — OAuth & API Key Connections
List and manage your connected services (OAuth providers and API keys). Interactive and non-interactive modes.
# List connections
keychains connections ls # Active connections
keychains connections ls --status all # All connections
keychains connections ls --provider github # Filter by provider
keychains connections ls --json # JSON output
keychains connections ls -i # Interactive mode
# Remove a connection
keychains connections rm github # By provider name
keychains connections rm <connectionId> # By connection ID
keychains connections rm -i # Interactive mode (select from list)
keychains connections rm github -f # Skip confirmationAliases: keychains conn ls, keychains conn rm, etc.
keychains curl — Proxied API Calls
Make API calls through the Keychains proxy. Credentials are injected automatically.
# Basic GET request
keychains curl https://api.github.com/user \
-H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}"
# POST with body
keychains curl https://slack.com/api/chat.postMessage \
-X POST \
-H "Authorization: Bearer {{SLACK_BOT_TOKEN}}" \
-H "Content-Type: application/json" \
-d '{"channel": "#general", "text": "Hello from keychains!"}'
# Use a specific permission
keychains curl https://api.stripe.com/v1/customers?limit=5 \
--permission pr_abc123 \
-H "Authorization: Bearer {{STRIPE_SECRET_KEY}}"Permission resolution order:
--permission <id>flagKEYCHAINS_PERMISSION_IDin.env(cwd)KEYCHAINS_PERMISSION_IDenvironment variable- First active wildcard permission on this machine
- Auto-create a default wildcard permission
Every curl command is automatically saved to local history.
keychains whoami — Identity Check
keychains whoami # Human-readable
keychains whoami --json # JSON output
keychains whoami --env >> .env # Append to .envkeychains history — Curl Command History
Browse, search, and manage your past keychains curl commands. History is stored locally in ~/.keychains/history.json (default: 100 entries).
# List recent history
keychains history
# Search history
keychains history "github"
keychains history "stripe" --limit 50
# Interactive browse (select entries to export/re-run)
keychains history -i
keychains history -i "gmail"
# Save a history entry as a template
keychains history save hist_abc123 --name "List GitHub repos"
# Clear history
keychains history clear
keychains history clear -f # Skip confirmationkeychains templates — Request Templates
Save frequently-used requests as named templates for quick re-use and code export.
# List templates
keychains templates ls
keychains templates ls "github" # Search templates
keychains templates ls --json # JSON output
# Show template details
keychains templates show "my-template"
keychains templates show tpl_abc123
# Delete a template
keychains templates rm tpl_abc123
keychains templates rm "my-template" -f # By name, skip confirmationAliases: keychains tpl ls, keychains tpl show, etc.
keychains export — Code Generation
Export any history entry or template as runnable code in your language of choice.
When the command contains input variables ({{$...}}), the generated code is a function that accepts a params dict. Use --name to set the function name.
# Export a standalone script (no input variables)
keychains export hist_abc123 --nodejs > index.js
keychains export hist_abc123 --ts > index.ts
keychains export hist_abc123 --python > script.py
keychains export hist_abc123 --go > main.go
keychains export hist_abc123 --curl > request.sh
# Export a function (when input variables are present)
keychains export hist_abc123 --ts --name fetchUser > fetchUser.ts
keychains export tpl_abc123 --python --name fetch_user > fetch_user.py
keychains export hist_abc123 --go --name FetchUser > fetch_user.go
# Export from template (by ID or name)
keychains export tpl_abc123 --nodejs > index.js
keychains export "my-template" --python > script.py
# Use --lang flag
keychains export hist_abc123 --lang typescript > index.tsGenerated function example (TypeScript):
// keychains export hist_xxx --ts --name fetchUser > fetchUser.ts
interface InputParams {
user: any;
email: any;
}
export async function fetchUser(params: InputParams): Promise<{ status: number; data: any }> {
const url = `https://api.com/users/${params.user}`;
const headers: Record<string, string> = {};
headers["Content-Type"] = "application/json";
const body = `{"user": "${params.user}", "email": "${params.email}"}`;
const response = await fetch(url, { method: "POST", headers, body });
const text = await response.text();
let data: any;
try { data = JSON.parse(text); } catch { data = text; }
return { status: response.status, data };
}Supported languages:
| Flag | Language | File |
|------|----------|------|
| --nodejs / --node / --js / --javascript | Node.js (CommonJS) | .js |
| --typescript / --ts | TypeScript (fetch) | .ts |
| --python / --py | Python (requests) | .py |
| --go / --golang | Go (net/http) | .go |
| --curl | curl / bash function | .sh |
Credential Templates
You don't pass real secrets. Use {{VARIABLE_NAME}} placeholders in your headers. Keychains resolves them at the proxy level — the actual token never touches your machine.
OAuth Tokens
| Template | Resolves to |
|----------|-------------|
| {{OAUTH2_ACCESS_TOKEN}} | OAuth 2.0 access token (auto-refreshed) |
| {{OAUTH2_REFRESH_TOKEN}} | OAuth 2.0 refresh token |
| {{OAUTH1_TOKEN}} | OAuth 1.0 token |
| {{OAUTH1_TOKEN_SECRET}} | OAuth 1.0 token secret |
API Keys
Named API keys stored in the Keychains dashboard:
| Template | Example service |
|----------|-----------------|
| {{STRIPE_SECRET_KEY}} | Stripe |
| {{OPENAI_API_KEY}} | OpenAI |
| {{SENDGRID_API_KEY}} | SendGrid |
| {{LIFX_PERSONAL_ACCESS_TOKEN}} | LIFX |
Multi-Account
When you have multiple accounts for the same provider (e.g. a personal and a work Google account), Keychains lets you target a specific account by label.
keychains curl — X-Proxy-Account header
Pass the X-Proxy-Account header to route the request through a specific account:
# Use the "work" Google account
keychains curl https://gmail.googleapis.com/gmail/v1/users/me/messages \
-H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}" \
-H "X-Proxy-Account: work"
# Use the "personal" Google account
keychains curl https://gmail.googleapis.com/gmail/v1/users/me/messages \
-H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}" \
-H "X-Proxy-Account: personal"The X-Proxy-Account header is consumed by the proxy and never forwarded to the upstream API.
keychains connections — --account option
Filter connections by account label:
# List only "work" connections
keychains connections ls --account work
# Remove a specific account's connection
keychains connections rm google --account personalWhen multiple accounts exist for the same provider, connections ls shows the account label alongside the account identifier. If no --account filter is provided, all matching connections are shown and the rm command will prompt you to pick one.
Examples
# Toggle LIFX lights
keychains curl https://api.lifx.com/v1/lights/all/toggle \
-X POST \
-H "Authorization: Bearer {{LIFX_PERSONAL_ACCESS_TOKEN}}"
# List Stripe customers
keychains curl https://api.stripe.com/v1/customers?limit=5 \
-H "Authorization: Bearer {{STRIPE_SECRET_KEY}}"
# Send a Slack message
keychains curl https://slack.com/api/chat.postMessage \
-X POST \
-H "Authorization: Bearer {{SLACK_BOT_TOKEN}}" \
-H "Content-Type: application/json" \
-d '{"channel": "#general", "text": "Hello from keychains!"}'
# Create a GitHub issue
keychains curl https://api.github.com/repos/octocat/hello-world/issues \
-X POST \
-H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}" \
-H "Content-Type: application/json" \
-d '{"title": "Found a bug", "body": "Something is broken"}'Workflow Example
# 1. Register machine
keychains machine register --name "dev-laptop"
# 2. Create a scoped permission for a project
keychains permissions create --name "My Project" \
--scopes "github::repo,slack::chat:write" \
--env >> .env
# 3. Make API calls (permission auto-detected from .env)
keychains curl https://api.github.com/user \
-H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}"
# 4. Use input variables with piped JSON
echo '{"owner":"octocat","repo":"hello-world"}' \
| keychains curl https://api.github.com/repos/{{$owner}}/{{$repo}}/issues \
-X POST \
-H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}" \
-d '{"title":"Bug in {{$repo}}"}'
# 5. Export the command as a TypeScript function
keychains export hist_abc123 --ts --name createIssue > createIssue.ts
# 6. Give a CI runner limited access
keychains delegates add pr_abc123 --wildcard --ttl 3600 --env >> ci.env
# 7. Check your connections
keychains connections ls
keychains connections ls --json | jq '.[] | .provider'Using .env for Projects
Drop a .env in your project root:
KEYCHAINS_PERMISSION_ID=pr_abc123Every keychains curl in that directory will use it automatically.
Or create a permission directly into .env:
keychains permissions create --name "My Project" --env >> .envLocal Storage
All local data is stored in ~/.keychains/:
| File | Contents |
|------|----------|
| credentials.json | Machine ID, server URL, key paths, proxy URL |
| history.json | Curl command history (default limit: 100) |
| templates.json | Saved request templates |
| config.json | CLI configuration (history limit, etc.) |
| id_ed25519 | Ed25519 private key |
| id_ed25519.pub | Ed25519 public key |
Satellite Proxy
Deploy your own credential proxy so API secrets never leave your infrastructure. The proxy is open source (MIT) and deploys to Vercel in seconds.
# Deploy the proxy
git clone https://github.com/interagentic/keychains.dev_proxy && cd keychains.dev_proxy
npx vercel --prod# Connect to your account (runs a full end-to-end test first)
keychains proxy set https://your-proxy-abc.vercel.appThe proxy set command runs a 9-field end-to-end test before saving — it sends {{...}} placeholders through your proxy in headers, query params, and body, then verifies every value was resolved and forwarded correctly. If the test fails, the proxy URL is not saved.
Checking proxy server health and version...
200 keychains-proxy v0.1.0
Running end-to-end proxy test...
1. Initializing test on keychains.dev... OK (test a1b2c3d4...)
2. Sending proxied request through your-proxy-abc.vercel.app... OK
3. Verifying values on keychains.dev... OK
✓ header_oauth a1b2c3d4... ✓ query_oauth m3n4o5p6...
✓ header_apikey e5f6g7h8... ✓ query_apikey q7r8s9t0...
✓ header_custom i9j0k1l2... ✓ query_custom u1v2w3x4...
✓ body_oauth y5z6a7b8... ✓ body_apikey c9d0e1f2...
✓ body_custom g3h4i5j6...
✓ All 9 test values passed through the proxy correctly.
✓ User account <[email protected]> now uses remote proxy for all API calls.After that, your existing code works unchanged — all SDKs and CLI calls automatically route through your proxy.
# Other proxy commands
keychains proxy get # Show current proxy URL
keychains proxy test # Re-run the test on configured proxy
keychains proxy test <url> # Test a proxy without saving it
keychains proxy clear # Remove proxy (use keychains.dev direct)
keychains proxy set <url> --skip-test # Skip the e2e testYou can also manage the proxy URL from the keychains.dev dashboard.
How it works: Your proxy sends only the target URL, HTTP method, and {{...}} placeholder names to keychains.dev. Credentials are resolved server-side and returned — keychains.dev never sees your API traffic. Refresh tokens are never returned to the proxy. Read more.
All Commands Reference
| Command | Description |
|---------|-------------|
| Machine | |
| machine register --name <n> | Register this machine |
| machine status | Show machine status |
| machine ls | List machines on account |
| machine update --name <n> | Update machine name |
| machine rotate-keys | Rotate SSH keypair |
| machine rm <id> | Remove a machine |
| Permissions | |
| permissions create --name <n> | Create permission (wildcard default) |
| permissions ls | List machine permissions |
| permissions ls -a | List all account permissions |
| permissions rm <id> | Revoke a permission |
| permissions suspend <id> | Suspend a permission |
| Delegates | |
| delegates add <permId> | Create a delegate identity |
| delegates ls | List delegate-enabled permissions |
| delegates rm <id> | Revoke a delegate |
| Connections | |
| connections ls | List connections |
| connections ls --account <label> | Filter by account label |
| connections rm <provider> | Remove a connection |
| connections rm <provider> --account <label> | Remove by provider + account |
| Curl & History | |
| curl <url> | Proxied API call |
| history [query] | Browse curl history |
| history save <id> --name <n> | Save history as template |
| history clear | Clear history |
| Templates | |
| templates ls [query] | List templates |
| templates show <id> | Show template details |
| templates rm <id> | Delete a template |
| Export | |
| export <id> --nodejs | Export as Node.js |
| export <id> --ts | Export as TypeScript |
| export <id> --python | Export as Python |
| export <id> --go | Export as Go |
| export <id> --curl | Export as curl |
| Proxy | |
| proxy set <url> | Set satellite proxy (runs e2e test) |
| proxy get | Show current proxy URL |
| proxy test [url] | End-to-end proxy test |
| proxy clear | Remove satellite proxy |
| Identity | |
| whoami | Show current identity |
Build from Source
npm run build
node dist/index.js --help