Skip to content
Fran Gonzalez
← Back to blog
·Clanker·6 min read

mise lockfiles: why locked=true needs lockfile=true

A missing lockfile setting left mise warning about unresolved tools; the CI lock check also depended on generating locks on glibc rather than musl.

Some matmuls wrote this slop, sorry. My goal with this content is to document some work I (a real human bean) do while poking the Clanker, and try to learn something along the way.

Every CI step printed the following warning twice:

mise WARN  Failed to resolve tool version list for node: node@26.5.0: node@26.5.0 is not in the lockfile
hint: Run `mise install` without --locked to update the lockfile
mise WARN  Failed to resolve tool version list for python: python@3.14.6: python@3.14.6 is not in the lockfile

The installs and builds completed, but every mise x invocation printed the warning. The output showed that lockfile enforcement was incomplete.

The Problem

Note

This is a point-in-time account of mise lockfile behavior on glibc and musl hosts. The tool versions, platform set, and lockfile contents are the values from this investigation.

The repository pins every tool in mise.toml and ships a mise.lock with checksummed download URLs. The settings block was:

# mise.toml
[settings]
minimum_release_age = "1d"
experimental = true
locked = true
npm.package_manager = "npm"

locked = true is intended to fail if a tool lacks a pre-resolved URL in the lockfile for the current platform. In CI, this prevents resolution through external APIs and makes a missing entry a build failure.

The command warned instead. The first check was whether node and python were missing from mise.lock.

What Changed

The lockfile was complete. node had all seven platforms pinned with checksums and URLs, including linux-x64-musl, the platform used inside the CI node:*-alpine images. git log -S 'tools."node"' -- mise.lock returned no result because it searched for a quoted key. The entry was [[tools.node]], with no quotes. The warning came from the lockfile mode setting.

The fix was one line. The mise settings docs describe locked this way:

When enabled and lockfile is also enabled, mise install fails if tools lack pre-resolved URLs in the lockfile for the current platform.

locked requires lockfile. The repository had locked = true but did not set lockfile = true. As a result, each mise x resolved the version list and printed the warning. The lockfile docs describe lockfile = true as the setting that enables lockfile mode before locked enforces it.

Layer one of the fix:

# mise.toml
[settings]
minimum_release_age = "1d"
experimental = true
lockfile = true          # was missing: enables lockfile mode
locked = true            # now actually enforces (needs lockfile)
lockfile_platforms = ["linux-x64", "linux-x64-musl", "macos-arm64"]
npm.package_manager = "npm"

lockfile_platforms trims the lock to the platforms the project actually uses: CI runs in node:*-alpine images (Alpine is musl-based, so mise detects linux-x64-musl inside the containers), and developers are on Apple Silicon. That drops linux-arm64, windows-x64, and Intel macOS, shrinking mise.lock and speeding up resolution.

After regenerating, mise install --locked ran clean:

mise all tools are installed

No warnings.

Musl and glibc lockfile generation

After removing the warnings, I checked whether the CI gate regenerated the lockfile and compared it with the committed file. The repository already used this pattern with pnpm (check:lockfile-sync), so I applied it to mise:

# what I tried first
toolchain:mise-lock-sync:
  extends: .monorepo_workspace_lint_template # runs in node:*-alpine
  script:
    - ./bin/mise lock
    - git diff --exit-code mise.lock

It failed on the first pipeline run. The diff showed node and python being rewritten to this:

[[tools.node]]
version = "26.5.0"
backend = "core:node"

[tools.node.options]
compile = "true"

[tools.node."platforms.linux-x64-musl"]
checksum = "sha256:b3db373d..."
url = "https://nodejs.org/dist/v26.5.0/node-v26.5.0.tar.gz"
install = "source"

install = "source", compile = "true". On the musl host, mise resolved node and python to source builds because prebuilt musl binaries were unavailable for those tools. The committed lockfile contained prebuilt binaries generated on a glibc host or macOS. mise lock on Alpine therefore produced a different file from mise lock on glibc, and the gate reported a diff caused by the platform.

mise lock is not platform-independent. It reflects the host’s ability to find prebuilts. A lockfile committed from glibc or macOS will not regenerate cleanly on musl.

Then I looked one directory over and found this already in ci/projects/supply-chain.yml:

mise-lock:validate:
  stage: build
  image: *renovate_image # renovate/renovate, a glibc image
  variables:
    MISE_SAFE: "1"
  script:
    - ./bin/mise --version
    - ./bin/mise lock
    - git diff --exit-code -- mise.lock
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" || ...
      changes: [mise.toml, mise.lock, bin/mise, ci/projects/supply-chain.yml]

The gate already existed. It ran on the glibc renovate/renovate image, where mise lock matches the committed lockfile. It passed on my branch. I deleted my duplicate and documented the existing one instead.

Results

  • The not in the lockfile warnings are gone. lockfile = true completes the subsystem; with a complete lockfile, mise x resolves from it directly.
  • mise.lock shrank from seven platforms to three.
  • The existing mise-lock:validate gate catches future drift (a Renovate bump, a manual pin, or a platform change that touches mise.toml without refreshing the lock). It runs on glibc, so it is stable.
  • One doc page (docs/dev-tools/mise-lock.md) records the model, the gate, and the musl-vs-glibc caveat.

What I’d Do Differently

The first CI attempt created a duplicate gate before checking whether one already existed. Searching ci/ for mise.lock would have found mise-lock:validate.

The initial diagnosis also took longer because git log -S searched for a quoted key that was unquoted in the file. rg '^\[\[tools\.node' mise.lock identifies the entry directly.

The investigation showed that a strict setting can depend on a second setting, and that lockfile generation can vary with the libc and platform used to generate it.

Renovate lock refresh

Renovate’s mise manager bumps versions in mise.toml, but it will not refresh mise.lock on its own. Per the Renovate mise manager docs, running mise lock counts as an unsafe execution because it can run repository-defined behavior:

Self-hosted administrators must explicitly allow this by including mise in the global allowedUnsafeExecutions setting.

Until "mise" lands in the runner’s allowedUnsafeExecutions, a Renovate mise MR will bump mise.toml and leave mise.lock stale, so mise-lock:validate fails and the MR needs a manual mise lock before it can merge. The gate makes that drift loud instead of silent. Enabling allowedUnsafeExecutions: ["mise"] plus lockFileMaintenance on the runner is the remaining follow-up.

References

This post was written with AI assistance.