| 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. |
Mops CLI
Opinionated guide for Motoko projects. Covers project config, dependency management, type-checking, building, and linting.
Key Principles
- 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.
- No
mo:base — it is deprecated. Always use mo:core (import Array "mo:core/Array").
- All config in
mops.toml — canisters, moc flags, toolchain versions, build settings.
- 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
[toolchain]
moc = "1.7.0"
lintoko = "0.10.0"
pocket-ic = "15.0.0"
[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"
[canisters.backend.check-stable]
path = "deployed/backend.most"
[build]
outputDir = "src/backend/dist"
args = ["--release"]
[optimize]
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 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):
[moc].args — global, all commands (check, build, test, bench, etc.)
[build].args — build only (e.g. --release)
[canisters.<name>.migrations] — auto-injected --enhanced-migration (managed by mops)
[canisters.<name>].args — per-canister
- 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
mops install
mops install --locked
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
mops verify
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>
mops add core
mops add core@2.5.0
mops add --dev test
mops add org/repo
mops add ./pkg
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.15.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. 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 for where the baseline comes from — that differs between a fresh project and an already-deployed canister.
mops check
mops check backend
mops check --fix
mops check --no-lint
mops check --verbose
mops check -- -Werror
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
mops build
mops build backend
mops build --verbose
mops build --check-wasm
mops build --no-check-wasm
mops build --check-deploy
mops build --no-check-deploy
mops build -- --ai-errors
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.
mops deployed init backend
mops deployed backend
mops deployed
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
mops generate candid
mops generate candid backend
mops generate candid backend -o <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
mops toolchain use moc 1.7.0
mops toolchain use moc latest
mops toolchain use lintoko 0.10.0
mops toolchain use pocket-ic 15.0.0
mops toolchain use wasm-opt 131
mops toolchain update moc
mops toolchain update
mops toolchain info <tool>
mops toolchain info <tool> --versions
mops toolchain info <tool> --versions --all
mops toolchain bin moc
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-.gitignored). 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>
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
mops outdated
mops outdated core
mops update
mops update core
mops update --major
mops update --patch
mops update --verbose
mops sync
mops sync --dry-run
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
mops publish
mops publish --dry-run
mops publish --dry-run --no-test --no-docs --no-bench
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:
mops test
mops test my-test
mops test --mode wasi
mops test --reporter files
mops test --watch
mops test -- -Werror
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:
mops bench
mops bench my-bench
mops bench --gc incremental
mops bench --save
mops bench --compare
mops bench -- -Werror
mops lint
Runs lintoko (also runs automatically as part of mops check when lintoko is in toolchain):
mops lint
mops lint --fix
mops lint <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
mops format
mops format --check
mops watch
mops watch
mops watch --warning
mops watch -t
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:
[canisters.backend]
main = "src/backend/main.mo"
args = ["-A=M0198"]
New project
mops init -y
mops toolchain use moc latest
mops toolchain use lintoko latest
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).