sigil-mcp
Provides out-of-band human confirmation for signing requests via ntfy push notifications, adding an extra layer of security before any signing operation is performed.
Allows signing Solana messages and transactions using a locally managed private key, enabling Claude Code to interact with the Solana blockchain without exposing key material.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@sigil-mcpSign a transaction sending 0.01 ETH to 0xabc"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
sigil
Claude can sign, but never see.
sigil is a local signing tool and Claude Code integration that lets agentic coding tools use private keys without ever putting key material in the model's context window.
Status: pre-alpha. The MCP server, CLI, unlock flow, ward hooks, policy engine (static checks), out-of-band confirmation via ntfy, Solana signing, and the JSON-RPC signing proxy (Foundry/Hardhat) all work end-to-end. Rolling-window value caps and EIP-712 domain allowlists are not yet implemented. Until they land — and until the supply-chain attestations promised for v0.1.0 ship — do not use this with real funds yet. Build plan lives in the tracking issue.
What it is
One MCP server process, four bins (plus a legacy sigild alias), six runtime deps (all pinned, zero transitive):
sigil-mcp— the only thing that runs. Claude Code spawns it per session via yourmcpServersconfig; it dies when Claude exits. Holds unlocked keys in process memory (zeroized on shutdown,sigil lock, or unlock-failure; mlock against swap is planned). Keys at rest are encrypted with XChaCha20-Poly1305 and an Argon2id-derived key. Signs over stdio using a DIY MCP wire protocol (~200 lines, no SDK dep). Claude never sees key material — only opaque handles likeevm:executor.sigil— control CLI.init,status,portal new/add/list/qr/remove,policy show/init,unlock,lock.sigil-hook-pre/sigil-hook-post— Claude Code hook binaries that block reads of common key paths and redact key-shaped strings from tool output.
sigil-mcp boots locked: empty in-memory handle table, no keys loaded. Sign methods return DAEMON_LOCKED (-32003) with a "run sigil unlock" message until you push the passphrase in from a separate terminal via sigil unlock. That CLI connects to a per-session Unix socket at ~/.sigil/control/<pid>.sock (0600) that sigil-mcp opens at startup — and fans out to every such socket so one sigil unlock reaches all open windows. After unlock, signs work for the rest of the session; sigil lock zeroizes the table without killing the process.
Sign methods exposed today: EIP-191 personal_sign, EIP-1559 + legacy transactions, EIP-712 typed data, plus Solana (ed25519) message + transaction signing — see Solana support below.
Related MCP server: mcp-local-llm
What it isn't
Not a hardware wallet replacement. If you can use a Ledger or YubiKey, do that.
Not a custody solution. It runs on your laptop or VPS and protects you from one specific class of failure: leaking key material through an LLM agent.
A first cut of bounding signing authority via the policy engine — but not the full thing. Shipped: static checks (chain ID, destination allowlist, per-tx value cap, function-selector allowlist, contract-creation gating, on/off toggles for personal_sign and EIP-712) plus out-of-band human confirmation via ntfy. Rolling-window caps and EIP-712 domain allowlists are tracked in #3 and will land incrementally.
Install
npm install -g sigildThis drops four binaries on your $PATH: sigil, sigil-mcp, sigil-hook-pre, sigil-hook-post (plus sigild, a legacy alias for sigil-mcp). (The package name on npm is sigild for legacy reasons; the bins do not include a daemon any more.)
Requires Node 22+, macOS or Linux. (The CLI ↔ session control channel uses Unix domain sockets; Windows is untested and currently unsupported.)
Quick start
# 1. Wire sigil into Claude Code (project-scoped). Pass --user to do it globally.
sigil init
# 2a. Generate a fresh key inside sigil (no plaintext ever hits disk):
sigil portal new evm:bot
# → prompts for a passphrase, mints a fresh secp256k1 key, prints the
# address, writes ~/.sigil/keys/evm:bot.sigil + permissive policy.
#
# 2b. OR import an existing private key from a file:
# Accepts either 32 raw bytes or 64 hex chars (with optional 0x prefix).
sigil portal add evm:bot --key-file ./private.hex
# → same as above but seeded from the file. Source file is deleted by
# default (pass --no-remove-source to keep it).
#
# Either form: pass --strict to start with a locked-down policy template
# you fill in before any sign succeeds.
# 3. Open Claude Code. It spawns sigil-mcp automatically via your MCP config.
# sigil-mcp boots locked — the first sign attempt will return DAEMON_LOCKED.
# 4. In a separate terminal, push the passphrase to every running sigil-mcp.
sigil unlock
# → prompts once, decrypts every keyfile in ~/.sigil/keys/ into each open window
# 5. Use Claude Code. The sigil_* tools (EVM + Solana) will work for the rest of the session.
# Optional: re-lock without restarting Claude.
sigil lockIf you close Claude Code, sigil-mcp exits and its memory is wiped. Open a new session and sigil unlock again — the encrypted keyfiles on disk persist.
CLI reference
sigil init [--user]
Project scope: writes the ward hooks to <cwd>/.claude/settings.json
and the MCP server registration to <cwd>/.mcp.json.
--user: writes hooks to ~/.claude/settings.json and the MCP server
registration to ~/.claude.json. (Claude Code CLI reads MCP configs
from .mcp.json / ~/.claude.json — not from settings.json.)
Idempotent — preserves your unrelated settings, and on upgrade
migrates any stale mcpServers.sigil entry out of settings.json.
sigil portal new <handle> [--strict]
Generate a fresh secp256k1 key inside sigil, encrypt with your
passphrase, write it to ~/.sigil/keys/<handle>.sigil (mode 0600).
No plaintext key ever lands on disk. Use this when you want a clean
hot wallet for a bot (vs importing an existing key from a file).
Also writes ~/.sigil/policy/<handle>.toml — permissive by default,
or --strict for a locked-down template.
sigil portal add <handle> --key-file <path> [--no-remove-source] [--strict]
Import an existing private key. Encrypts it with your passphrase
and stores at ~/.sigil/keys/<handle>.sigil (mode 0600). Handle
format is <kind>:<name> where kind is "evm". The source key file
is deleted by default — pass --no-remove-source to keep it.
Also writes ~/.sigil/policy/<handle>.toml — permissive by default
(signs anything), or --strict for a locked-down template you fill
in before signs succeed.
sigil policy show <handle>
Print the current policy file for a portal. Validates schema; exits
1 if the file is missing or malformed.
sigil policy init <handle> [--strict]
Provision a policy file for an existing portal whose policy is
missing (e.g. a keyfile from an older sigil version, or one you
manually deleted). Refuses to overwrite — edit the file directly
or remove it first. Defaults to permissive; --strict writes the
locked-down template.
sigil rpc init <handle> --upstream <url> [--port <n>]
Enable the JSON-RPC signing proxy for a portal: generates a strong
auth token and appends the [rpc] block to ~/.sigil/config.toml
(refuses if one exists; preserves the rest of the file). Prints the
ready-to-paste authenticated endpoint and forge invocation. Restart
Claude Code sessions to pick it up.
sigil portal list
List the encrypted keyfiles on disk with their derived addresses
(EVM + Solana). Requires the passphrase.
sigil portal qr <handle>
Render a portal's address as a terminal QR code (for funding it from
a phone wallet). Requires the passphrase.
sigil portal remove <handle>
Delete a keyfile from disk.
sigil unlock
Prompt for the passphrase and push it to every running sigil-mcp at
once (one per Claude window). After unlock, sign calls succeed for the
rest of each session. Idempotent — sessions already unlocked are left
as-is. Fails if no sigil-mcp is running (start a Claude Code session
first).
sigil lock
Tell every running sigil-mcp to zeroize and clear its in-memory keys.
Re-unlock with sigil unlock — the sigil-mcp processes keep running.
sigil status
Report which sigil-mcp sessions are running (one entry per window,
with PID, unlocked flag, and loaded portals) and how many keyfiles
exist on disk. Does not require the passphrase.Set SIGIL_HOME to override ~/.sigil. Set SIGIL_CONTROL_DIR to override the control-socket directory.
Multi-window behaviour
Each Claude Code window spawns its own sigil-mcp, and each binds its own control socket at ~/.sigil/control/<pid>.sock. They share the on-disk keyfiles + audit log but keep separate in-memory handle tables.
sigil unlock / lock / status fan out across every socket in ~/.sigil/control/, so a single sigil unlock loads keys into all currently-open windows — no more guessing which process the CLI reaches. Sockets left behind by hard-killed sessions are detected and cleaned up automatically on the next CLI call.
Each window still holds its own decrypted keys only for its own lifetime: closing a window zeroizes that session's keys, and a window opened after you unlock starts locked (run sigil unlock again to include it). Keys never outlive the Claude sessions that use them — a deliberate property from #23.
OS-keychain integration (planned, v0.3) will make unlock zero-touch for users who set it up.
Policy engine
Once a portal is unlocked, signing authority over its key is real. To bound the blast radius of a successful prompt injection, every portal has a policy file at ~/.sigil/policy/<handle>.toml. Two modes:
Permissive (default for sigil portal add): no rules. Sign anything the agent asks. The key isolation guarantees still hold — your key never enters the agent's context — but the unlocked portal can be made to sign whatever an attacker can get the agent to ask for. Useful for: testnet bots, demo flows, anyone who only cares about the context-window protection.
Strict (opt in with --strict): every sign request is checked. Generated template:
mode = "strict"
chain_ids = [1] # allowed chain IDs
allow_to = [] # allowed destination addresses (lowercase 0x)
max_value_wei = "0" # per-tx cap, in wei, as decimal string
allowed_selectors = [] # 4-byte function selectors, e.g. "0xa9059cbb"
allow_contract_creation = false # deploys (to = null); when true, every
# deploy still requires a confirm tap
allow_message_signing = false # EIP-191 personal_sign (e.g. SIWE)
allow_typed_data = false # EIP-712 (Permit, OpenSea — can be financial)
# Optional: above this value, sigil pushes a notification to your phone and
# waits for an approve/deny tap before signing. See "Out-of-band confirm"
# below. Must be strictly less than max_value_wei.
require_confirm_above_wei = "10000000000000000" # 0.01 ETHA failed rule throws POLICY_DENIED (-32001) back to the agent with the human-readable reason ("tx denied — value X exceeds max_value_wei Y"), and the deny is appended to the hash-chained audit log alongside allows. Denies are forensically the more interesting half — they're the prompt-injection canary.
What's deferred to follow-up PRs (still in #3): rolling-window value caps (e.g. 1 ETH/day per portal), EIP-712 domain + primary-type allowlists, decoded-calldata arg checks.
Out-of-band confirm
For sign requests above require_confirm_above_wei, sigil pushes a notification to a channel you control (not the agent) and waits for an explicit human ack before signing. Today the only transport is ntfy — zero-setup, no accounts. SMS and Telegram transports are wired behind the same ConfirmTransport interface and will land in follow-ups.
Wire it up in ~/.sigil/config.toml:
[confirm.ntfy]
topic = "your-unguessable-string-here" # the topic name IS the credential
# server = "https://ntfy.example.com" # optional, default https://ntfy.sh
[confirm]
# timeout_ms = 60000 # default 60s; timeout = denyInstall the ntfy app on your phone, subscribe to that topic, and you'll get a push with Approve / Deny buttons every time the threshold is crossed. The buttons hit a local 127.0.0.1 listener inside sigil-mcp with a one-time, request-bound token — a leaked or replayed token can't approve a different sign. Timeout, deny click, and push-provider outage all fail closed.
If any policy file sets require_confirm_above_wei but no transport is configured, sigil-mcp refuses to start with a clear error rather than silently degrading every confirm-gated sign to a deny.
JSON-RPC signing proxy (Foundry / Hardhat / any web3 tool)
sigil can expose a local JSON-RPC endpoint that makes any portal a drop-in signer for tooling that expects an unlocked node account — the same pattern Clef and web3signer use. Contract bytecode goes from forge straight into sigil; it never transits the agent's context or an MCP tool parameter.
Enable it with one command (generates the token, writes the config block, prints the forge invocation):
sigil rpc init evm:bot --upstream https://sepolia.example/v3/KEY...or by hand in ~/.sigil/config.toml:
[rpc]
portal = "evm:bot" # which portal signs
upstream = "https://sepolia.example/v3/KEY" # real node for everything else
token = "<openssl rand -hex 24>" # required — guards the endpoint
# port = 8547 # default 8547 (clear of anvil's 8545)Then point any tool at it, with the token as the Basic-auth password:
forge script script/Deploy.s.sol \
--rpc-url "http://sigil:<token>@127.0.0.1:8547" \
--unlocked --sender 0xYourPortalAddress --broadcastThe proxy serves three things with the portal key and forwards everything else (eth_call, eth_estimateGas, eth_getTransactionReceipt, …) to the upstream:
eth_accounts→ the portal address (empty while locked)eth_signTransaction→ fills nonce/gas/fees if missing, signs, returns the raw txeth_sendTransaction→ same, then broadcasts via the upstream and returns the hash
Security properties. The listener binds 127.0.0.1 only; every request must present the config token (constant-time compared) and a loopback Host header (DNS-rebinding defence). Signing runs through the identical daemon pipeline as the MCP tools — policy checks, the out-of-band confirm gate, and the hash-chained audit log all apply, so this surface adds a transport, not a privilege. The filled transaction carries the upstream's chain id, so a strict policy's chain_ids allowlist binds the proxy to the network you configured. Message/typed-data methods (eth_sign, personal_sign, eth_signTypedData*) are rejected on this surface — use the MCP tools, which have their own policy toggles. A strict policy with allow_contract_creation = true gives you confirm-gated forge script deploys: forge submits, your phone buzzes, the tx signs when you tap approve.
With multiple Claude windows open, each sigil-mcp tries to bind the port; the first wins and the rest log and continue — any one session's proxy serves the machine.
When the proxy is enabled, sigil-mcp advertises the endpoint — including the authenticated URL — in the sigil_eth_sign_transaction tool description, so the agent discovers it on its own and reaches for forge --unlocked instead of transcribing bytecode through the MCP tool. This is deliberate: the token gates other local software, not your agent (see THREAT_MODEL.md).
Solana (SVM)
Every portal also controls a Solana address, derived from the same secret. EVM uses secp256k1; Solana uses ed25519 — different curves, so you can't share a public key. But the portal's raw 32-byte secret doubles as an ed25519 seed, yielding one secret → two addresses:
$ sigil portal list
evm:executor
evm: 0x1234…abcd
svm: 7vWxK…Qm9f # base58 ed25519 address, same keyThis is exactly the derivation Phantom/Solflare perform on "import private key", so the Solana address is recoverable there (it does not match a seed-phrase / BIP44 account — it's the raw-key account).
Two MCP tools:
sigil_svm_sign_message— sign arbitrary off-chain bytes (e.g. Sign-In With Solana) with the ed25519 key. Input is base64; returns a base58 signature.sigil_svm_sign_transaction— pass a serialized Solana transaction message (base64, legacy or v0); sigil ed25519-signs those bytes and returns the base58 signature for you to assemble into the transaction.
Policy. Solana hides most semantics behind account indices and on-chain state, so sigil only decodes what it can offline: native SOL (System Program) transfers, which it gates on svm_allow_to (base58 recipient allowlist) and svm_max_lamports (per-tx cap), exactly like EVM. Anything it can't fully decode — SPL tokens, program calls, address-lookup-table accounts — is routed to the out-of-band confirm gate, never silently allowed. Auto-allow is all-or-nothing: a tx is only signed without a human tap if every instruction decoded and passed policy. require_confirm_above_lamports adds a value threshold (and, in strict mode, the undecodable-tx confirm); in strict mode an undecodable tx with no confirm transport configured fails closed (deny).
Relevant policy fields (~/.sigil/policy/<handle>.toml): allow_svm_message_signing, svm_allow_to, svm_max_lamports, require_confirm_above_lamports.
Supply chain posture
Key-management libraries die from supply chain compromise, not from clever attacks on the code. Given the npm ecosystem in 2026 (Mini Shai-Hulud, Axios, pgserve, TanStack), sigil commits to:
Zero install scripts. No
postinstall,preinstall,prepare. CI-enforced: every PR runs a guard that fails if any package in the resolved tree declares one.Six runtime deps, all version-pinned (no caret ranges), all zero-transitive — the entire
npm ls --omit devtree is exactly these six packages:@noble/ciphersfor XChaCha20-Poly1305@noble/hashesfor Argon2id, keccak256, sha2/sha512, HMAC@noble/secp256k1for ECDSA (EVM)@noble/ed25519for EdDSA (Solana)@iarna/tomlfor parsing per-portal policy TOML filesqrcode-generatorforsigil portal qrrendering
No MCP SDK. The official
@modelcontextprotocol/sdkpulls 92 transitive deps (ajv, hono, cors, cross-spawn, etc) — unacceptable surface. We implement the MCP wire protocol directly in ~200 lines.No Bun. Plain Node only. Bun is currently being weaponized by Mini Shai-Hulud as an evasion layer; we will not give that pattern any cover.
Provenance attestations on every npm publish. Starting v0.0.4, releases are built by a GitHub Actions workflow under OIDC trusted-publisher auth, signed with a Sigstore attestation. No long-lived npm token; tampered or out-of-band publishes fail signature verification.
CycloneDX SBOM attached to every GitHub Release. Full transitive dep tree enumerated at release time.
Install-scripts CI guard. Every PR fails if any package in the resolved tree declares
preinstall/install/postinstall..npmrcalready hasignore-scripts=trueso these never actually run for us; the guard catches new transitive deps that might run for a user without our.npmrc.Still planned for v0.1.0:
Signed standalone binaries from GitHub Releases for users who'd rather not touch npm
Action SHA pinning rotation via Dependabot
Verifying a release
You can confirm a sigild tarball was built by the public workflow at the commit it claims to come from:
# Validates every package in your install tree:
npm audit signatures
# Inspect the attestation for a specific sigild version:
npm view sigild@<version> dist.attestations
# → shows the workflow filename, the commit SHA, and the Sigstore signing certWhat the attestation tells you: this tarball was built by cdrn/sigil's .github/workflows/release.yml, at a specific commit on main, at a specific time. It does not tell you that commit is non-malicious — for that, read the diff between the version you trust and the version you're upgrading to. But it does mean an attacker who steals an npm token can't publish a malicious sigild under our name; they'd need to compromise the GitHub repo + push a tag, which leaves an audit trail.
Every release also publishes a CycloneDX SBOM as a GitHub Release asset, enumerating every package (direct + transitive) in the install tree at the version pinned by package-lock.json:
# Download + inspect the SBOM for a specific release:
gh release download v0.0.4 --repo cdrn/sigil --pattern '*.cdx.json'
# → produces sigild-v0.0.4.cdx.json — feed to syft/grype/etc. for vuln scanThreat model
See THREAT_MODEL.md. Read it before trusting this with anything.
Development
git clone https://github.com/cdrn/sigil
cd sigil
npm install # respects .npmrc ignore-scripts=true
npm test # builds + runs 600+ tests; should finish in under 15sSee CONTRIBUTING.md for the PR-per-layer workflow.
License
Apache License 2.0. See LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/cdrn/sigil'
If you have feedback or need assistance with the MCP directory API, please join our Discord server