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-wasm
ic-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:
The[toolchain] moc = "1.9.0" [canisters.backend] main = "src/backend/main.mo"@dfinity/motoko@v5+recipe compiles viamops build <canister-name>. The canister name inicp.yamlmust exactly match a key in[canisters]— a missing or mismatched key causesmops buildto fail withNo Motoko canisters found in mops.toml configuration(see Pitfall 17). Withoutmops.toml, the recipe fails becausemopsis not found. Templates includemops.tomlautomatically; for manual projects, create it before runningicp build. Loadmops-clifor[canisters]configuration options, dependency management, andmops builddetails.
Common Pitfalls
-
Using
dfxinstead oficp. Thedfxtool is legacy. All commands haveicpequivalents — seereferences/dfx-migration.mdfor the full command mapping. Never generatedfxcommands or referencedfxdocumentation. Configuration usesicp.yaml, notdfx.json— and the structure differs: canisters are an array of objects, not a keyed object. -
Using
--network icto 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/--networktargets a network directly and works with canister IDs (principals). Use-e/--environmentwhen referencing canisters by name. For token and cycles operations, use-nsince they don’t reference project canisters. -
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.3.0" -
Writing manual build steps when a recipe exists. Official recipes handle Rust, Motoko, and asset canister builds. Use
recipe: { type: "@dfinity/rust@v3.3.0" }instead of writing shell commands inbuild.steps— the Rust recipe defaults the Cargopackageto the canister name, so no configuration is needed when they match (see Pitfall 21). -
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). If you have environments using a connected network that gets reset frequently you can add those specific environment mapping files to .gitignore. Never add the entire.icpor.icp/datadirectory to gitignore. -
Using
icp identity useinstead oficp identity default. The dfx commanddfx identity use <name>becameicp identity default <name>(setter).icp identity defaultwith no argument is the getter — it prints the current default identity, equivalent todfx identity whoami. The commandicp identity usedoes not exist. Similarly,dfx identity get-principalbecameicp identity principal, anddfx identity removebecameicp identity delete. -
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 onic). -
Writing
networksorenvironmentsas a YAML map instead of an array. Bothnetworksandenvironmentsare arrays of objects inicp.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] -
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 -din 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 done -
Not specifying build commands for asset canisters. dfx automatically runs
npm run buildfor 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 build -
Expecting
output_env_fileor.envwith canister IDs. dfx writes canister IDs to a.envfile (CANISTER_ID_BACKEND=...) viaoutput_env_file. icp-cli does not generate.envfiles. Instead, it injects canister IDs as environment variables (PUBLIC_CANISTER_ID:<name>) directly into canisters duringicp deploy. Frontends read these from theic_envcookie set by the asset canister. Removeoutput_env_filefrom your config and any code that readsCANISTER_ID_*from.env— frontends use theic_envcookie, and canister code reads the same variables at runtime (see Canister Environment Variables below and Pitfall 22). -
Expecting
dfx generatefor TypeScript bindings. icp-cli does not have adfx generateequivalent. 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.didfiles at build time. UseoutDir: "./src/bindings"so imports are clean (e.g.,./bindings/backend). The.didfile must exist on disk — either commit it to the repo, or generate it withicp buildfirst (recipes auto-generate it whencandidis not specified). Seereferences/binding-generation.mdfor the full Vite plugin setup. -
Passing
{ agent }tocreateActorfrom@icp-sdk/bindgen. The old@dfinity/agentpattern wascreateActor(canisterId, { agent }). The@icp-sdk/bindgenpattern iscreateActor(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. Seereferences/binding-generation.mdfor the correct pattern. -
Mixing canister-level fields across config styles. When using a recipe, the only valid canister-level fields are
name,recipe,sync,settings, andinit_args. Fields likecandid,build, orwasmare not valid at canister level alongside a recipe — recipe-specific options go insiderecipe.configuration. When using barebuild(no recipe), valid canister-level fields arename,build,sync,settings, andinit_args. The fieldinit_arg_filedoes not exist — useinit_args.pathinstead (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.3.0" # Correct — candid goes inside recipe.configuration canisters: - name: backend recipe: type: "@dfinity/rust@v3.3.0" configuration: candid: backend/backend.did -
Placing
mops.tomlwheremopscannot find it.mopssearches upward from the build working directory. Where to placemops.tomldepends 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.tomlis not found,mops buildfails because it cannot locate the project configuration. Whenmops.tomlexists but is missing the matching[canisters.<name>]entry, see Pitfall 17. - Inline canisters (defined directly in
-
Misunderstanding Candid file generation with recipes. Binding generation tools (e.g.
@icp-sdk/bindgen) require a.didfile at a known path on disk. Where to configure it depends on the recipe:Rust —
candidgoes insiderecipe.configurationinicp.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.didinsiderecipe.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 buildauto-generates the.didto.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 — generate a curated
.didat a stable path withmops generate candid:
This extracts the Candid interface directly from Motoko source without compiling WASM. Withmops generate candid backend[canisters.backend].candidset inmops.toml, it overwrites that file; otherwise it writes<name>.didnext tomain(e.g.main = "src/backend/main.mo"→src/backend/backend.did) and records the path inmops.toml. Point the binding tool’s config (e.g.@icp-sdk/bindgen’sdidFile) at that.did. Re-run after any interface change, and commit the.didandmops.tomltogether. Loadmops-clifor details. (The oldermops build backend+cp .mops/.build/backend.did …two-step still works but builds the full WASM just to extract the interface.)
-
Missing or mismatched
[canisters]key inmops.toml. The@dfinity/motoko@v5+recipe callsmops build <canister-name>, where the name comes from thenamefield inicp.yaml.mops buildrequires a matching[canisters.<name>]entry inmops.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 inicp.yaml:[canisters.backend] main = "src/backend/main.mo" -
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: 0so 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.yamland read the actual URLs dynamically viaicp network status --jsonrather than hardcoding localhost:8000:networks: - name: local mode: managed gateway: port: 8001icp network status --json # returns gateway URL, replica URL, etc. -
icp newhangs in CI without--silent. Without--defineflags,icp newlaunches an interactive prompt that blocks indefinitely in non-interactive environments. Always pass--subfolder,--define, and--silentfor scripted use:icp new my-project --subfolder rust --define project_name=my-project --silent -
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 needed -
Over-specifying (or wrongly omitting)
packagein the Rust recipe. As of@dfinity/rust@v3.3.0thepackageconfig is optional and defaults to the canisternameinicp.yaml. By convention, keep the canisternameequal to the[package] nameinCargo.tomland omitpackageentirely:# Preferred — name matches the Cargo package, so no package config canisters: - name: backend # matches [package] name = "backend" in Cargo.toml recipe: type: "@dfinity/rust@v3.3.0" # Required only when the names differ (e.g. a workspace crate) canisters: - name: backend recipe: type: "@dfinity/rust@v3.3.0" configuration: package: my-project-backend # actual [package] name in Cargo.tomlOmitting
packagewhen the names differ causescargo buildto fail with a package-not-found error — set it to the exact[package] name. On recipe versions beforev3.3.0,packagewas required in all cases. -
Hand-wiring canister IDs with setter methods or deploy scripts. Controller-only setters (
setBridge(principal)) called by a post-deploy script — or sibling IDs hardcoded insettings.environment_variablesor init args — are unnecessary:icp deployinjects every canister’s ID into every canister’s settings asPUBLIC_CANISTER_ID:<canister-name>, readable by canister code at runtime with the correct per-environment value (see Canister Environment Variables). Setter wiring is also more fragile: a--mode reinstallsilently wipes the stored pointer, while the automatic variables are re-stamped on every deploy.
How 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 --silent
Available templates and options: dfinity/icp-cli-templates.
Build → Deploy → Sync
Source Code → [Build] → WASM → [Deploy] → Running Canister → [Sync] → Configured State
icp deploy runs all three phases in sequence:
- Build — Compile canisters to WASM (via recipes or explicit build steps)
- Deploy — Create canisters (if new), apply settings, install WASM
- Sync — Post-deployment operations via
scriptorpluginsteps (e.g., uploading assets). Asset uploading is not built into the CLI: the@dfinity/asset-canister@v2.2.1recipe supplies apluginsync step that uploads thedircontents. The legacy built-intype: assetsstep is removed in icp-cli 0.3.0 — see theasset-canisterskill.
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: 7776000
Parallel 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 worktrees
icp 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.icp
The 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.3.0"
configuration:
candid: backend.did # optional — if specified, file must exist (auto-generated when omitted)
# package: my-crate # optional — only when the Cargo [package] name differs from the canister name
By convention the canister name must match the [package] name in Cargo.toml. When they match, package is optional (recipe >= v3.3.0 defaults it to the canister name). Set package explicitly only when the Cargo package name differs — e.g. a workspace with a differently named crate (see Pitfall 21).
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 omitted
The 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 build
For 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-section
Available recipes
| Recipe | Type string | Required config | Optional config |
|---|---|---|---|
| Rust | @dfinity/rust@v3.3.0 | — | package (defaults to canister name), candid, locked, shrink, compress, metadata |
| 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
During icp deploy, icp-cli injects the ID of every canister in the environment into every canister’s settings as PUBLIC_CANISTER_ID:<canister-name> (the principal in text form). These are canister settings env vars, readable at runtime by canister code, with per-environment values refreshed on every deploy — the same code is correct on local, staging, and mainnet with zero configuration.
Use these for inter-canister wiring instead of setter methods, init args, or post-deploy scripts (Pitfall 22). Read them lazily at call time — Motoko: Runtime.envVar<system>(...) (motoko-core v2.1.0+), Rust: ic_cdk::api::env_var_value(...) — see references/canister-env-vars.md for code and the first-install/reinstall behavior.
Frontend (JavaScript): asset canisters expose the variables through the ic_env cookie, set on all HTML responses. Read it with @icp-sdk/core:
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
const backendId = safeGetCanisterEnv()?.["PUBLIC_CANISTER_ID:backend"];
Caveats:
- Variables are only updated at deploy time. After adding a canister, run
icp deploy(without a canister name) so every canister gets the complete ID set. - User-defined variables (
settings.environment_variablesinicp.yaml) are static YAML strings shared across environments: a create-class install (first install or--mode reinstall) re-applies them, overwriting anything set withicp canister settings update --add-environment-variable. Re-assert environment-specific values (e.g. production origins) after such a deploy. - The replica caps an environment-variable value at 128 characters.
Full reference: icp-cli environment variables.
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/canister-env-vars.md— readingPUBLIC_CANISTER_ID:<name>from canister code at runtime (Motoko/Rust examples, lazy-read pattern)references/dev-server.md— Vite dev server configuration to simulate theic_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)