Keyhole is a local MCP server that talks over stdio. Register it with your client and eight wallet tools show up next to the rest.
claude mcp add keyhole \
-e KEYHOLE_PASSPHRASE='your passphrase' \
-e KEYHOLE_RPC=https://api.mainnet-beta.solana.com \
-- keyhole-mcpAdd this to claude_desktop_config.json (Settings → Developer → Edit Config), then restart Claude.
{
"mcpServers": {
"keyhole": {
"command": "keyhole-mcp",
"env": {
"KEYHOLE_PASSPHRASE": "your passphrase",
"KEYHOLE_RPC": "https://api.mainnet-beta.solana.com"
}
}
}
}Add this to ~/.cursor/mcp.json, or Settings → MCP → Add new server.
{
"mcpServers": {
"keyhole": {
"command": "keyhole-mcp",
"env": { "KEYHOLE_PASSPHRASE": "your passphrase" }
}
}
}Add this to ~/.codex/config.toml.
[mcp_servers.keyhole]
command = "keyhole-mcp"
env = { KEYHOLE_PASSPHRASE = "your passphrase" }Any MCP client that can launch a stdio server works. Run this command and pass the variables below.
keyhole-mcpKEYHOLE_PASSPHRASEUnlocks the vault. Required. Use the one you chose at keyhole init.
KEYHOLE_RPCSolana RPC URL. Defaults to devnet. Use your own node for the most privacy.
KEYHOLE_HOMEWhere the vault lives. Defaults to ~/.keyhole. Handy for running separate vaults per agent.
Every wallet argument accepts a label, an id or an address. Errors come back as readable text, so the agent can explain a refusal.
Opens a new wallet. The keypair is generated and encrypted locally. Returns the address to fund. The owner can switch this tool off.
labelstring, max 40optional{
"label": "research-bot",
"address": "7Kq3bXw9RfLm2VtP8cYh4NnAe6JdGsUo1ZiQ5HrkTWpE",
"network": "mainnet",
"limits": { "maxPerTxSol": 0.1, "dailyLimitSol": 0.5 }
}Sends SOL, plus a 1% Keyhole fee on top on mainnet. Checked in order: frozen, allowlist, per-send cap, rolling 24-hour cap including fees. The amount is reserved before broadcast, so parallel calls can't overshoot. Waits for confirmation before it returns.
walletstringtoaddressamount_solnumber > 0memostring, max 200optional{
"signature": "2uReJSXjSCyvjTrpNpcTYmJKLzbK9xcieJi43AqrJNN63o8DNDB3gNKEcRSCa7zrmewZmwdmB7XZTzRUNEiJwCjW",
"sent": 0.05,
"keyholeFee": 0.0005,
"to": "9xF2pLqTzR4mWv8NcY3hKs6BdJa1GeUo7XiQ5HrnvQ7c"
}
// refused:
Error: 3 SOL is over the per-transaction limit of 0.1 SOLSends an SPL token, classic or Token-2022, plus a 1% Keyhole fee in the same token on mainnet. Only mints the owner has given a limit can move. Creates the recipient's token account when it doesn't exist yet, and that rent counts against the SOL limit.
walletstringmintaddresstoaddressamountnumber, whole tokens{
"signature": "5Bc1…",
"sent": 12.5,
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
}
// not allowed yet:
Error: token EPjF… has no limit set; the owner must allow it with the CLI firstSOL balance and every token account with a balance above zero.
walletstring{
"sol": 1.842,
"tokens": [
{ "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "amount": "250", "decimals": 6 }
]
}Signs plain text with the wallet key, for Sign-In With Solana and similar log-ins. Off until the owner runs keyhole policy <wallet> --sign on. Text with control bytes is refused, so a serialised transaction can't be slipped in as a "message". Every signature is logged.
walletstringmessagestring, 1 to 2000 chars{
"address": "7Kq3bXw9RfLm2VtP8cYh4NnAe6JdGsUo1ZiQ5HrkTWpE",
"message": "Sign in to app.xyz\nNonce: 42",
"signature": "3Fx9…" // base58 ed25519
}Read-only view of the wallet's caps and what it has sent in the last 24 hours. Useful before the agent plans a payment.
walletstring{
"frozen": false,
"sol": { "maxPerTx": 0.1, "daily": 0.5, "usedLast24hIncludingFees": 0.18 },
"tokens": { "EPjF…": { "maxPerTx": 50, "daily": 200, "usedLast24hBaseUnits": "12500000" } },
"allowlistOnly": false,
"signMessages": false
}Recent transactions for the wallet, newest first.
walletstringlimit1 to 50, default 10optional[
{ "signature": "2uRe…", "time": "2026-11-05T09:14:02Z", "ok": true }
]Labels and addresses of every wallet in this vault. Never returns secrets.
noneno arguments[
{ "label": "research-bot", "address": "7Kq3bXw9RfLm2VtP8cYh4NnAe6JdGsUo1ZiQ5HrkTWpE", "frozen": false }
]Changing limits, allowing tokens, the allowlist, message signing, freezing, exporting keys and switching off wallet creation. All of these live in the owner CLI and need the passphrase.
"Open a wallet called scout and tell me the address." · "What can you still spend today?" · "Pay 5 USDC to this address for the dataset." · "Sign this log-in message for app.xyz."
The docs walk through install, limits and the security model.