The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Sigil listing page.
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.
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 your mcpServers config; 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 like evm: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.
This 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.)
If 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.
Set SIGIL_HOME to override ~/.sigil. Set SIGIL_CONTROL_DIR to override the control-socket directory.
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.
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:
A 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.
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:
Install 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.
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):
...or by hand in ~/.sigil/config.toml:
Then point any tool at it, with the token as the Basic-auth password:
The 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 hashSecurity 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).
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:
This 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.
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:
postinstall, preinstall, prepare. CI-enforced: every PR runs a guard that fails if any package in the resolved tree declares one.npm ls --omit dev tree is exactly these six packages:
@noble/ciphers for XChaCha20-Poly1305@noble/hashes for Argon2id, keccak256, sha2/sha512, HMAC@noble/secp256k1 for ECDSA (EVM)@noble/ed25519 for EdDSA (Solana)@iarna/toml for parsing per-portal policy TOML filesqrcode-generator for sigil portal qr rendering@modelcontextprotocol/sdk pulls 92 transitive deps (ajv, hono, cors, cross-spawn, etc) — unacceptable surface. We implement the MCP wire protocol directly in ~200 lines.preinstall / install / postinstall. .npmrc already has ignore-scripts=true so these never actually run for us; the guard catches new transitive deps that might run for a user without our .npmrc.You can confirm a sigild tarball was built by the public workflow at the commit it claims to come from:
What 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:
See THREAT_MODEL.md. Read it before trusting this with anything.
See CONTRIBUTING.md for the PR-per-layer workflow.
Apache License 2.0. See LICENSE.