opencode auto-loads AGENTS.md as instructions, overriding the built-in default of 'only commit when explicitly asked.' This repo's stance is the opposite — commit and push when reasonable — which already matches the custom agents in .opencode/agents/ (coordinator.md §5 'Push Main After Every Merge', implementation-specialist.md §5 'Push immediately'). The file also surfaces the project conventions (no comments, OsRng for nonces, zeroize-on-drop, thiserror/anyhow, no async, frozen wire format) so they apply to all sessions, not just spawned implementation agents. No code change. Restart opencode to load the new instructions.
alkvault
Local key vault: BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, and AES-256-GCM encryption for securing provider keys, credentials, and identity material.
alkvault is a standalone crate with zero network dependencies. It
holds the master seed — the root of trust for all derived keys and
encrypted credentials — and provides the cryptographic primitives and a
runtime API for managing it. Nothing important goes in env vars.
What it is
A local key vault built on the principle that secrets are derived, not
stored. From a single BIP39 mnemonic, the vault deterministically
derives Ed25519 identity keys, SSH host keys, and AES-256-GCM encryption
keys on demand. External credentials (API keys, OAuth tokens) that can't
be derived are encrypted with a seed-derived key and stored as
EncryptedData blobs.
The vault is local-only by construction — direct method calls on
VaultServiceHandle, no actor, no message enum, no wire format, no
remote dispatch. The master seed and derived private keys never cross a
process boundary.
Usage
use alkvault::derivation::PATHS;
use alkvault::{VaultServiceHandle, KeyType};
// Generate a new mnemonic and unlock the vault.
let vault = VaultServiceHandle::new();
let phrase = vault.unlock_new(24)?; // 24-word BIP39 mnemonic
// Store `phrase` securely — it is the root of trust.
// Derive an Ed25519 identity keypair at m/74'/0'/0'/0'.
let identity = vault.derive_ed25519(PATHS::IDENTITY)?;
assert_eq!(identity.key_type, KeyType::Ed25519);
assert_eq!(identity.private_key.len(), 32);
// Encrypt an external credential with a seed-derived AES-256-GCM key.
let plaintext = "sk-proj-abc123xyz789";
let encrypted = vault.encrypt(plaintext, 2)?;
let decrypted = vault.decrypt(&encrypted)?;
assert_eq!(decrypted, plaintext);
// Lock the vault — purges the seed and all cached derived keys.
vault.lock();
# Ok::<(), Box<dyn std::error::Error>>(())
Features
| Feature | Default | Description |
|---|---|---|
secp256k1 |
off | BIP-0032 secp256k1 HD key derivation for Ethereum signing keys (m/44'/60'/0'/0/0) |
Without secp256k1, derive_ethereum_key returns
UnsupportedKeyType. The secp256k1 crate is a heavy dependency (C
library for curve operations); feature-gating it keeps the default build
lightweight.
Derivation paths
alkvault reserves the 74' coin type (unallocated per SLIP-0044).
| Path | Purpose | Key type |
|---|---|---|
m/74'/0'/0'/0' |
Primary identity keypair | Ed25519 |
m/74'/0'/0'/{n}' |
Worker/device identity | Ed25519 |
m/74'/0'/1'/0' |
SSH host key | Ed25519 |
m/74'/2'/0'/0' |
Encryption key (v2) | AES-256-GCM |
m/74'/2'/0'/{n}' |
Encryption key (v{n+2}) | AES-256-GCM |
m/44'/60'/0'/0/0 |
Ethereum signing key | secp256k1 (feature-gated) |
Key rotation re-encrypts EncryptedData from one version to another via
version-indexed derivation paths — same seed, different keys, no new
mnemonic needed.
Security model
- Seed never persisted. The BIP39 mnemonic is entered at startup or
via
unlockand held only inZeroize-protected RAM.lock()purges the seed and all cached derived keys. - Zeroize everything sensitive.
Mnemonic,Seed,ExtendedPrivKey,EncryptionKey,DerivedKey, andCachedKeyall implementZeroizeandZeroizeOnDrop. Secret material does not linger in freed heap memory. - OsRng for nonces. AES-GCM IVs use the operating system's CSPRNG
(
SysRng), never a thread-local RNG. IV reuse under the same key is catastrophic for GCM. DerivedKeyredaction.DerivedKeyserializesprivate_keyas"[REDACTED]"in all formats (defense-in-depth for logging accidents) and rejects redacted payloads on deserialization.Debugimpls also redact.- Move-only, not
Clone.DerivedKeyandEncryptionKeyare move-only — no accidental duplication of secret material. - No
unwrap()outside tests. Vault operations propagate errors. A poisoned lock is recovered withunwrap_or_else(|e| e.into_inner()), not panicked.
Crate independence
alkvault does not depend on any application or networking crate.
It defines its own types (VaultServiceError, DerivedKey,
EncryptedData, etc.) and is usable in contexts where networking doesn't
exist — CLI tools, test harnesses, key-derivation utilities, and future
WASM targets.
Documentation
Architecture documentation lives under docs/architecture/:
- Mnemonic and key derivation — BIP39, SLIP-0010, BIP-0032, derivation paths
- Encryption — AES-256-GCM,
EncryptedData, key versioning - Service —
VaultServiceHandlelifecycle, cache, error model - Protocol —
DerivedKeyredaction,KeyType, serialization - Architecture decisions (ADRs) — standalone crate, local-only dispatch, HD derivation, key rotation
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.