
Security News
Re-Enabled GitHub Actions Expose Thousands of Repositories to Mini Shai-Hulud
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.
CLI for Keychains.dev — register machines, manage permissions, and make proxied API calls
CLI for Keychains.dev — secure credential delegation for AI agents.
npm install -g keychains
# 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:
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.
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!
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 >> .env
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": "alice@example.com"}' \
| keychains curl https://api.com/users/{{$user}} \
-X POST \
-d '{"user": "{{$user}}", "email": "{{$email}}"}'
| 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"] |
{{$...}} patterns in the URL, headers, and body{{$path}} is replaced with the resolved value from the piped JSON{{$...}} patterns) is saved to historyparams# 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}}"}'
keychains machine — Machine IdentityManage 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 confirmation
keychains permissions — Permission RequestsCreate, 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 IdentitiesGive 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 >> .env
Aliases: keychains delegate add, keychains delegate ls, etc.
keychains connections — OAuth & API Key ConnectionsList 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 confirmation
Aliases: keychains conn ls, keychains conn rm, etc.
keychains curl — Proxied API CallsMake 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_ID in .env (cwd)KEYCHAINS_PERMISSION_ID environment variableEvery curl command is automatically saved to local history.
keychains whoami — Identity Checkkeychains whoami # Human-readable
keychains whoami --json # JSON output
keychains whoami --env >> .env # Append to .env
keychains history — Curl Command HistoryBrowse, 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 confirmation
keychains templates — Request TemplatesSave 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 confirmation
Aliases: keychains tpl ls, keychains tpl show, etc.
keychains export — Code GenerationExport 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.ts
Generated 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 |
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.
| 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 |
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 |
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 headerPass 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 optionFilter connections by account label:
# List only "work" connections
keychains connections ls --account work
# Remove a specific account's connection
keychains connections rm google --account personal
When 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.
# 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"}'
# 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'
.env for ProjectsDrop a .env in your project root:
KEYCHAINS_PERMISSION_ID=pr_abc123
Every keychains curl in that directory will use it automatically.
Or create a permission directly into .env:
keychains permissions create --name "My Project" --env >> .env
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 |
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.app
The 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 <you@example.com> 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 test
You 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.
| 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 |
npm run build
node dist/index.js --help
FAQs
CLI for Keychains.dev — register machines, manage permissions, and make proxied API calls
The npm package keychains receives a total of 3 weekly downloads. As such, keychains popularity was classified as not popular.
We found that keychains demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.

Research
/Security News
The compromise affects MemTensor's MemOS, an open source memory framework for large language models (LLMs) and AI agents. Both npm package @memtensor/memos-cloud-openclaw-plugin and the PyPI package MemoryOS are compromised. They drop cross-platform Go binaries that exfiltrate developer secrets.