Manage Motoko projects with the mops CLI — toolchain pinning, dependency management, type-checking, building, and linting. Use when working with mops.toml, mops.lock, running mops commands, adding/removing packages, pinning moc or lintoko versions, checking or building canisters, configuring moc flags, or setting up a new Motoko project.
Scanned 9/7/2026
Install to Claude Code
npx -y skills add jiayaoqijia/cryptoskill --skill dfinity-official-mops-cli --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Dfinity Official Mops Cli?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jiayaoqijia-dfinity-official-mops-cli)More formats (shields.io, HTML) on the badges page.
---
name: mops-cli
description: "Manage Motoko projects with the mops CLI — toolchain pinning, dependency management, type-checking, building, and linting. Use when working with mops.toml, mops.lock, running mops commands, adding/removing packages, pinning moc or lintoko versions, checking or building canisters, configuring moc flags, or setting up a new Motoko project."
license: Apache-2.0
compatibility: "mops >= 3.1.0"
metadata:
title: Mops CLI
category: Infrastructure
---
# Mops CLI
Opinionated guide for Motoko projects. Covers project config, dependency management, type-checking, building, and linting.
## Key Principles
1. **No dfx** — mops neither invokes nor supports `dfx`. There is no `mops toolchain init`, no `moc-wrapper`, and no `mops watch --generate` / `--deploy`. Always pin `moc` in `[toolchain]`; every command that compiles requires it. Use the newest `moc` version. Pin `pocket-ic` too if you have replica tests, benchmarks, or `--check-deploy` — with no pin those commands error, naming `mops toolchain use pocket-ic 15.0.0`. When `MOPS_POCKET_IC_URL` points at an already-running PocketIC server, no pin is needed and an existing pin is ignored with a warning — do not add one to silence it; unset the variable instead.
2. **No `mo:base`** — it is deprecated. Always use `mo:core` (`import Array "mo:core/Array"`).
3. **All config in `mops.toml`** — canisters, moc flags, toolchain versions, build settings.
4. **Canister-centric workflow** — define all canisters in `[canisters]`; never pass file paths to `mops check`. Exception: library packages (no `[canisters]`) use file paths directly: `mops check src/**/*.mo`.
## Project Setup
### Minimal `mops.toml`
```toml
[toolchain]
moc = "1.7.0"
lintoko = "0.10.0"
pocket-ic = "15.0.0" # required for replica tests / benchmarks / --check-deploy
[dependencies]
core = "2.5.0"
[moc]
args = ["--default-persistent-actors", "-W=M0223,M0236,M0237"]
[canisters.backend]
main = "src/backend/main.mo"
[canisters.backend.migrations]
chain = "src/backend/migrations"
check-limit = 10 # optional — speeds up `mops check` when the chain gets long
[canisters.backend.check-stable]
path = "deployed/backend.most"
[build]
outputDir = "src/backend/dist"
args = ["--release"]
check-wasm = true # optional: analyze final Wasm complexity
check-deploy = true # optional: verify fresh PocketIC installation after build
# Opt-in Wasm optimization (Binaryen wasm-opt) for build + bench
[optimize]
# level = "O3" # default
# keep-names = true # default
# wasm-opt pin: [toolchain] wasm-opt = "131" (required when [optimize] is set)
```
`check-stable` runs ICP's upgrade-time stable-variable compatibility check locally, so incompatible changes fail in `mops check` instead of being rejected when upgrading a live canister. It compares the current code against a `.most` from the deployed version.
Bootstrap that `.most`: new project → `mops deployed init` (empty-actor baseline); already-deployed canister → build from the deployed commit, then `mops deployed`. After every deploy, run `mops deployed` to promote the just-built `.most` (see [`mops deployed`](#mops-deployed) below).
Optional canister fields: `candid` (path to .did for compatibility checking), `initArg` (Candid-encoded init args).
### Warning Flags
`-W=M0223,M0236,M0237` — redundant type instantiation (M0223), suggest contextual dot notation (M0236), suggest redundant explicit arguments (M0237). These are allowed (disabled) by default; `-W=` enables them as warnings.
### Moc Args Layering
Flags are applied in this order (later overrides earlier):
1. `[moc].args` — global, all commands (check, build, test, bench, etc.)
2. `[build].args` — build only (e.g. `--release`)
3. `[canisters.<name>.migrations]` — auto-injected `--enhanced-migration` (managed by mops)
4. `[canisters.<name>].args` — per-canister
5. CLI `-- <flags>` — one-off overrides; supported by `mops check`, `mops build`, `mops check-stable`, `mops generate`, `mops migrate`, `mops test`, and `mops bench`
## Core Commands
### `mops install`
```bash
mops install # dev flow: keeps mops.lock in sync, self-heals a broken one
mops install --locked # CI flow: fail if mops.lock is missing or would change
```
Run after cloning or after manual `mops.toml` edits. `mops.lock` is always maintained — there is no flag to opt out, and no `--lock` flag (removed in v3). A missing, unparseable, legacy-format or `mops.toml`-inconsistent lock is regenerated by a plain `mops install`, including locks that still carry absolute local paths from older CLIs.
`--locked` requires an up-to-date lock and never writes it. It is also available on every implicitly-installing command (`mops build`, `mops check`, `mops check-stable`, `mops check-candid`, `mops test`, `mops bench`, `mops generate candid`), so CI can run `mops test --locked` with no prior install. `mops sources` has no `--locked` (a packtool caller parses its stdout mid-build) — put `mops install --locked` earlier in the pipeline instead.
The `CI` env var no longer affects lockfile behavior (removed in v3). Commit `mops.lock` — for applications and libraries alike.
A lock also goes stale when a local `path` dependency's own `mops.toml` changes — including one nested further down the chain. `mops install` regenerates it; `--locked` fails until the regenerated lock is committed. Note the first `mops install` after upgrading regenerates the lock of any project that has a `path` dependency, so commit it before running `--locked` in CI. Projects without `path` dependencies keep their existing lock.
`{MOPS_ENV}` in a `path` dependency expands to `$MOPS_ENV` (default `local`), which makes the lock environment-specific. Switching `MOPS_ENV` makes it stale: `mops install` regenerates it, and `--locked` fails with a message naming `MOPS_ENV`. Keep one lock per environment, or drop `--locked` — a lock generated under one `MOPS_ENV` will not satisfy `--locked` under another.
Integrity is verified at download time, so `mops install` no longer re-hashes `.mops/`: editing a dependency in place will not fail the next install. Use `mops verify` for the on-demand on-disk audit.
Downloaded files are always checked before anything enters the cache — against `mops.lock` when it already records the package, otherwise against the registry. A committed lock therefore makes verification free, which is why a clean checkout installs without asking the registry about hashes.
Two consequences worth knowing: a corrupt or hand-edited `mops.lock` now fails a _download_ (the error names `mops.lock` as a possible culprit — restore it from version control; already-cached packages are unaffected), and a package the registry publishes no hashes for still installs, unverified, with a warning.
Packages download in parallel through a bounded pool. `mops install --concurrency <n>` or the `MOPS_CONCURRENCY` env var (works on every installing command) caps simultaneous registry requests; the default derives from the CPU count and the file-descriptor soft limit (4–16). Transient network errors (`fetch failed`, `ECONNRESET`, `EMFILE`) retry automatically with the concurrency halved, up to twice. Set `MOPS_CONCURRENCY=1` only if installs still fail after the retries (an egress proxy capping connections, for example).
### `mops verify`
```bash
mops verify # re-hash .mops/ against mops.lock, and mops.lock against the registry
```
Covers GitHub dependencies as well as registry ones.
Exits 1 with the offending files and a recovery hint. This is the replacement for anyone who relied on `mops install` failing when `.mops/` had been modified.
### `mops add <package>`
```bash
mops add core # latest version
mops add core@2.5.0 # specific version
mops add --dev test # dev dependency
mops add org/repo # GitHub shorthand (also accepts a full github.com url)
mops add ./pkg # local package directory
```
Updates `mops.toml` and `mops.lock`.
Adding a package that is already declared in the other section **moves** it rather than declaring it twice. `<pkg>@<version>` replaces the declared version and reports what it replaced; it leaves pinned aliases like `"core@1.0.0" = "1.0.0"` alone, and there is no flag to create one — write it by hand.
### `mops check`
Primary correctness command — runs moc check, then check-stable (if configured), then lint (if lintoko is in toolchain).
On moc 1.12.0+, canisters with `[migrations]` get stricter upgrade diagnostics: a field the initial actor requires that no migration produces fails as an `M0267` error instead of only warning (`M0254`), and compat errors carry a source location. **Temporarily disabled** — `moc --stable-baseline` is buggy, so every pin runs the pre-1.12.0 check. Older moc pins and canisters without `[migrations]` are unaffected either way.
The `check-stable` baseline is always a `.most` file — as `[canisters.<name>.check-stable].path` or as the `mops check-stable <baseline.most>` argument. A `.mo` source is rejected. See [`mops deployed`](#mops-deployed) for where the baseline comes from — that differs between a fresh project and an already-deployed canister.
```bash
mops check # all canisters
mops check backend # single canister
mops check --fix # autofix + check + stable + lint
mops check --no-lint # skip the lint step for one run
mops check --verbose # show moc invocations
mops check -- -Werror # treat warnings as errors
```
**Always use canister names, not file paths.** Per-canister args from `mops.toml` are applied automatically.
`--fix` applies machine-applicable fixes from both moc and lintoko in one pass. Concurrent `--fix` runs (across processes) serialize automatically via an advisory lock at `.mops/fix.lock` — safe to invoke from multiple agents on the same project. Read-only files (e.g. frozen migrations) are skipped with a warning, not fixed.
### `mops build`
```bash
mops build # all canisters
mops build backend # single canister
mops build --verbose # show compiler commands
mops build --check-wasm # analyze final Wasm complexity without PocketIC
mops build --no-check-wasm # skip configured [build].check-wasm once
mops build --check-deploy # verify fresh installation on PocketIC
mops build --no-check-deploy # skip configured [build].check-deploy once
mops build -- --ai-errors # pass extra moc flags
```
Produces `.wasm`, `.did`, and `.most` files in `[build].outputDir` (default `.mops/.build`).
With `[optimize]` in `mops.toml`, runs `wasm-opt` after candid metadata (default `-O3 -g`). Requires a Binaryen pin: `mops toolchain use wasm-opt 131`. Build commands never write the pin themselves, and a `wasm-opt` failure fails the build. Pass `--no-optimize` (on `build` or `bench`) to skip the pass for a single run without editing `mops.toml`.
When `--check-wasm` or `[build].check-wasm = true` is enabled, Mops runs fast Walrus analysis on the final Wasm without starting PocketIC. Per-function IC0505 complexity below 750,000 is quiet, 750,000 through 899,999 emits an early warning, and 900,000 or more emits a critical warning. `MOPS-WASM-COMPLEXITY` output includes actionable function metrics, the three largest complexity contributors, and Motoko correction guidance. The estimate never fails the build. Use `--no-check-wasm` to skip configured analysis once.
### `mops deployed`
Post-deploy hook — keeps the on-disk `.most` baseline used by `check-stable` in sync with what's actually deployed.
```bash
mops deployed init backend # one-time bootstrap: empty-actor baseline + sets [check-stable].path
mops deployed backend # post-deploy: promotes .mops/.build/backend.most → deployed/backend.most
mops deployed # all canisters
```
Default destination is `deployed/<name>.most`; override with `[deployed].dir` in `mops.toml` or `--dir`. It reads built `.most` files from `[build].outputDir` (default `.mops/.build`); override with `--build-dir`. `mops deployed` errors if the source `.most` is missing — it never regenerates. Run it from your deploy pipeline immediately after a successful deploy.
### `mops generate candid`
```bash
mops generate candid # all canisters
mops generate candid backend # single canister
mops generate candid backend -o <path> # single canister, ad-hoc path
```
(Re)generates the curated `.did` from current Motoko source. With `[canisters.<name>].candid` set, overwrites that file. Without it, writes `<name>.did` next to `main` (e.g. `main = "src/Backend.mo"` → `src/backend.did`) and sets `[canisters.<name>].candid` in `mops.toml`. Run after every interface change; commit `.did` + `mops.toml` together. Same moc invocation as `mops build`, so the result always passes `mops build`'s subtype check.
### `mops toolchain`
```bash
mops toolchain use moc 1.7.0 # pin specific version
mops toolchain use moc latest # pin latest version (non-interactive)
mops toolchain use lintoko 0.10.0 # pin specific version
mops toolchain use pocket-ic 15.0.0 # pin for replica tests / benchmarks / --check-deploy
mops toolchain use wasm-opt 131 # Binaryen for [optimize] (or `latest`)
mops toolchain update moc # update to latest (requires existing [toolchain] entry)
mops toolchain update # update all tools to latest
mops toolchain info <tool> # show release info (latest, pinned, history)
mops toolchain info <tool> --versions # list recent stable releases, newest first
mops toolchain info <tool> --versions --all # full stable history (cache warming)
mops toolchain bin moc # print path to binary
```
**`pocket-ic` versions**: pin anything from `9.0.0` up, `latest` included — mops keeps no list of blessed versions. Pins below `9.0.0` error with a migration message (they ran on the legacy client that mops 3.0.0 removed). With no pin, replica tests / `mops bench` / `--check-deploy` / `mops toolchain bin pocket-ic` error naming `mops toolchain use pocket-ic 15.0.0`. That version is a hint, not a fallback.
**Agent note**: `toolchain use <tool>` without a version opens an interactive picker — do not use in scripts or agents. Always pass a version or `latest`. `toolchain update` only works when the tool already has a `[toolchain]` entry. `toolchain info <tool> --versions` works without `mops.toml` (first GitHub page by default; pass `--all` for full history).
### Enhanced migrations
When `[canisters.<name>.migrations]` is configured, `mops check`, `mops build`, and `mops check-stable` automatically inject `--enhanced-migration`. Do not add `--enhanced-migration` to `[canisters.<name>].args` — mops will error.
Create migration files directly in the `chain` directory.
After `mops check --fix` (or `mops check <canister>`) confirms the chain compiles, run `mops build` to produce the wasm artifact.
Use `mops build --check-deploy`, or set `[build].check-deploy = true` for every build, to install each built Wasm on a fresh PocketIC canister and catch module validation, initialization, and installation failures. Requires `[toolchain] pocket-ic` (a version from `9.0.0` up, or a local binary path) — or set `MOPS_POCKET_IC_URL` to an already-running PocketIC server (the pin is then ignored). Unpinned with no URL, the build errors naming `mops toolchain use pocket-ic 15.0.0`. Use `--no-check-deploy` to skip configured validation once. The command uses each canister's `initArg`, or `()` when omitted. Set `wasmMemoryLimit` to a positive integer byte limit on a canister to check deployment under that limit. PocketIC errors are reported as provided by the client, and installation failures are collected across canisters. Before installation, Mops runs `moc --stable-compatible` from a temporary empty-actor `.most` to each generated `.most`. If moc reports incompatibility, Mops emits `MOPS-CHECK-DEPLOY-SKIPPED` with the compiler diagnostic and does not check that canister on fresh PocketIC. Eligible siblings are still checked; validate the skipped upgrade against representative baseline state.
`check-limit` (optional) caps how many recent chain files `mops check` and `mops lint` consider — useful when the chain grows long and re-checking every old migration slows feedback down. `mops build` is unaffected by `check-limit`. When the limit kicks in, mops stages the included files into `.migrations-<canister>/` next to the `chain` directory (auto-`.gitignore`d). `moc` diagnostics may then print paths there — the real file lives in the `chain` directory with the same name.
Override `check-limit` for a single run with `--no-check-limit` (`mops check`, `mops check-stable`, `mops lint`) — e.g. `mops check --fix --no-check-limit` to autofix older, normally-trimmed migrations. On `mops check` and `mops check-stable`, `--no-check-limit` also suppresses the pending-migration warning.
When `check-limit` is set, `mops check-stable` (and the stable check inside `mops check`) reports if more migrations are pending than the limit allows — as an error if compat failed (replacing the misleading `moc` message), otherwise a warning.
### `mops remove <package>`
```bash
mops remove core
```
Removes from whichever section declares the package; `--dev` limits it to `[dev-dependencies]`. A package declared in both sections is removed from both.
### Dependency Management
```bash
mops outdated # list outdated deps (caret-bound); exit 1 if any, 2 if the check failed
mops outdated core # check a single package
mops update # rewrite mops.toml versions within caret bound (no major-version crossing)
mops update core # update specific package within caret bound
mops update --major # allow updates that cross major versions
mops update --patch # restrict to patch bumps only (mutually exclusive with --major)
mops update --verbose # verbose output
mops sync # add missing / remove unused packages
mops sync --dry-run # print what would change, write nothing
```
`mops update` rewrites `mops.toml` in place (like `cargo upgrade`, not `cargo update`) and re-pins GitHub dependencies to their branch head. Like `mops outdated`, it exits `2` when it cannot run or complete — no `mops.toml`, a package that is not declared, or a dependency that failed to update (the other dependencies are still updated).
`mops sync` needs a pinned `[toolchain] moc` — it reads imports with `moc --print-deps`. Packages imported only from `test`/`tests`/`bench`/`benchmark` directories are added to `[dev-dependencies]`; already-declared packages are never moved between sections.
## Other Commands
### `mops publish`
```bash
mops publish # publish to the registry (runs tests/docs/bench by default)
mops publish --dry-run # same local steps as publish; no registry contact / identity
mops publish --dry-run --no-test --no-docs --no-bench # packaging checks only
mops publish --no-test --no-docs --no-bench
```
`--dry-run` runs the same local publish pipeline (packaging checks, docs, changelog, tests, benchmarks) and prints the final file list, then stops before identity/upload. `--no-*` flags work as usual. It does **not** run canister config validation (SPDX/semver/name rules) or prove registry acceptance (already published, permissions, missing deps).
### `mops test`
Tests live in `test/*.test.mo`:
```bash
mops test # run all tests
mops test my-test # filter by name
mops test --mode wasi # use wasmtime (for to_candid/from_candid)
mops test --reporter files # one line per file (default is verbose)
mops test --watch # re-run on file changes
mops test -- -Werror # pass extra moc flags
```
Replica tests (actor files or `// @testmode replica`) run on PocketIC — the `pocket-ic` version from `[toolchain]`, or an already-running server when `MOPS_POCKET_IC_URL` is set (no pin needed; canister log output is not streamed in attached mode). Unpinned with no URL, they error naming `mops toolchain use pocket-ic 15.0.0`. Same for `mops bench` and `mops watch --test`. There is no `--replica` flag and no dfx replica.
### `mops bench`
Benchmarks live in `bench/*.bench.mo`:
```bash
mops bench # run all benchmarks
mops bench my-bench # filter by name
mops bench --gc incremental # select GC
mops bench --save # save results to .bench/<name>.json
mops bench --compare # compare with saved results
mops bench -- -Werror # pass extra moc flags
```
### `mops lint`
Runs lintoko (also runs automatically as part of `mops check` when lintoko is in toolchain):
```bash
mops lint # lint all .mo files
mops lint --fix # autofix lint issues
mops lint <name> # filter to .mo files matching <name>
```
When `[canisters.<name>.migrations].check-limit` is set, `mops lint` skips the trimmed chain migrations to match what `moc` sees during `mops check`. To lint a trimmed migration on demand, pass an explicit filter (e.g. `mops lint OldMigrationName`) or `--no-check-limit` to lint the full chain.
### `mops format`
```bash
mops format # format all .mo files
mops format --check # check formatting without modifying
```
### `mops watch`
```bash
mops watch # errors + warnings + format (the default set)
mops watch --warning # errors + warnings only
mops watch -t # errors + tests
```
Error checking is always on. Passing any flag selects only the named tasks, so add `-w` / `-f` when you want them alongside `-t`. Those four are the whole set — there is no `--generate` or `--deploy`.
## Common Patterns
### Warning suppression for a canister
Use per-canister `args` (not global) for suppressions:
```toml
[canisters.backend]
main = "src/backend/main.mo"
args = ["-A=M0198"]
```
### New project
```bash
mops init -y
mops toolchain use moc latest # pin latest moc (non-interactive)
mops toolchain use lintoko latest # pin latest lintoko
mops add core
```
Then configure `[moc].args`, `[canisters]`, and `[build]` in `mops.toml`.
To update tools later: `mops toolchain update moc` or `mops toolchain update` (all tools).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!