dfinity/icskills

mops-cli

Manage Motoko projects with the mops CLI — toolchain pinning, dependency management, type-checking, building, and linting.

First seen May 10, 2026

Installation

$ npx skills add dfinity/icskills --skill mops-cli

Summary

  • 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.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from dfinity/icskills · top by installs.

npx skills add dfinity/icskills

Browse all from dfinity/icskills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 34
License LICENSE
Default branch main
Open issues 24
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

LicenseApache-2.0
Compatibilitymops >= 3.1.0
More metadata
title
Mops CLI
category
Infrastructure

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 22,431 B
  • docs SUMMARY.md 357 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 111 installs

SKILL.md

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 MOPSPOCKETIC_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

[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

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.

{MOPSENV} in a path dependency expands to $MOPSENV (default local), which makes the lock environment-specific. Switching MOPSENV makes it stale: mops install regenerates it, and --locked fails with a message naming MOPSENV. 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 MOPSCONCURRENCY 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 MOPSCONCURRENCY=1 only if installs still fail after the retries (an egress proxy capping connections, for example).

mops verify

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>

mops add core             # latest version
mops add [email protected]       # 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 "[email protected]" = "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 disabledmoc --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.

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

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.

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

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

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 MOPSPOCKETIC_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             # 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

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:

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 MOPSPOCKETIC_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                        # 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):

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

mops format               # format all .mo files
mops format --check       # check formatting without modifying

mops watch

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:

[canisters.backend]
main = "src/backend/main.mo"
args = ["-A=M0198"]

New project

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).