
Icp Cli
- 249 installs
- 28 repo stars
- Updated August 4, 2026
- dfinity/icskills
Use the Internet Computer CLI to create identities, deploy canisters, run local replicas, inspect logs, and automate ICP workflows from terminal-driven developer and CI environments.
About
The icp-cli skill teaches practical command-line workflows for building and operating on the Internet Computer. It spans project setup, canister deployment, identity management, and debugging via terminal tools. Use it when automating ICP development, wiring CLI steps into CI, or letting agents execute reliable deploy and inspection commands.
- Covers dfx/icp commands for deploy, canister install, and calls
- Supports local replica workflows for fast feedback loops
- Enables scripted identity and network configuration management
- Integrates with CI/CD and agent-driven deployment steps
- Reduces friction for backend and integration tasks on ICP
Icp Cli by the numbers
- 249 all-time installs (skills.sh)
- Ranked #187 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dfinity/icskills --skill icp-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 249 |
|---|---|
| repo stars | ★ 28 |
| Last updated | August 4, 2026 |
| Repository | dfinity/icskills ↗ |
What it does
Use the Internet Computer CLI to create identities, deploy canisters, run local replicas, inspect logs, and automate ICP workflows from terminal-driven developer and CI environments.
Files
ICP CLI
What This Is
The icp command-line tool builds and deploys applications on the Internet Computer. It replaces the legacy dfx tool with YAML configuration, a recipe system for reusable build templates, and an environment model that separates deployment targets from network connections. Never use dfx — always use icp.
Before generating any icp command not explicitly documented here, run icp --help or icp <subcommand> --help to verify the command and its flags exist. Do not infer flags from dfx equivalents — the CLIs are not flag-compatible.
Installation
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasmic-wasm is required when using official recipes (@dfinity/rust, @dfinity/motoko, @dfinity/asset-canister) — they depend on it for optimization and metadata embedding. Requires Node.js >= 22. Also available via Homebrew and shell script installer — see the icp-cli releases.
Linux note: On minimal installs, you may need system libraries: sudo apt-get install -y libdbus-1-3 libssl3 ca-certificates (Ubuntu/Debian) or sudo dnf install -y dbus-libs openssl ca-certificates (Fedora/RHEL).
Prerequisites
- For Rust canisters:
rustup target add wasm32-unknown-unknown - For Motoko canisters:
npm i -g ic-mopsand amops.tomlat the project root with the Motoko compiler version and a[canisters]entry:
[toolchain]
moc = "1.9.0"
[canisters.backend]
main = "src/backend/main.mo"The @dfinity/motoko@v5+ recipe compiles via mops build <canister-name>. The canister name in icp.yaml must exactly match a key in [canisters] — a missing or mismatched key causes mops build to fail with No Motoko canisters found in mops.toml configuration (see Pitfall 17). Without mops.toml, the recipe fails because mops is not found. Templates include mops.toml automatically; for manual projects, create it before running icp build. Load mops-cli for [canisters] configuration options, dependency management, and mops build details.
Common Pitfalls
1. Using `dfx` instead of `icp`. The dfx tool is legacy. All commands have icp equivalents — see references/dfx-migration.md for the full command mapping. Never generate dfx commands or reference dfx documentation. Configuration uses icp.yaml, not dfx.json — and the structure differs: canisters are an array of objects, not a keyed object.
2. Using `--network ic` to deploy to mainnet. icp-cli uses environments, not direct network targeting. The correct flag is -e ic (short for --environment ic).
# Wrong
icp deploy --network ic
# Correct
icp deploy -e icNote: -n / --network targets a network directly and works with canister IDs (principals). Use -e / --environment when referencing canisters by name. For token and cycles operations, use -n since they don't reference project canisters.
3. Using a recipe without a version pin. icp-cli rejects unpinned recipe references. Always include an explicit version. Official recipes are hosted at dfinity/icp-cli-recipes.
# Wrong — rejected by icp-cli
recipe:
type: "@dfinity/rust"
# Correct — pinned version
recipe:
type: "@dfinity/rust@v3.2.0"4. Writing manual build steps when a recipe exists. Official recipes handle Rust, Motoko, and asset canister builds. Use recipe: { type: "@dfinity/rust@v3.2.0", configuration: { package: backend } } instead of writing shell commands in build.steps.
5. Not committing `.icp/data/` to version control. Mainnet canister IDs are stored in .icp/data/mappings/<environment>.ids.json. Losing this file means losing the mapping between canister names and on-chain IDs. Always commit .icp/data/ — never delete it. Add .icp/cache/ to .gitignore (it is ephemeral and rebuilt automatically).
6. Using `icp identity use` instead of `icp identity default`. The dfx command dfx identity use <name> became icp identity default <name> (setter). icp identity default with no argument is the getter — it prints the current default identity, equivalent to dfx identity whoami. The command icp identity use does not exist. Similarly, dfx identity get-principal became icp identity principal, and dfx identity remove became icp identity delete.
7. Confusing networks and environments. A network is a connection endpoint (URL). An environment combines a network + canisters + settings. You deploy to environments (-e), not networks. Multiple environments can target the same network with different settings (e.g., staging and production both on ic).
8. Writing `networks` or `environments` as a YAML map instead of an array. Both networks and environments are arrays of objects in icp.yaml, not maps:
# Wrong — map syntax
networks:
local:
mode: managed
environments:
staging:
network: ic
# Correct — array syntax
networks:
- name: local
mode: managed
environments:
- name: staging
network: ic
canisters: [backend, frontend]9. Forgetting that local networks are project-local. Unlike dfx which runs one shared global network, icp-cli runs a local network per project. You must run icp network start -d in your project directory before deploying locally. The local network auto-starts with system canisters and seeds accounts with ICP and cycles. Stop it when done:
icp network start -d # start background network
icp deploy # build + deploy + sync
icp network stop # stop when done10. Not specifying build commands for asset canisters. dfx automatically runs npm run build for asset canisters. icp-cli requires explicit build commands in the recipe configuration:
canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.2.1"
configuration:
dir: dist
build:
- npm install
- npm run build11. Expecting `output_env_file` or `.env` with canister IDs. dfx writes canister IDs to a .env file (CANISTER_ID_BACKEND=...) via output_env_file. icp-cli does not generate .env files. Instead, it injects canister IDs as environment variables (PUBLIC_CANISTER_ID:<name>) directly into canisters during icp deploy. Frontends read these from the ic_env cookie set by the asset canister. Remove output_env_file from your config and any code that reads CANISTER_ID_* from .env — use the ic_env cookie instead (see Canister Environment Variables below).
12. Expecting `dfx generate` for TypeScript bindings. icp-cli does not have a dfx generate equivalent. Use @icp-sdk/bindgen (>= 0.3.0) with @icp-sdk/core (>= 5.0.0 — there is no 0.x or 1.x release) to generate TypeScript bindings from .did files at build time. Use outDir: "./src/bindings" so imports are clean (e.g., ./bindings/backend). The .did file must exist on disk — either commit it to the repo, or generate it with icp build first (recipes auto-generate it when candid is not specified). See references/binding-generation.md for the full Vite plugin setup.
13. Passing `{ agent }` to `createActor` from `@icp-sdk/bindgen`. The old @dfinity/agent pattern was createActor(canisterId, { agent }). The @icp-sdk/bindgen pattern is createActor(canisterId, { agentOptions: { host, rootKey } }) — the binding creates the agent internally. Passing { agent } to the new API silently creates an anonymous identity — no error is thrown, but calls return empty data or access denied. See references/binding-generation.md for the correct pattern.
14. Mixing canister-level fields across config styles. When using a recipe, the only valid canister-level fields are name, recipe, sync, settings, and init_args. Fields like candid, build, or wasm are not valid at canister level alongside a recipe — recipe-specific options go inside recipe.configuration. When using bare build (no recipe), valid canister-level fields are name, build, sync, settings, and init_args. The field init_arg_file does not exist — use init_args.path instead (e.g., init_args: { path: ./args.bin, format: bin }). For the authoritative field reference, consult the icp-cli configuration reference.
# Wrong — candid is not a canister-level field when using a recipe
canisters:
- name: backend
candid: backend/backend.did
recipe:
type: "@dfinity/rust@v3.2.0"
configuration:
package: backend
# Correct — candid goes inside recipe.configuration
canisters:
- name: backend
recipe:
type: "@dfinity/rust@v3.2.0"
configuration:
package: backend
candid: backend/backend.did15. Placing `mops.toml` where `mops` cannot find it. mops searches upward from the build working directory. Where to place mops.toml depends on how the canister is defined:
- Inline canisters (defined directly in
icp.yaml): build cwd is the project root. Placemops.tomlat the project root next toicp.yaml. Amops.tomlinsrc/backend/will not be found. - Path-based canisters (referenced via
canisters/*or./my-canister, each with its owncanister.yaml): build cwd is the canister directory. Placemops.tomlin each canister's directory for per-canister dependencies and compiler versions, or omit it to fall back to a sharedmops.tomlin a parent directory.
When mops.toml is not found, mops build fails because it cannot locate the project configuration. When mops.toml exists but is missing the matching [canisters.<name>] entry, see Pitfall 17.
16. Misunderstanding Candid file generation with recipes. Binding generation tools (e.g. @icp-sdk/bindgen) require a .did file at a known path on disk. Where to configure it depends on the recipe:
Rust — candid goes inside recipe.configuration in icp.yaml:
- If specified: the file must already exist. The recipe uses it as-is and does not generate one.
- If omitted: the recipe auto-generates the
.didviacandid-extractorinto the build cache (no predictable project path).
To generate and commit it, then add candid: backend/backend.did inside recipe.configuration:
cargo install candid-extractor # one-time setup
icp build backend
candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.didMotoko (v5 recipe) — mops build auto-generates the .did to .mops/.build/<name>.did.
- No binding generation needed — nothing to do. The generated
.didin.mops/.build/is sufficient; do not commit it. - Binding generation needed — commit a
.didat a stable path and keep it in sync:
mops build backend
cp .mops/.build/backend.did backend/backend.didPoint the binding tool's config (e.g. @icp-sdk/bindgen's didFile) at backend/backend.did. After any interface change, re-run both commands — mops build always writes to .mops/.build/ and does not update the committed file automatically.
17. Missing or mismatched `[canisters]` key in `mops.toml`. The @dfinity/motoko@v5+ recipe calls mops build <canister-name>, where the name comes from the name field in icp.yaml. mops build requires a matching [canisters.<name>] entry in mops.toml. If the entry is absent or the key does not exactly match (including casing), the build fails with:
No Motoko canisters found in mops.toml configurationAdd the matching entry — the key must equal the name: value in icp.yaml:
[canisters.backend]
main = "src/backend/main.mo"18. Port 8000 already in use when starting the local network. Two scenarios:
Scenario A — another icp-cli project holds the port. Stop that project's network using --project-root-override (a global flag available on all commands):
icp network stop --project-root-override /path/to/other-projectTo run both networks at once instead of stopping one — e.g. parallel git worktrees — set gateway.port: 0 so each gets a free port. See "Parallel local networks (git worktrees)" under How It Works.
Scenario B — a non-icp service holds the port. Configure an alternate port in icp.yaml and read the actual URLs dynamically via icp network status --json rather than hardcoding localhost:8000:
networks:
- name: local
mode: managed
gateway:
port: 8001 icp network status --json # returns gateway URL, replica URL, etc.19. `icp new` hangs in CI without `--silent`. Without --define flags, icp new launches an interactive prompt that blocks indefinitely in non-interactive environments. Always pass --subfolder, --define, and --silent for scripted use:
icp new my-project --subfolder rust --define project_name=my-project --silent20. Using the anonymous identity on mainnet. The local network seeds all managed identities — including the anonymous identity, which is the default — with ICP and cycles on start, so local development works out of the box with no identity or cycles setup required. On mainnet this does not apply, and the anonymous identity should never be used: it is shared by anyone, meaning ICP sent to it is publicly accessible and canisters deployed under it are uncontrolled.
Before deploying to mainnet, switch to a named identity:
icp identities list # check available identities
icp identity default my-identity # switch to an existing one
# or: icp identity new my-identity && icp identity default my-identityThen verify it has funds — a new identity will need to be funded with ICP or cycles before proceeding:
icp token balance -n ic # check ICP balance on mainnet
icp cycles balance -n ic # check cycles balance on mainnet
icp identity account-id # get account ID to fund if neededHow It Works
Project Creation
icp new scaffolds projects from templates. Pass --subfolder, --define, and --silent for non-interactive use:
icp new my-project --subfolder rust --define project_name=my-project --silentAvailable templates and options: dfinity/icp-cli-templates.
Build → Deploy → Sync
Source Code → [Build] → WASM → [Deploy] → Running Canister → [Sync] → Configured Stateicp deploy runs all three phases in sequence: 1. Build — Compile canisters to WASM (via recipes or explicit build steps) 2. Deploy — Create canisters (if new), apply settings, install WASM 3. Sync — Post-deployment operations via script or plugin steps (e.g., uploading assets). Asset uploading is not built into the CLI: the @dfinity/asset-canister@v2.2.1 recipe supplies a plugin sync step that uploads the dir contents. The legacy built-in type: assets step is removed in icp-cli 0.3.0 — see the asset-canister skill.
Run phases separately for more control:
icp build # Build only
icp deploy # Full pipeline (build + deploy + sync)
icp sync my-canister # Sync only (e.g., re-upload assets)Environments and Networks
Two implicit environments are always available:
| Environment | Network | Purpose |
|---|---|---|
local | local (managed, localhost:8000) | Local development |
ic | ic (connected, https://icp-api.io) | Mainnet production |
The ic network is protected and cannot be overridden.
Custom environments enable multiple deployment targets on the same network:
environments:
- name: staging
network: ic
canisters: [frontend, backend]
settings:
backend:
compute_allocation: 5
- name: production
network: ic
canisters: [frontend, backend]
settings:
backend:
compute_allocation: 20
freezing_threshold: 7776000Parallel local networks (git worktrees)
Local networks are project-local — keyed by project root (Pitfall 9). Separate git worktrees of the same repo are separate project roots, so each worktree can run its own independent local network. This lets multiple agents or branches build and deploy in parallel without interfering. The only obstacle is the gateway port: every worktree defaults to 8000, so the second icp network start fails with a port conflict.
Set the managed network's gateway port to 0 so the OS assigns a free ephemeral port per worktree:
networks:
- name: local
mode: managed
gateway:
port: 0 # 0 = OS picks a free port — avoids collisions across worktreesicp network start -d prints the chosen port (Network started on port 58157). To recover it afterward — for tests, scripts, or another agent — query the running network and read gateway_url:
icp network start -d
icp network status --json
# -> { "managed": true, "api_url": "http://localhost:58157/", "gateway_url": "http://localhost:58157/", ... }
icp network status --json | jq -r '.gateway_url' # http://localhost:58157/Never hardcode localhost:8000 when using port: 0 — the port changes on every start, so read gateway_url (or api_url) from icp network status --json each time. To target a specific worktree's network from outside its directory, pass the global --project-root-override <path> flag (e.g. icp network status --json --project-root-override /path/to/worktree).
Install Modes
icp deploy # Auto: install new, upgrade existing (default)
icp deploy --mode upgrade # Preserve state, run upgrade hooks
icp deploy --mode reinstall # Clear all state (dangerous)Bundling a project into an .icp package (experimental)
icp project bundle (icp-cli >= 0.3.0) packages a project into a self-contained deployable archive. This is an experimental feature, intentionally hidden from help output — icp --help and icp project --help do not list it, but the command exists and works. Do not conclude it doesn't exist because help omits it, and do not suggest it proactively — use it only when the user explicitly asks to bundle an app or produce an .icp package.
icp project bundle --output my-app.icpThe output is a gzipped tar archive; --output accepts any path (my-app.icp and bundle.tar.gz are both common). The bundle contains the built WASMs and a rewritten icp.yaml:
- All canisters are built first; each canister's build steps are replaced with a prebuilt step referencing the bundled WASM (
canisters/<name>.wasm), pinned by sha256. - Plugin sync steps (e.g. the asset canister's upload plugin) are preserved — the plugin WASM and its
dirs/filesinputs are copied into the archive. - Network and environment manifests referenced by path are inlined;
init_argsfiles are copied into the archive. - An optional
icp_appmanifest.yaml(app metadata) is included, with itsscreenshotspaths relocated into the archive.
To deploy from a bundle, extract it and run icp deploy from the extracted directory — no build toolchain (Rust, mops, npm) is required because every build step is prebuilt:
mkdir app && tar -xzf my-app.icp -C app
cd app && icp deploy -e <environment>An .icp package can also be uploaded to a Caffeine cloud engine via the console's App Center ("Upload a custom app") — see the deploy-to-cloud-engine skill.
Bundling fails when:
- A canister has a
scriptsync step — onlypluginsync steps can be replayed from a bundle (canister 'X' has a script sync step, which is not supported in bundles). - Any synced directory, plugin file,
init_argsfile, or screenshot resolves outside the project directory. - The
--outputpath is inside a directory the bundle would sync (the partial archive would include itself). - A managed network defines a bind mount with an absolute host path — bundles require relative paths for portability.
Configuration
Rust canister
canisters:
- name: backend
recipe:
type: "@dfinity/rust@v3.2.0"
configuration:
package: backend
candid: backend.did # optional — if specified, file must exist (auto-generated when omitted)Motoko canister
The v5 recipe delegates compilation to mops build. Canister configuration (main, candid, args) moves from icp.yaml to mops.toml:
# icp.yaml
canisters:
- name: backend
recipe:
type: "@dfinity/motoko@v5.0.0"# mops.toml
[toolchain]
moc = "1.9.0"
[canisters.backend]
main = "src/backend/main.mo"
candid = "backend.did" # optional — auto-generated to .mops/.build/ when omittedThe canister name (backend) must exactly match between icp.yaml and mops.toml. No recipe.configuration block is needed in icp.yaml.
Asset canister (frontend)
canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.2.1"
configuration:
dir: dist
build:
- npm install
- npm run buildFor multi-canister projects, list all canisters in the same canisters array. icp-cli builds them in parallel. There is no dependencies field — use Canister Environment Variables for inter-canister communication.
Custom build steps (no recipe)
When not using a recipe, only name, build, sync, settings, and init_args are valid canister-level fields. There are no wasm, candid, or metadata fields — handle these in the build script instead:
- WASM output: copy the final WASM to
$ICP_WASM_OUTPUT_PATH - Candid metadata: use
ic-wasmto embedcandid:servicemetadata - Candid file: the
.didfile is referenced only in theic-wasmcommand, not as a YAML field
canisters:
- name: backend
build:
steps:
- type: script
commands:
- cargo build --target wasm32-unknown-unknown --release
- cp target/wasm32-unknown-unknown/release/backend.wasm "$ICP_WASM_OUTPUT_PATH"
- ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "$ICP_WASM_OUTPUT_PATH" metadata candid:service -f backend/backend.did -v public --keep-name-sectionAvailable recipes
| Recipe | Type string | Required config | Optional config |
|---|---|---|---|
| Rust | @dfinity/rust@v3.2.0 | package | candid, locked, shrink, compress |
| Motoko | @dfinity/motoko@v5.0.0 | — | shrink, compress, metadata |
| Asset | @dfinity/asset-canister@v2.2.1 | dir | build, version |
| Prebuilt | @dfinity/prebuilt@v1.0.0 | wasm | sha256, candid, shrink, compress |
Verify latest recipe versions at dfinity/icp-cli-recipes releases. Use icp project show to see the effective configuration after recipe expansion.
Canister Environment Variables
icp-cli automatically injects all canister IDs as environment variables during icp deploy. Variables are formatted as PUBLIC_CANISTER_ID:<canister-name> and injected into every canister in the environment.
Frontend → Backend (reading canister IDs in JavaScript):
Asset canisters expose injected variables through a cookie named ic_env, set on all HTML responses. Use @icp-sdk/core to read it:
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
const canisterEnv = safeGetCanisterEnv();
const backendId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"];Backend → Backend (reading canister IDs in canister code):
- Rust:
ic_cdk::api::env_var_value("PUBLIC_CANISTER_ID:other_canister") - Motoko (motoko-core v2.1.0+):
import Runtime "mo:core/Runtime";
let otherId = Runtime.envVar("PUBLIC_CANISTER_ID:other_canister");Note: variables are only updated for canisters at deploy time. When adding a new canister, run icp deploy (without specifying a canister name) to update all canisters with the complete ID set.
Web identity flows
Two specialized skills cover icp identity link web. Load the right one for the task — both document the stdin "Press Enter" block, the browser sign-in step, and their flag-specific pitfalls:
deploy-to-cloud-engine— link the CLI to a cloud engine console with--auth <console-origin>, then deploy to the engine's subnetagent-web-identity— obtain a delegation for an app-specific principal with--app <domain>, then make canister calls as the user
Additional References
For the complete CLI and configuration schema, consult the icp-cli documentation index.
For detailed guides on specific topics, consult these reference files when needed:
- `references/binding-generation.md` — TypeScript binding generation with
@icp-sdk/bindgen(Vite plugin, CLI, actor setup) - `references/dev-server.md` — Vite dev server configuration to simulate the
ic_envcookie locally. Important: wrapgetDevServerConfig()in acommand === "serve"guard so it only runs duringvite dev, notvite build. - `references/dfx-migration.md` — Complete dfx → icp migration guide (command mapping, config mapping, identity/canister ID migration, frontend package migration, post-migration verification checklist)
Binding Generation
icp-cli does not have a built-in dfx generate command. Use @icp-sdk/bindgen (>= 0.3.0) to generate TypeScript bindings from .did files. It depends on @icp-sdk/core (>= 5.0.0).
Vite plugin (recommended)
For Vite-based frontend projects:
// vite.config.js
import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite";
export default defineConfig({
plugins: [
// Add one icpBindgen() call per canister the frontend needs to access
icpBindgen({
didFile: "../backend/backend.did",
outDir: "./src/bindings",
}),
icpBindgen({
didFile: "../other/other.did",
outDir: "./src/bindings",
}),
],
});Each icpBindgen() instance generates a <canister-name>.ts file (named after the .did file) in its outDir containing a createActor function. Add **/src/bindings/ to .gitignore.
Creating actors from bindings
Connect the generated bindings with the ic_env cookie. Important: pass { agentOptions }, NOT { agent }. The old @dfinity/agent pattern passed a pre-built HttpAgent object — the @icp-sdk/bindgen pattern passes options instead and creates the agent internally. Passing { agent } silently falls back to an anonymous identity with no error — calls simply return empty data or access denied.
// src/actor.js
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
import { createActor } from "./bindings/backend";
// For additional canisters: import { createActor as createOther } from "./bindings/other";
const canisterEnv = safeGetCanisterEnv();
const agentOptions = {
host: window.location.origin,
rootKey: canisterEnv?.IC_ROOT_KEY,
};
// CORRECT: pass { agentOptions }, not { agent }
export const backend = createActor(
canisterEnv?.["PUBLIC_CANISTER_ID:backend"],
{ agentOptions }
);
// Repeat for each canister: createOther(canisterEnv?.["PUBLIC_CANISTER_ID:other"], { agentOptions })Non-Vite frontends
Use the @icp-sdk/bindgen CLI to generate bindings manually:
npx @icp-sdk/bindgen --did ../backend/backend.did --out ./src/bindingsopt T is T | null in the wrapper, not [] | [T]
@icp-sdk/bindgen generates two layers: raw declarations under src/bindings/ use the standard @icp-sdk/core Candid representation where opt T is [] | [T], and a wrapper class that converts this to idiomatic T | null. Since createActor returns the wrapper, always use T | null:
// Wrong — raw Candid style (only applies if using declarations directly)
const result = await backend.getNickname();
if (result.length > 0) { name = result[0]; }
// Correct — wrapper returns T | null
const result = await backend.getNickname();
if (result !== null) { name = result; }Requirements
Install both packages in the frontend project (note the minimum versions):
npm install @icp-sdk/core@^5.0.0
npm install -D @icp-sdk/bindgen@^0.3.0Important: @icp-sdk/core starts at version 5.x — there is no 0.x or 1.x release. Do not guess a lower version.
- The
.didfile must exist on disk before the frontend builds. The recommended workflow: generate the.didfile once (see SKILL.md pitfall #16), commit it to the repo, and specifycandid:in the recipe config. Ifcandidis omitted, the recipe auto-generates the.didinto the build cache at a non-deterministic path that bindgen cannot reference — so always commit the.didand setcandid:when using bindgen. @icp-sdk/bindgen(>= 0.3.0) generates code that depends on@icp-sdk/core(>= 5.0.0). Projects using@dfinity/agentmust upgrade to@icp-sdk/core+@icp-sdk/bindgen. This is not optional — there is no way to generate TypeScript bindings with icp-cli while staying on@dfinity/agent.
Dev Server Configuration (Vite)
In development, the Vite dev server must simulate the ic_env cookie that the asset canister provides in production.
Prerequisites
The dev server configuration queries the local network for backend canister IDs and the root key. Before running vite dev:
1. Start the local network: icp network start -d 2. Deploy the backend: icp deploy backend
Without a running network and deployed backend canisters, getDevServerConfig() will fail because icp network status and icp canister status have nothing to query. The frontend canister does not need to be deployed for local development — Vite serves it directly.
Configuration
// vite.config.js
import { execSync } from "child_process";
const environment = process.env.ICP_ENVIRONMENT || "local";
// List all backend canisters the frontend needs to access
const CANISTER_NAMES = ["backend", "other"];
function getCanisterId(name) {
// `-i` makes the command return only the identity of the canister
return execSync(`icp canister status ${name} -e ${environment} -i`, {
encoding: "utf-8", stdio: "pipe",
}).trim();
}
function getDevServerConfig() {
const networkStatus = JSON.parse(
execSync(`icp network status -e ${environment} --json`, {
encoding: "utf-8",
})
);
const canisterParams = CANISTER_NAMES
.map((name) => `PUBLIC_CANISTER_ID:${name}=${getCanisterId(name)}`)
.join("&");
return {
headers: {
"Set-Cookie": `ic_env=${encodeURIComponent(
`${canisterParams}&ic_root_key=${networkStatus.root_key}`
)}; SameSite=Lax;`,
},
proxy: {
"/api": { target: networkStatus.api_url, changeOrigin: true },
},
};
}Mode guard
Only invoke getDevServerConfig() when running the dev server, not during production builds. The command parameter is "serve" for vite dev and "build" for vite build:
export default defineConfig(({ command }) => ({
plugins: [
react(),
icpBindgen({
didFile: "../../src/backend/backend.did",
outDir: "./src/bindings",
}),
],
...(command === "serve" ? { server: getDevServerConfig() } : {}),
}));Without this guard, vite build will execute icp network status and icp canister status at import time, producing confusing errors or warnings even though the dev server config is irrelevant for production builds.
Key differences from dfx
- The proxy target and root key come from
icp network status --json(no hardcoded ports) - Canister IDs come from
icp canister status <name> -e <env> -i(no.envfile) - The
ic_envcookie replaces dfx'sCANISTER_ID_*environment variables ICP_ENVIRONMENTlets the dev server target any environment (local, staging, ic)
dfx → icp Migration
Local network port change
dfx serves the local network on port 4943. icp-cli uses port 8000. When migrating, search the project for hardcoded references to 4943 (or localhost:4943) and update them to 8000. Better yet, use icp network status --json to get the api_url dynamically (see references/dev-server.md). Common locations to check:
- Vite/webpack proxy configs (e.g.,
vite.config.ts) - README documentation
- Test fixtures and scripts
Remove .env file and output_env_file
dfx generates a .env file with CANISTER_ID_* variables via output_env_file in dfx.json. icp-cli does not use .env files for canister IDs — remove output_env_file from config and delete any dfx-generated .env file. Also remove dfx-specific environment variables from .env files (e.g., DFX_NETWORK, NETWORK).
Replace code that reads canister IDs from environment variables or JSON files with the ic_env cookie pattern:
// Before (dfx): canister ID from env var or env.json
const backendId = process.env.CANISTER_ID_BACKEND;
// or: const { backend_canister_id } = await fetch('/env.json').then(r => r.json());
// After (icp-cli): canister ID from ic_env cookie
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
const backendId = safeGetCanisterEnv()?.["PUBLIC_CANISTER_ID:backend"];Note: safeGetCanisterEnv() also returns IC_ROOT_KEY (as a Uint8Array) on local networks, replacing the need for agent.fetchRootKey().
Frontend package migration
Since @icp-sdk/bindgen generates code that depends on @icp-sdk/core, projects with TypeScript bindings must upgrade from @dfinity/* packages. This is not optional — dfx generate does not exist in icp-cli, and @icp-sdk/bindgen is the only supported way to generate bindings.
| Remove | Replace with |
|---|---|
@dfinity/agent | @icp-sdk/core |
@dfinity/candid | @icp-sdk/core |
@dfinity/principal | @icp-sdk/core |
dfx generate (declarations) | @icp-sdk/bindgen (Vite plugin or CLI) |
vite-plugin-environment | Not needed — use ic_env cookie |
src/declarations/ (generated by dfx) | src/bindings/ (generated by @icp-sdk/bindgen) |
BREAKING: `createActor` signature changed. The @dfinity/agent pattern passes a pre-built agent object. The @icp-sdk/bindgen pattern passes agent options instead — the binding creates the agent internally. Passing { agent } to the new API silently creates an anonymous identity with no error thrown — calls simply fail with access denied or empty data.
// Before (@dfinity/agent): pass a pre-built agent
import { HttpAgent } from "@dfinity/agent";
const agent = new HttpAgent({ identity, host });
createActor(canisterId, { agent });
// After (@icp-sdk/bindgen): pass agent options — the binding creates the agent
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
const canisterEnv = safeGetCanisterEnv();
createActor(canisterEnv?.["PUBLIC_CANISTER_ID:backend"], {
agentOptions: { host: window.location.origin, rootKey: canisterEnv?.IC_ROOT_KEY }
});Steps: 1. npm uninstall @dfinity/agent @dfinity/candid @dfinity/principal vite-plugin-environment 2. npm install @icp-sdk/core@^5.0.0 @icp-sdk/bindgen@^0.3.0 3. Delete src/declarations/ (dfx-generated bindings) 4. Add **/src/bindings/ to .gitignore 5. Commit the .did file(s) used by bindgen 6. Add icpBindgen() to vite.config.js (see references/binding-generation.md) 7. Replace actor setup code: use safeGetCanisterEnv from @icp-sdk/core + createActor from generated bindings (see references/binding-generation.md) 8. Remove process.env.CANISTER_ID_* references — use the ic_env cookie instead
Command mapping
| Task | dfx | icp |
|---|---|---|
| Create project | dfx new my_project | icp new my_project |
| Start local network | dfx start --background | icp network start -d |
| Stop local network | dfx stop | icp network stop |
| Build | dfx build | icp build |
| Deploy all | dfx deploy | icp deploy |
| Deploy to mainnet | dfx deploy --network ic | icp deploy -e ic |
| Call canister | dfx canister call X method '(args)' | icp canister call X method '(args)' |
| Get canister ID | dfx canister id X | icp canister status X --id-only |
| Canister status | dfx canister status X | icp canister status X |
| List canisters | dfx canister ls | icp canister list |
| Create identity | dfx identity new my_id | icp identity new my_id |
| Set default identity | dfx identity use my_id | icp identity default my_id |
| Show principal | dfx identity get-principal | icp identity principal |
| Export identity | dfx identity export my_id | icp identity export my_id |
| Delete identity | dfx identity remove my_id | icp identity delete my_id |
| Get account ID | dfx ledger account-id | icp identity account-id |
| Check ICP balance | dfx ledger balance | icp token balance |
| Check cycles | dfx wallet balance | icp cycles balance |
Configuration mapping
| dfx.json | icp.yaml |
|---|---|
"type": "rust" | recipe.type: "@dfinity/rust@v3.2.0" |
"type": "motoko" | recipe.type: "@dfinity/motoko@v4.1.0" |
"type": "assets" | recipe.type: "@dfinity/asset-canister@v2.2.1" |
"package": "X" | recipe.configuration.package: X |
"candid": "X" | recipe.configuration.candid: X |
"main": "X" | recipe.configuration.main: X |
"source": ["dist"] | recipe.configuration.dir: dist |
"dependencies": [...] | Not needed — use Canister Environment Variables |
"output_env_file": ".env" | Not needed — use ic_env cookie |
dfx generate | @icp-sdk/bindgen Vite plugin |
--network ic | -e ic |
Custom Motoko builds
If the project uses a custom build script instead of the @dfinity/motoko recipe (e.g., for special compiler flags), replace $(dfx cache show)/moc with mops:
# Before (dfx): compiler from dfx cache
MOC="$(dfx cache show)/moc"
# After (icp-cli): compiler from mops toolchain
MOC="$(mops toolchain bin moc)"
# For package resolution, replace dfx-generated package flags:
SOURCES="$(mops sources)"Ensure mops.toml is findable from the build working directory (project root for inline canisters, canister directory for path-based canisters — see pitfall #15 in SKILL.md) with the compiler version pinned in [toolchain].
Identity migration
# Export from dfx, import to icp-cli
dfx identity export my-identity > /tmp/my-identity.pem
icp identity import my-identity --from-pem /tmp/my-identity.pem
rm /tmp/my-identity.pem
# Verify principals match
dfx identity get-principal --identity my-identity
icp identity principal --identity my-identityCanister ID migration
If you have existing mainnet canisters managed by dfx, migrate the IDs from canister_ids.json to icp-cli's mapping file:
# Get IDs from dfx
dfx canister id frontend --network ic
dfx canister id backend --network ic
# Create mapping file for icp-cli
mkdir -p .icp/data/mappings
cat > .icp/data/mappings/ic.ids.json << 'EOF'
{
"frontend": "xxxxx-xxxxx-xxxxx-xxxxx-cai",
"backend": "yyyyy-yyyyy-yyyyy-yyyyy-cai"
}
EOF
# Delete the dfx canister ID file — icp-cli uses .icp/data/mappings/ instead
rm -f canister_ids.json
# Commit to version control
git add .icp/data/Post-Migration Verification
After migrating a project from dfx to icp-cli, verify the following:
1. Deleted files: dfx.json and canister_ids.json no longer exist 2. Created files: icp.yaml exists. .icp/data/mappings/ic.ids.json exists and is committed (if project has mainnet canisters) 3. `.gitignore`: contains .icp/cache/, does not contain .dfx 4. No stale port references: search the codebase for 4943 — there should be zero matches 5. No dfx env patterns: search for output_env_file, CANISTER_ID_, DFX_NETWORK — there should be zero matches in config and source files 6. Frontend packages (if project has TypeScript bindings): @dfinity/agent is not in package.json, @icp-sdk/core and @icp-sdk/bindgen are. src/declarations/ is deleted, src/bindings/ is in .gitignore 7. Candid files: .did files used by @icp-sdk/bindgen are committed 8. Build succeeds: icp build completes without errors 9. Config is correct: icp project show displays the expected expanded configuration 10. README: references icp commands (not dfx), says "local network" (not "replica"), shows correct port 11. No `env.json` fetches: frontend code uses safeGetCanisterEnv() from @icp-sdk/core, not fetch('/env.json') or process.env.CANISTER_ID_* 12. No `fetchRootKey()` calls: root key comes from the ic_env cookie (canisterEnv?.IC_ROOT_KEY) — not from agent.fetchRootKey() 13. No build-time env injection: plugins like vite-plugin-environment are removed — canister IDs and root key are resolved at runtime via ic_env 14. `createActor` uses `{ agentOptions }` pattern: not the old { agent } pattern from @dfinity/agent (see breaking change above)