For AI agents: Documentation index at https://cli.internetcomputer.org/llms.txt

Skip to content
ICP CLI
Feedback welcome! Report issues on GitHub, ask questions on the Forum, or chat with us on Discord.

Migrating from dfx

This guide helps developers familiar with dfx transition to icp-cli.

Key Differences

Configuration Format

Aspectdfxicp-cli
Config filedfx.jsonicp.yaml
FormatJSONYAML
CanistersObject with canister names as keysArray of canister definitions

Deployment Model

dfx deploys to networks directly:

Terminal window
dfx deploy --network ic

icp-cli deploys to environments (which reference networks):

Terminal window
icp deploy --environment production
# or use the implicit ic environment:
icp deploy --environment ic
icp deploy -e ic

Environments add a layer of abstraction, allowing different settings for the same network.

Recipe System

icp-cli introduces recipes — reusable build templates. Instead of dfx’s built-in canister types, you reference recipes:

# dfx.json style (not supported)
"my_canister": {
"type": "rust",
"package": "my_canister"
}
# icp-cli style
canisters:
- name: my_canister
recipe:
type: "@dfinity/rust@v3.0.0"
configuration:
package: my_canister

Build Process

dfx has built-in build logic. icp-cli delegates to the appropriate toolchain as specified in the build configuration or through the use of a recipe.

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"

Build parallelism

dfx requires users to specify the inter canister dependencies so it can build canisters in order.

icp-cli assumes users will use canister environment variables to connect canisters and builds all canisters in parallel.

Local networks

Operationdfxicp-cli
Launching a local networkShared local network for all projectsLocal network is local to the project
System canistersRequires that you pass additional parameters to setup system canistersLaunches a network with system canisters and seeds accounts with ICP and Cycles
TokensUser must mint tokensAnonymous principal and local account are seeded with tokens
docker supportN/ASupports launching a dockerized network

Command Mapping

Taskdfxicp-cli
Create projectdfx new my_projecticp new my_project
Start local networkdfx start --backgroundicp network start -d
Stop local networkdfx stopicp network stop
Build canisterdfx build my_canistericp build my_canister
Deploy alldfx deployicp deploy
Deploy to mainnetdfx deploy --network icicp deploy -e ic
Call canisterdfx canister call my_canister method '(args)'icp canister call my_canister method '(args)'
Get canister IDdfx canister id my_canistericp canister status my_canister --id-only
List canistersdfx canister lsicp canister list
Canister statusdfx canister status my_canistericp canister status my_canister
Create identitydfx identity new my_idicp identity new my_id
Use identitydfx identity use my_idicp identity default my_id
Show principaldfx identity get-principalicp identity principal
Export identitydfx identity export my_idicp identity export my_id
Rename identitydfx identity rename old_id new_idicp identity rename old_id new_id
Delete identitydfx identity remove my_idicp identity delete my_id
Get account IDdfx ledger account-idicp identity account-id (--format ledger is default; use --format icrc1 for ICRC-1 format)

Converting dfx.json to icp.yaml

Basic Rust Canister

dfx.json:

{
"canisters": {
"backend": {
"type": "rust",
"package": "backend",
"candid": "src/backend/backend.did"
}
}
}

icp.yaml:

canisters:
- name: backend
recipe:
type: "@dfinity/rust@v3.0.0"
configuration:
package: backend
candid: "src/backend/backend.did"

Basic Motoko Canister

dfx.json:

{
"canisters": {
"backend": {
"type": "motoko",
"main": "src/backend/main.mo"
}
}
}

icp.yaml:

canisters:
- name: backend
recipe:
type: "@dfinity/motoko@v4.0.0"
configuration:
main: src/backend/main.mo
candid: src/backend/candid.did

Asset Canister

dfx.json:

{
"canisters": {
"frontend": {
"type": "assets",
"source": ["dist"]
}
}
}

icp.yaml:

canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
dir: dist

Note: dfx automatically builds frontend assets by looking for package.json and running npm run build. With icp-cli, you need to specify build commands explicitly if your assets need to be built:

canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
dir: dist
build:
- npm install
- npm run build

Multi-Canister Project

dfx.json:

{
"canisters": {
"frontend": {
"type": "assets",
"source": ["dist"],
"dependencies": ["backend"]
},
"backend": {
"type": "rust",
"package": "backend"
}
}
}

icp.yaml:

canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
dir: dist
build:
- npm install
- npm run build
- name: backend
recipe:
type: "@dfinity/rust@v3.0.0"
configuration:
package: backend

Key differences:

  • icp-cli doesn’t have explicit dependencies between canisters (dfx’s dependencies field)
  • Frontend build commands must be specified explicitly in icp-cli
  • Deploy order is determined automatically or you can deploy specific canisters

Network Configuration

Remote network example:

dfx.json:

{
"networks": {
"staging": {
"providers": ["https://icp-api.io"],
"type": "persistent"
}
}
}

icp.yaml:

networks:
- name: staging
mode: connected
url: https://icp-api.io
environments:
- name: staging
network: staging
canisters: [frontend, backend]

Testnet with root key:

dfx.json:

{
"networks": {
"testnet": {
"providers": ["https://testnet.example.com"],
"type": "persistent"
}
}
}

icp.yaml:

networks:
- name: testnet
mode: connected
url: https://testnet.example.com
root-key: 308182301d060d2b0601040182dc7c05030102... # Hex-encoded root key

Local network with custom bind address:

dfx.json:

{
"networks": {
"local": {
"bind": "127.0.0.1:4943",
"type": "ephemeral"
}
}
}

icp.yaml:

networks:
- name: local
mode: managed
gateway:
bind: 127.0.0.1
port: 4943

Key differences:

  • dfx’s "type": "persistent" maps to icp-cli’s mode: connected (external networks)
  • dfx’s "type": "ephemeral" maps to icp-cli’s mode: managed (local networks that icp-cli controls)
  • dfx’s "providers" array (which can list multiple URLs for redundancy) becomes a single url field in icp-cli
  • dfx’s "bind" address for local networks maps to icp-cli’s gateway.bind and gateway.port
  • Root key handling: dfx automatically fetches the root key from non-mainnet networks at runtime. icp-cli requires you to specify the root-key explicitly in the configuration for testnets (connected networks). For local managed networks, icp-cli retrieves the root key from the network launcher. The root key is the public key used to verify responses from the network. Explicit configuration ensures the root key comes from a trusted source rather than the network itself.

Note: icp-cli uses https://icp-api.io as the default IC mainnet URL, while dfx currently uses https://icp0.io. Both URLs point to the same IC mainnet, but https://icp-api.io is the recommended API gateway. The implicit ic network in icp-cli is configured with https://icp-api.io.

Features Not in icp-cli

Some dfx features work differently or aren’t directly available:

dfx Featureicp-cli Equivalent
dfx.json defaultsUse recipes or explicit configuration
Canister dependenciesUse bindings compatible with Canister Environment Variables
dfx generateUse language-specific tooling
dfx ledgericp token and icp cycles commands
dfx walletUse the proxy canister pattern for forwarding calls with cycles; use icp canister top-up to fund a canister and icp canister status to check its balance
dfx upgradeReinstall icp-cli

Replacing the dfx Wallet Canister

The dfx wallet canister served two main purposes:

  1. Funded canister creation and upgrades — dfx routed dfx deploy through the wallet so cycles could be attached to management canister calls.
  2. Forwarded calls with cyclesdfx canister call --with-cycles sent calls through the wallet, which attached cycles before forwarding.

icp-cli replaces both with the proxy canister pattern: a lightweight canister with a single proxy method that forwards calls and attaches cycles. For cycle account management, icp-cli has dedicated icp cycles commands.

What the dfx Wallet Had That the Proxy Doesn’t

The wallet canister had features the proxy doesn’t:

Featuredfx walleticp-cli proxy
Forwarding calls with cycleswallet_call()proxy()
Funding canister creationwallet_create_canister()✅ via proxy()
Sending cycles to a canisterwallet_send()Use icp cycles transfer
Address bookNot needed
Event log / historyNot needed
Custodian systemUse IC-level controllers
Web UIUse icp canister status

The features the proxy lacks are either superseded by icp-cli commands or not necessary in a controller-based workflow.

Migration Strategy: Reinstall the Wallet WASM

The recommended approach is to reinstall the existing wallet canister with the proxy WASM. This keeps the canister ID intact, which means:

  • Cycles are preserved — the cycle balance is IC-level state, not stored in WASM memory. Reinstalling the WASM does not touch it.
  • Managed canister controllers are preserved — any canister that lists the wallet as a controller continues to list the same canister ID (now the proxy). No controller updates needed.
  • Identity access is preserved — the wallet’s own controllers (your identity principal) remain unchanged.
  • Wallet-specific state is lost — the address book, event log, and custodian list are stored in WASM stable memory and are wiped on reinstall. Back these up if you need them.

Step-by-Step Migration

Before you start, record your wallet state:

Terminal window
# Note the wallet canister ID
WALLET_ID=$(dfx identity get-wallet --network ic)
echo "Wallet ID: $WALLET_ID"
# Note the cycle balance
dfx wallet balance --network ic

Step 1 — Download the proxy WASM.

Get it from the proxy-canister releases:

Terminal window
curl -L -o proxy.wasm \
https://github.com/dfinity/proxy-canister/releases/download/v0.1.0/proxy.wasm

Verify the SHA-256 matches the published checksum before proceeding.

Step 2 — Reinstall the wallet canister with the proxy WASM.

Terminal window
dfx canister install $WALLET_ID \
--mode reinstall \
--wasm proxy.wasm \
--yes \
--network ic

Step 3 — Verify the migration.

Check that cycles are intact and the proxy is running:

Terminal window
# Cycle balance should be nearly unchanged (only reinstall cost deducted, ~3B cycles)
dfx canister status $WALLET_ID --network ic
# Verify a managed canister still lists the (now-proxy) canister as controller
dfx canister info <your-managed-canister-id> --network ic

Step 4 — Add your icp-cli identity as a controller (if not already one).

The proxy only accepts calls from its controllers. Import your dfx identity into icp-cli (see Migrating Identities) and verify the principals match. If you use a different identity in icp-cli, add it as a controller while you still have dfx access:

Terminal window
ICP_PRINCIPAL=$(icp identity principal)
# Use dfx (while it still controls the proxy) to add the icp-cli identity
dfx canister update-settings $WALLET_ID --add-controller $ICP_PRINCIPAL --network ic

Step 5 — Use the proxy with icp-cli.

Set PROXY_ID to the former wallet canister ID:

Terminal window
export PROXY_ID=$WALLET_ID
# Deploy through the proxy
icp deploy -e ic --proxy $PROXY_ID
# Call a canister with cycles attached
icp canister call my-canister method '(args)' \
-e ic \
--proxy $PROXY_ID \
--cycles 500_000_000_000

Alternative: Deploy a Fresh Proxy

If you prefer to keep the wallet canister running (for example, to preserve the event log), deploy a separate proxy canister and gradually transition:

Terminal window
# 1. Create a new project from the proxy template
icp new my-proxy --subfolder proxy
cd my-proxy
icp deploy -e ic
# 2. For each managed canister, add the new proxy as a controller
PROXY_ID=$(icp canister status -e ic --id-only proxy)
dfx canister update-settings <canister-id> --add-controller $PROXY_ID --network ic
# 3. Switch to icp-cli using the new proxy
icp deploy -e ic --proxy $PROXY_ID
# 4. Transfer remaining wallet cycles to the proxy
dfx wallet balance --network ic # check remaining balance
dfx wallet send $PROXY_ID <amount> --network ic

The proxy starts with the cycles used for its initial deployment. Once you’ve verified everything works through the new proxy, transfer the remaining wallet balance into it to fully retire the old wallet. You can then optionally remove the wallet as a controller from your managed canisters.

Identity Considerations

The proxy enforces controller-based access: only principals listed as controllers of the proxy can call its proxy method.

Both dfx and icp-cli support importing identities from PEM files. After import, verify that the principal matches:

Terminal window
dfx identity get-principal --identity my-identity
icp identity principal --identity my-identity
# Both should print the same value

Migrating Identities

dfx identities can be imported into icp-cli. Both tools use compatible key formats and support the same storage modes.

Understanding Identity Storage

Both dfx and icp-cli support three storage modes:

  • Keyring (default): Stores private keys in your system keychain/keyring
  • Password-protected: Encrypts keys with a password in a file
  • Plaintext: Stores unencrypted keys in a file (not recommended except for CI/CD)

Default behavior: Both tools try to use the system keyring first. If unavailable, dfx falls back to password-protected files.

Identity Storage Locations

ToolIdentity DirectoryStructure
dfx~/.config/dfx/identity/Per-identity subdirectories:
<name>/identity.json (metadata)
<name>/identity.pem (key, if not in keyring)
icp-climacOS: ~/Library/Application Support/org.dfinity.icp-cli/identity/
Linux: ~/.local/share/icp-cli/identity/
Windows: %APPDATA%\icp-cli\data\identity\
Centralized files:
identity_list.json (all identities)
identity_defaults.json (default selection)
keys/<name>.pem (keys, if not in keyring)

Private key storage (both tools): System keyring (default), or encrypted/plaintext PEM files

Note: dfx and icp-cli use different service names in the system keyring (internet_computer_identities vs icp-cli), so identities must be explicitly migrated using the import/export process described below.

Checking Your dfx Identity Storage Mode

To see how your dfx identity is stored:

Terminal window
cat ~/.config/dfx/identity/<name>/identity.json

Look for:

  • "keyring_identity_suffix": "<name>" → Stored in system keyring
  • "encryption": {...} → Password-protected file
  • No identity.json or neither field present → Plaintext file

Import dfx Identities

The import process depends on your dfx identity’s storage mode.

Legacy PEM files: icp identity import automatically handles non-conforming PKCS#8 PEM files generated by very old versions of dfx. No special flags are needed.

For Keyring or Password-Protected Identities

Export from dfx first (this works for both storage types):

Terminal window
# Export from dfx (will prompt for password if encrypted)
dfx identity export my-identity > /tmp/my-identity.pem
# Import to icp-cli (uses keyring by default)
icp identity import my-identity --from-pem /tmp/my-identity.pem
# Clean up temporary file
rm /tmp/my-identity.pem
# Verify the principal matches
dfx identity get-principal --identity my-identity
icp identity principal --identity my-identity

Both commands should display the same principal.

For Plaintext Identities

If your dfx identity is stored as plaintext (has identity.pem file with no encryption):

Terminal window
# Direct import from dfx location
icp identity import my-identity \
--from-pem ~/.config/dfx/identity/my-identity/identity.pem
# By default, icp-cli will store securely in keyring
# To keep as plaintext (not recommended):
icp identity import my-identity \
--from-pem ~/.config/dfx/identity/my-identity/identity.pem \
--storage plaintext

Choosing Storage Mode in icp-cli

When importing, you can specify how icp-cli should store the private key:

Terminal window
# System keyring (default, recommended)
icp identity import my-id --from-pem key.pem --storage keyring
# Password-protected file
icp identity import my-id --from-pem key.pem --storage password
# Plaintext file (not recommended for production)
icp identity import my-id --from-pem key.pem --storage plaintext

If keyring is unavailable, icp-cli will prompt for a password to use password-protected storage.

Migrate All Identities

To migrate all dfx identities at once:

Terminal window
# Export and import each identity
for id in $(dfx identity list | grep -v "^anonymous"); do
echo "Migrating $id..."
# Export from dfx (handles all storage types)
dfx identity export "$id" > "/tmp/${id}.pem"
# Import to icp-cli (uses keyring by default)
icp identity import "$id" --from-pem "/tmp/${id}.pem"
# Clean up
rm "/tmp/${id}.pem"
# Verify principals match
echo " dfx principal: $(dfx identity get-principal --identity "$id")"
echo " icp-cli principal: $(icp identity principal --identity "$id")"
echo ""
done
# List all imported identities
icp identity list

Note: This script copies identities to icp-cli without removing them from dfx. Your original dfx identities remain intact and both tools can be used side-by-side. The script will prompt for passwords if any dfx identities are password-protected or stored in keyring.

Setting the Default Identity

After importing, set your default identity:

Terminal window
icp identity default my-identity

Migration Checklist

A complete migration involves these steps:

1. Create icp.yaml

Create icp.yaml in your project root using the conversion examples above.

2. Migrate Identities

Import the identities you use for this project:

Terminal window
icp identity import deployer --from-pem ~/.config/dfx/identity/deployer/identity.pem

3. Test Locally

Terminal window
icp network start -d
icp build
icp deploy
icp canister call my-canister test_method '()'

4. Migrate Canister IDs (Optional)

If you have existing canisters on mainnet that you want to continue managing with icp-cli, create a mapping file to preserve their IDs.

icp-cli uses different storage paths based on network type:

  • Connected networks (ic, mainnet): .icp/data/mappings/<environment>.ids.json
  • Managed networks (local): .icp/cache/mappings/<environment>.ids.json

Important: Unlike .dfx/ (which was typically gitignored entirely), .icp/data/ contains your mainnet canister ID mappings and should be committed to version control. Only .icp/cache/ should be gitignored. Losing these mappings means you’ll need to manually look up your canister IDs.

For the ic environment, create .icp/data/mappings/ic.ids.json:

{
"frontend": "xxxxx-xxxxx-xxxxx-xxxxx-cai",
"backend": "yyyyy-yyyyy-yyyyy-yyyyy-cai"
}

Get the canister IDs from your dfx project:

Terminal window
# dfx stores IDs in different locations depending on network type:
# - Persistent networks: canister_ids.json (project root)
# - Ephemeral networks: .dfx/<network>/canister_ids.json
# For mainnet/ic network:
dfx canister id frontend --network ic
dfx canister id backend --network ic

5. Verify Mainnet Access

Terminal window
# Check you can reach IC mainnet
icp network ping ic
# Verify identity has correct principal
icp identity principal
# Check canister status (if you migrated IDs)
icp canister status my-canister -e ic

6. Update CI/CD

Replace dfx commands with icp-cli equivalents in your CI/CD scripts:

Before (dfx):

steps:
- run: dfx start --background
- run: dfx deploy
- run: dfx deploy --network ic

After (icp-cli):

steps:
- run: icp network start -d
- run: icp deploy
- run: icp deploy -e ic

7. Update Documentation

Update any project documentation that references dfx commands.

Keeping Both Tools

During migration, you can use both tools side-by-side with some considerations:

What works side-by-side:

  • Configuration files: dfx uses dfx.json, icp-cli uses icp.yaml (no conflicts)
  • Identities: Both store identities separately (dfx uses internet_computer_identities keyring service, icp-cli uses icp-cli), so they don’t interfere with each other
  • Canister IDs: Stored in different locations (.dfx/ vs .icp/), no conflicts
  • Remote networks: Both can deploy to IC mainnet independently

Potential conflicts:

  • ⚠️ Local networks: Both default to localhost:8000 for local development networks
    • If running both local networks simultaneously, they will conflict on port 8000
    • Solution: Configure icp-cli to use a different port by overriding the local network:
      icp.yaml
      networks:
      - name: local
      mode: managed
      gateway:
      port: 8001 # Use different port from dfx
    • Or stop dfx’s local network before starting icp-cli’s: dfx stop then icp network start

This allows gradual migration without disrupting existing workflows, as long as you manage local network ports.

Getting Help