skills/vetkd/SKILL.md
Implement on-chain encryption using vetKeys (verifiable encrypted threshold key derivation). Covers key derivation, IBE encryption/decryption, transport keys, and access control. Use when adding encryption, decryption, on-chain privacy, vetKeys, or identity-based encryption to a canister. Do NOT use for authentication — use internet-identity instead.
npx skillsauth add dfinity/icskills vetkdInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Note: vetKeys is a newer feature of the IC. The
ic-vetkeysRust crate and@dfinity/vetkeysnpm package are published, but the APIs may still change over time. Pin your dependency versions and check the DFINITY forum for any migration guides after upgrades.
vetKeys (verifiably encrypted threshold keys) bring on-chain privacy to the IC via the vetKD protocol: secure, on-demand key derivation so that a public blockchain can hold and work with secret data. Keys are verifiable (users can check correctness and lack of tampering), encrypted (derived keys are encrypted under a user-supplied transport key—no node or canister ever sees the raw key), and threshold (a quorum of subnet nodes cooperates to derive keys; no single party has the master key). A canister requests a derived key from the subnet’s threshold infrastructure, receives it encrypted under the client’s transport public key, and only the client decrypts it locally. This unlocks decentralized key management (DKMS), encrypted on-chain storage, private messaging, identity-based encryption (IBE), timelock encryption, threshold BLS, and verifiable randomness—use cases.
ic-vetkeys = "0.6" (crates.io)@dfinity/vetkeys v0.4.0| Canister | ID | Purpose |
|----------|-----|---------|
| Management Canister | aaaaa-aa | Exposes vetkd_public_key and vetkd_derive_key system APIs |
| Chain-key testing canister | vrqyr-saaaa-aaaan-qzn4q-cai | Testing only: fake vetKD implementation to test key derivation without paying production API fees. Insecure, do not use in production. |
The management canister is not a real canister, it is a system-level API endpoint. Calls to aaaaa-aa are routed by the system to the vetKD-enabled subnet that holds the master key specified in key_id; that subnet's nodes run the threshold key derivation. Your canister can call from any subnet.
Testing canister: The chain-key testing canister is deployed on mainnet and provides a fake vetKD implementation (hard-coded keys, no threshold) so you can exercise key derivation without production cycle costs. Use key name insecure_test_key_1. Insecure, for testing only: never use it in production or with sensitive data. You can also deploy your own instance from the repo.
Any canister on the IC can use any available master key regardless of which subnet the canister or the key resides on; the management canister routes calls to the subnet that holds the master key.
| Key name | Environment | Purpose | Cycles (approx.) | Notes |
|----------------|------------------|-------------------|--------------------|-------|
| test_key_1 | Local + Mainnet | Development & testing | 10_000_000_000 (mainnet) | Works both locally and on mainnet. Use for development and testing. |
| key_1 | Mainnet | Production | 26_153_846_153 | Subnet pzp6e (backed up on uzr34) |
Fees depend on the subnet where the master key resides (and its size), not on the calling canister's subnet. If the canister may be blackholed or used by other canisters, send more cycles than the current cost so that future subnet size increases do not cause calls to fail; unused cycles are refunded. See vetKD API — API fees for current USD estimates.
(canister_id, context, input). Same inputs always produce the same key. Neither the canister nor any subnet node ever sees the raw key, as it is encrypted under the client's transport key until decrypted locally.Not pinning dependency versions. The ic-vetkeys crate and @dfinity/vetkeys npm package are published, but the APIs may still change in new releases. Pin your versions and re-test after upgrades. If something stops working after an upgrade, consult the relevant change notes to understand what happened.
Reusing transport keys across sessions. Each session must generate a fresh transport key pair. The Rust and TypeScript libraries include support for generating keys safely; use them if at all possible.
Using raw vetkd_derive_key output as an encryption key. The output is an encrypted blob. You must decrypt it with the transport secret to get the vetKey (raw key material). What you do next depends on your use case: for example, you might derive a symmetric key (e.g. for AES) via toDerivedKeyMaterial() or the equivalent. Do not use the decrypted bytes directly as an AES key. Other uses (IBE decryption, signing, etc.) consume the vetKey in their own way; the libraries document the right pattern for each.
Confusing vetKD with traditional public-key crypto. There are no static key pairs per user. Keys are derived on-demand from the subnet's threshold master key (via the vetKD protocol). The same (canister, context, input) always yields the same derived key.
Putting secret data in the input field. The input is sent to the management canister in plaintext. It is a key identifier, not encrypted payload. Use it for IDs (principal, document ID), never for the actual secret data.
Forgetting that vetkd_derive_key is an async inter-canister call. It costs cycles and requires await. Capture caller before the await as defensive practice.
Using context inconsistently. If the backend uses b"my_app_v1" as context but the frontend verification uses b"my_app", the derived keys will not match and decryption will silently fail.
Not attaching enough cycles to vetkd_derive_key. vetkd_derive_key consumes cycles; vetkd_public_key does not. For derive_key, key_1 costs ~26B cycles and test_key_1 costs ~10B cycles.
Rolling your own IBE without proper authorization checks. If you implement IBE manually (bypassing KeyManager / EncryptedMaps), your canister must enforce that vetkd_derive_key only returns the derived key to the authorized caller — e.g. the principal whose identity was used as the input. Without this check, any caller can request any derived key and decrypt messages meant for someone else. The provided ic-vetkeys / @dfinity/vetkeys libraries handle this correctly; prefer them over a custom implementation.
The vetKD API lets canisters request vetKeys derived by the threshold protocol. Derivation is deterministic: the same inputs always produce the same key, so keys can be retrieved reliably. Different inputs yield different keys—canisters can derive an unlimited number of unique keys. Summary below; full spec: vetKD API and the IC interface specification.
Returns a public key used to verify keys derived with vetkd_derive_key. With an empty context you get the canister-level master public key; with a non-empty context you get the derived subkey for that context. In IBE, this public key lets anyone encrypt to an identity (e.g. a principal); only the holder of that identity can later obtain the matching vetKey and decrypt—no prior key exchange or recipient presence required.
vetkd_public_key : (record {
canister_id : opt canister_id;
context : blob;
key_id : record { curve : vetkd_curve; name : text };
}) -> (record { public_key : blob })
canister_id: Optional. If omitted (null), the public key for the calling canister is returned; if provided, the key for that canister is returned.context: Domain separator which has the same meaning as in vetkd_derive_key. Ensures keys are derived in a specific context and avoids collisions across apps or use cases.key_id.curve: bls12_381_g2 (only supported curve).key_id.name: Master key name: test_key_1 (local + mainnet testing) or key_1 (production).You can also derive this public key offline from the known mainnet master public key; see "Offline Public Key Derivation" below.
Derives key material for the given (context, input) and returns it encrypted under the recipient's transport public key. Only the holder of the transport secret can decrypt. The decrypted material is then used according to your use case (e.g. via toDerivedKeyMaterial() for symmetric keys, or for IBE decryption).
vetkd_derive_key : (record {
input : blob;
context : blob;
transport_public_key : blob;
key_id : record { curve : vetkd_curve; name : text };
}) -> (record { encrypted_key : blob })
input: Arbitrary data used as the key identifier—different inputs yield different derived keys. Does not need to be random; sent in plaintext to the management canister.context: Domain separator; must match the context used when obtaining the public key (e.g. for verification or IBE).transport_public_key: The recipient's public key; the derived key is encrypted under this for secure delivery.encrypted_key. Decrypt with the transport secret to get the raw vetKey, then use it as required (e.g. derive a symmetric key; do not use raw bytes directly as an AES key).Master key names and cycle costs are in Master Key Names and API Fees under Canister IDs.
Cargo.toml:
[dependencies]
candid = "0.10"
ic-cdk = "0.19"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"
# High-level library (recommended) — source: https://github.com/dfinity/vetkeys
ic-vetkeys = "0.6"
ic-stable-structures = "0.7"
Using ic-vetkeys library (recommended):
use candid::Principal;
use ic_cdk::update;
use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory};
use ic_stable_structures::DefaultMemoryImpl;
use ic_vetkeys::key_manager::KeyManager;
use ic_vetkeys::types::{AccessRights, VetKDCurve, VetKDKeyId};
// KeyManager is generic over an AccessControl type — AccessRights is the default.
// It uses stable memory for persistent storage of access control state.
thread_local! {
static MEMORY_MANAGER: std::cell::RefCell<MemoryManager<DefaultMemoryImpl>> =
std::cell::RefCell::new(MemoryManager::init(DefaultMemoryImpl::default()));
static KEY_MANAGER: std::cell::RefCell<Option<KeyManager<AccessRights>>> =
std::cell::RefCell::new(None);
}
#[ic_cdk::init]
fn init() {
let key_id = VetKDKeyId {
curve: VetKDCurve::Bls12381G2,
name: "key_1".to_string(), // "test_key_1" for local + mainnet testing
};
MEMORY_MANAGER.with(|mm| {
let mm = mm.borrow();
KEY_MANAGER.with(|km| {
*km.borrow_mut() = Some(KeyManager::init(
"my_app_v1", // domain separator
key_id,
mm.get(MemoryId::new(0)), // config memory
mm.get(MemoryId::new(1)), // access control memory
mm.get(MemoryId::new(2)), // shared keys memory
));
});
});
}
#[update]
async fn get_encrypted_vetkey(subkey_id: Vec<u8>, transport_public_key: Vec<u8>) -> Vec<u8> {
let caller = ic_cdk::caller(); // Capture BEFORE await
let future = KEY_MANAGER.with(|km| {
let km = km.borrow();
let km = km.as_ref().expect("not initialized");
km.get_encrypted_vetkey(caller, subkey_id, transport_public_key)
.expect("access denied")
});
future.await
}
#[update]
async fn get_vetkey_verification_key() -> Vec<u8> {
let future = KEY_MANAGER.with(|km| {
let km = km.borrow();
let km = km.as_ref().expect("not initialized");
km.get_vetkey_verification_key()
});
future.await
}
Calling management canister directly (lower level):
use candid::{CandidType, Deserialize, Principal};
use ic_cdk::update;
#[derive(CandidType, Deserialize)]
struct VetKdKeyId {
curve: VetKdCurve,
name: String,
}
#[derive(CandidType, Deserialize)]
enum VetKdCurve {
#[serde(rename = "bls12_381_g2")]
Bls12381G2,
}
#[derive(CandidType)]
struct VetKdPublicKeyRequest {
canister_id: Option<Principal>,
context: Vec<u8>,
key_id: VetKdKeyId,
}
#[derive(CandidType, Deserialize)]
struct VetKdPublicKeyResponse {
public_key: Vec<u8>,
}
#[derive(CandidType)]
struct VetKdDeriveKeyRequest {
input: Vec<u8>,
context: Vec<u8>,
transport_public_key: Vec<u8>,
key_id: VetKdKeyId,
}
#[derive(CandidType, Deserialize)]
struct VetKdDeriveKeyResponse {
encrypted_key: Vec<u8>,
}
const CONTEXT: &[u8] = b"my_app_v1";
fn key_id() -> VetKdKeyId {
VetKdKeyId {
curve: VetKdCurve::Bls12381G2,
// Key names: "test_key_1" for local + mainnet testing, "key_1" for production
name: "key_1".to_string(),
}
}
#[update]
async fn vetkd_public_key() -> Vec<u8> {
let request = VetKdPublicKeyRequest {
canister_id: None, // defaults to this canister
context: CONTEXT.to_vec(),
key_id: key_id(),
};
// vetkd_public_key does not require cycles (unlike vetkd_derive_key).
let (response,): (VetKdPublicKeyResponse,) = ic_cdk::api::call::call(
Principal::management_canister(), // aaaaa-aa
"vetkd_public_key",
(request,),
)
.await
.expect("vetkd_public_key call failed");
response.public_key
}
#[update]
async fn vetkd_derive_key(transport_public_key: Vec<u8>) -> Vec<u8> {
let caller = ic_cdk::caller(); // MUST capture before await
let request = VetKdDeriveKeyRequest {
input: caller.as_slice().to_vec(), // derive key specific to this caller
context: CONTEXT.to_vec(),
transport_public_key,
key_id: key_id(),
};
// key_1 costs ~26B cycles, test_key_1 costs ~10B cycles.
let (response,): (VetKdDeriveKeyResponse,) = ic_cdk::api::call::call_with_payment128(
Principal::management_canister(),
"vetkd_derive_key",
(request,),
26_000_000_000, // cycles for key_1 (use 10_000_000_000 for test_key_1)
)
.await
.expect("vetkd_derive_key call failed");
response.encrypted_key
}
mops.toml:
[package]
name = "my-vetkd-app"
version = "0.1.0"
[dependencies]
core = "2.0.0"
Using the management canister directly:
import Blob "mo:core/Blob";
import Principal "mo:core/Principal";
import Text "mo:core/Text";
persistent actor {
type VetKdCurve = { #bls12_381_g2 };
type VetKdKeyId = {
curve : VetKdCurve;
name : Text;
};
type VetKdPublicKeyRequest = {
canister_id : ?Principal;
context : Blob;
key_id : VetKdKeyId;
};
type VetKdPublicKeyResponse = {
public_key : Blob;
};
type VetKdDeriveKeyRequest = {
input : Blob;
context : Blob;
transport_public_key : Blob;
key_id : VetKdKeyId;
};
type VetKdDeriveKeyResponse = {
encrypted_key : Blob;
};
let managementCanister : actor {
vetkd_public_key : VetKdPublicKeyRequest -> async VetKdPublicKeyResponse;
vetkd_derive_key : VetKdDeriveKeyRequest -> async VetKdDeriveKeyResponse;
} = actor "aaaaa-aa";
let context : Blob = Text.encodeUtf8("my_app_v1");
// Key names: "test_key_1" for local + mainnet testing, "key_1" for production
func keyId() : VetKdKeyId {
{ curve = #bls12_381_g2; name = "key_1" }
};
public shared func getPublicKey() : async Blob {
// vetkd_public_key does not require cycles (unlike vetkd_derive_key).
let response = await managementCanister.vetkd_public_key({
canister_id = null;
context;
key_id = keyId();
});
response.public_key
};
public shared ({ caller }) func deriveKey(transportPublicKey : Blob) : async Blob {
// caller is captured here, before the await. vetkd_derive_key requires cycles.
let response = await (with cycles = 26_000_000_000) managementCanister.vetkd_derive_key({
input = Principal.toBlob(caller);
context;
transport_public_key = transportPublicKey;
key_id = keyId();
});
response.encrypted_key
};
};
The frontend generates a transport key pair, sends the public half to the canister, receives the encrypted derived key, decrypts it with the transport secret to get the vetKey (raw key material), then derives a symmetric key from that material (e.g. via toDerivedKeyMaterial()) for AES or other use.
import { TransportSecretKey, DerivedPublicKey, EncryptedVetKey } from "@dfinity/vetkeys";
// 1. Generate a transport secret key (BLS12-381)
const seed = crypto.getRandomValues(new Uint8Array(32));
const transportSecretKey = TransportSecretKey.fromSeed(seed);
const transportPublicKey = transportSecretKey.publicKey();
// 2. Request encrypted vetkey and verification key from your canister
const [encryptedKeyBytes, verificationKeyBytes] = await Promise.all([
backendActor.get_encrypted_vetkey(subkeyId, transportPublicKey),
backendActor.get_vetkey_verification_key(),
]);
// 3. Deserialize and decrypt
const verificationKey = DerivedPublicKey.deserialize(new Uint8Array(verificationKeyBytes));
const encryptedVetKey = EncryptedVetKey.deserialize(new Uint8Array(encryptedKeyBytes));
const vetKey = encryptedVetKey.decryptAndVerify(
transportSecretKey,
verificationKey,
new Uint8Array(subkeyId),
);
// 4. Derive a symmetric key for AES-GCM
const aesKeyMaterial = vetKey.toDerivedKeyMaterial();
const aesKey = await crypto.subtle.importKey(
"raw",
aesKeyMaterial.data.slice(0, 32), // 256-bit AES key
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"],
);
// 5. Encrypt
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
aesKey,
new TextEncoder().encode("secret message"),
);
// 6. Decrypt
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
aesKey,
ciphertext,
);
The @dfinity/vetkeys package also provides higher-level abstractions via sub-paths:
@dfinity/vetkeys/key_manager -- KeyManager and DefaultKeyManagerClient for managing access-controlled keys@dfinity/vetkeys/encrypted_maps -- EncryptedMaps and DefaultEncryptedMapsClient for encrypted key-value storageThese mirror the Rust KeyManager and EncryptedMaps types and handle the transport key flow automatically.
You can derive public keys offline (without any canister calls) from the known mainnet master public key for a given key name (e.g. key_1). This is useful for IBE: you derive the canister's public key for your context, then encrypt to an identity (e.g. a principal) without the recipient or the canister being online.
Rust:
use ic_vetkeys::{MasterPublicKey, DerivedPublicKey};
// Start from the known mainnet master public key for key_1
let master_key = MasterPublicKey::for_mainnet_key("key_1")
.expect("unknown key name");
// Derive the canister-level key
let canister_key = master_key.derive_canister_key(canister_id.as_slice());
// Derive a sub-key for a specific context/identity
let derived_key: DerivedPublicKey = canister_key.derive_sub_key(b"my_app_v1");
// Use derived_key for IBE encryption — no canister call needed
TypeScript:
import { MasterPublicKey, DerivedPublicKey } from "@dfinity/vetkeys";
// Start from the known mainnet master public key
const masterKey = MasterPublicKey.productionKey();
// Derive the canister-level key
const canisterKey = masterKey.deriveCanisterKey(canisterId);
// Derive a sub-key for a specific context/identity
const derivedKey: DerivedPublicKey = canisterKey.deriveSubKey(
new TextEncoder().encode("my_app_v1"),
);
// Use derivedKey for IBE encryption — no canister call needed
IBE lets you encrypt to an identity (e.g. a principal) using only the canister's derived public key—the recipient does not need to be online or have registered a key beforehand. The recipient later authenticates to the canister, obtains their vetKey (derived for that identity) via vetkd_derive_key, and decrypts locally.
TypeScript:
import {
TransportSecretKey, DerivedPublicKey, EncryptedVetKey,
IbeCiphertext, IbeIdentity, IbeSeed,
} from "@dfinity/vetkeys";
// --- Encrypt (sender side, no canister call needed) ---
// Derive the recipient's public key offline (see "Offline Public Key Derivation" above)
const recipientIdentity = IbeIdentity.fromBytes(recipientPrincipalBytes);
const seed = IbeSeed.random();
const plaintext = new TextEncoder().encode("secret message");
const ciphertext = IbeCiphertext.encrypt(derivedPublicKey, recipientIdentity, plaintext, seed);
const serialized = ciphertext.serialize(); // store or transmit this
// --- Decrypt (recipient side, requires canister call to get vetKey) ---
// 1. Get the vetKey (same flow as the Frontend section above)
const transportSecretKey = TransportSecretKey.fromSeed(crypto.getRandomValues(new Uint8Array(32)));
const [encryptedKeyBytes, verificationKeyBytes] = await Promise.all([
backendActor.get_encrypted_vetkey(subkeyId, transportSecretKey.publicKey()),
backendActor.get_vetkey_verification_key(),
]);
const verificationKey = DerivedPublicKey.deserialize(new Uint8Array(verificationKeyBytes));
const encryptedVetKey = EncryptedVetKey.deserialize(new Uint8Array(encryptedKeyBytes));
const vetKey = encryptedVetKey.decryptAndVerify(
transportSecretKey, verificationKey, new Uint8Array(subkeyId),
);
// 2. Decrypt the IBE ciphertext
const deserialized = IbeCiphertext.deserialize(serialized);
const decrypted = deserialized.decrypt(vetKey);
// decrypted is Uint8Array containing "secret message"
Rust (off-chain client or test):
use ic_vetkeys::{
DerivedPublicKey, IbeCiphertext, IbeIdentity, IbeSeed, VetKey,
};
// --- Encrypt ---
let identity = IbeIdentity::from_bytes(recipient_principal.as_slice());
let seed = IbeSeed::new(&mut rand::rng());
let plaintext = b"secret message";
let ciphertext = IbeCiphertext::encrypt(
&derived_public_key,
&identity,
plaintext,
&seed,
);
let serialized = ciphertext.serialize();
// --- Decrypt (after obtaining the VetKey) ---
let deserialized = IbeCiphertext::deserialize(&serialized)
.expect("invalid ciphertext");
let decrypted = deserialized.decrypt(&vet_key)
.expect("decryption failed");
// decrypted == b"secret message"
Both the Rust crate and TypeScript package provide two higher-level modules that handle the transport key flow, access control, and encrypted storage for you:
KeyManager<T: AccessControl> (Rust) / KeyManager (TS) — Manages access-controlled vetKeys with stable storage. The canister enforces who may request which keys; the library handles derivation requests, user rights (Read, ReadWrite, ReadWriteManage), and key sharing between principals.
EncryptedMaps<T: AccessControl> (Rust) / EncryptedMaps (TS) — Builds on KeyManager to provide an encrypted key-value store. Each map is access-controlled and encrypted under a derived vetKey. Encryption and decryption of values are handled on the client (frontend) using vetKeys; the canister only stores ciphertext.
In Rust, these live in ic_vetkeys::key_manager and ic_vetkeys::encrypted_maps. In TypeScript, import from @dfinity/vetkeys/key_manager and @dfinity/vetkeys/encrypted_maps. See the vetkeys repository for full examples.
# Start the local network (provisions test_key_1 and key_1 automatically)
icp network start -d
# Deploy your canister
icp deploy backend
# Test public key retrieval
icp canister call backend getPublicKey '()'
# Returns: (blob "...") -- the vetKD public key
# For derive_key, you need a transport public key (generated by frontend)
# Test with a dummy 48-byte blob:
icp canister call backend deriveKey '(blob "\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f\20\21\22\23\24\25\26\27\28\29\2a\2b\2c\2d\2e\2f")'
# Deploy to mainnet
icp deploy backend -e ic
# Use test_key_1 for initial testing, key_1 for production
# Make sure your canister code references the correct key name
# 1. Verify public key is returned (non-empty blob)
icp canister call backend getPublicKey '()'
# Expected: (blob "\ab\cd\ef...") -- 48+ bytes of BLS public key data
# 2. Verify derive_key returns encrypted key (non-empty blob)
icp canister call backend deriveKey '(blob "\00\01...")'
# Expected: (blob "\12\34\56...") -- encrypted key material
# 3. Verify determinism: same (caller, context, input) and same transport key produce same encrypted_key
# Call deriveKey twice with the same identity and transport key
# Expected: identical encrypted_key blobs both times
# 4. Verify isolation: different callers get different keys
icp identity new test-user-1 --storage-mode=plaintext
icp identity new test-user-2 --storage-mode=plaintext
icp identity default test-user-1
icp canister call backend deriveKey '(blob "\00\01...")'
# Note the output
icp identity default test-user-2
icp canister call backend deriveKey '(blob "\00\01...")'
# Expected: DIFFERENT encrypted_key (different caller = different derived key)
# 5. Frontend integration test
# Open the frontend, trigger encryption/decryption
# Verify: encrypted data can be decrypted by the same user
# Verify: encrypted data CANNOT be decrypted by a different user
tools
Deploys an already-built Internet Computer project to a user's own cloud engine (an OpenCloud / control-panel engine, administered from a web console). Covers verifying the icp CLI, linking the user's console identity to the CLI with `icp identity link web`, defaulting the console origin to https://opencloud.org (overridable when the user signs in to a different console), obtaining the engine's subnet id (asking the user when it is unknown), running `icp deploy` against that subnet, and tagging the canisters with `__META_*` environment variables so the engine console shows a named app with labelled backend/frontend canisters. Use when a developer wants to ship an app to their cloud engine, mentions a cloud engine, OpenCloud, an engine subnet id, linking the icp CLI to an engine console, or giving a deployed app a name in the console. Do NOT use for a general mainnet deploy with no specific engine or subnet (use the icp-cli skill) or for writing canister code.
tools
Guides use of the icp command-line tool for building and deploying Internet Computer applications. Covers project configuration (icp.yaml), recipes, environments, canister lifecycle, and identity management. Use when building, deploying, or managing any IC project. Use when the user mentions icp, dfx, canister deployment, local network, or project setup. Do NOT use for canister-level programming patterns like access control, inter-canister calls, or stable memory — use domain-specific skills instead.
development
Deploy frontend assets to the IC. Covers certified assets, SPA routing with .ic-assets.json5, content encoding, and programmatic uploads. Use when hosting a frontend, deploying static files, or setting up SPA routing on IC. Do NOT use for canister-level code patterns or custom domain setup — use custom-domains instead.
development
One-time installer that makes a Claude Code project keep its Internet Computer skills up to date automatically. Sets up a SessionStart hook plus a sync script so .claude/skills/ always mirrors the latest skills published at skills.internetcomputer.org. Use when a user wants to install, bootstrap, or enable "always-latest" Internet Computer / IC / ICP / Motoko skills in a project, or pastes the link to this skill. This is a one-time setup action, not ongoing IC knowledge — after it runs, the installed hook keeps skills current on every session. Do NOT use for IC coding questions themselves — this only configures auto-updating skills.