# Cargo.lock needs to be updated --locked in CI

> Cargo.lock needs to be updated --locked in CI means the manifest moved and the lockfile did not. Relock locally, commit both, and keep the flag in CI.

Source: https://latchkey.dev/learn/failures/cargo-lock-needs-update-in-ci  
Updated: 2026-09-21

Cargo.lock needs to be updated --locked in CI means cargo resolved your dependencies, found a result that differs from the committed lockfile, and refused to write the new one because you told it not to. The fix is to regenerate the lockfile on your machine and commit it beside the manifest change, not to remove the flag that caught the problem.

## What this error means

The build fails before any crate is compiled, with a single line naming the absolute path of `Cargo.lock` inside the runner workspace and the flag you passed, followed by a suggestion about `--offline`. Nothing is downloaded and no code is built, because this check runs at the point where cargo would have written the file. Read the verb: cargo says it cannot update the lock file when one already exists at that path, and cannot create it when there is no lockfile there at all, and those are two different problems with two different fixes. Re-running the job changes nothing, because nothing in the job is the variable.

```Reconstructed from write_pkg_lockfile in cargo src/ops/lockfile.rs
error: cannot update the lock file /home/runner/work/app/app/Cargo.lock because --locked was passed to prevent this
help: to generate the lock file without accessing the network, remove the --locked flag and use --offline instead.
```

## Common causes

### The manifest was edited and the lockfile was not regenerated

The ordinary case. Someone added or bumped a dependency in `Cargo.toml`, committed that file, and did not commit the `Cargo.lock` that a local build would have rewritten. The change passes review because the diff looks complete, and it fails in CI because a locked build is the only command in the pipeline that reads both files and compares them.

### Cargo.lock is not in the repository at all

This is the case where the verb reads `create` rather than `update`. Cargo documents `--locked` as failing when the lock file is missing, so a job that passes the flag against a repository without a committed lockfile fails on the first run and every run after it. For anything that produces a binary, the answer is to commit the lockfile; for a library published to a registry, the answer is usually to stop passing the flag in that particular job.

### A workspace member drifted, not the crate you were looking at

In a workspace the lockfile records the resolution for every member, so editing the manifest of any one of them invalidates it. The message names the lockfile at the workspace root and says nothing about which member moved, which sends people to the wrong `Cargo.toml`. In our experience this is the version that appears on a commit that looks unrelated to dependencies, and `git log` over all the manifests at once is faster than reading any one of them.

### A git or patched dependency moved underneath the lockfile

A dependency pinned to a git branch rather than a tag, or replaced through a `[patch]` section, can resolve to a different commit than the one recorded, and that is a resolution change even though no file in your repository was edited. The signal is that the failure appears on a run of unchanged code, and the fix is a relock followed by pinning the dependency to something that does not move on its own.

## How to fix it

### Regenerate the lockfile locally and commit it with the manifest

1. Run `cargo generate-lockfile` in the workspace root, which writes `Cargo.lock` from the current manifests without building anything.
2. Read the diff. A one-line manifest change should produce a small and explicable lockfile diff; a large one usually means a different cargo version wrote it, or a git dependency moved.
3. Commit `Cargo.toml` and `Cargo.lock` in the same commit, push, and re-run the job.

```Terminal
cargo generate-lockfile
git add Cargo.toml Cargo.lock
git commit -m "chore: relock after the dependency bump"
```

### Keep the flag, and catch the drift in review instead

The reason this failure keeps returning is that nothing notices the drift until the build runs. A relock followed by a diff that must be empty turns a mid-pipeline surprise into a review comment, costs a couple of seconds, and is the only fix here that prevents the next occurrence rather than clearing this one.

```.github/workflows/ci.yml
- name: Lockfile is in sync with the manifests
  run: |
    cargo generate-lockfile
    git diff --exit-code Cargo.lock
```

### Pin the toolchain so the same cargo writes and reads the file

Two cargo versions can resolve the same manifests differently, and a lockfile written by a newer toolchain on a developer machine against an older one in CI is a drift nobody edited. A `rust-toolchain.toml` in the repository makes both sides use the same version, which removes a whole class of unreproducible failures and also pins which of the two message wordings you will see.

```rust-toolchain.toml
[toolchain]
channel = "1.94.0"
components = ["rustfmt", "clippy"]
```

### Do not remove --locked to make the build green

It is the change that appears in every thread and it deletes the only check you had. Without the flag, cargo quietly rewrites `Cargo.lock` on the runner, the job passes, and the repository still contains a lockfile that nobody has reviewed while the build that just succeeded used dependency versions nobody chose. Cargo documents the flag as the way to assert that the exact same dependencies and versions are used as when the lockfile was generated, which is precisely the property a release pipeline needs.

## How to prevent it

- Commit `Cargo.lock` for every crate that produces a binary, and commit it in the same commit as the manifest change.
- Run a relock-and-diff check on pull requests so drift is caught in review rather than in the build step.
- Pin the toolchain with `rust-toolchain.toml` so the same cargo writes and verifies the lockfile.
- Prefer tags or explicit revisions over branches for git dependencies, so a resolution cannot move on its own.
- Keep `--locked` in CI. A green build that rewrote your lockfile has verified nothing about your dependencies.

## Read the verb, and check the wording against your cargo version

Cargo picks the verb from whether the file is there: the code chooses "update" when the lockfile path exists and "create" when it does not, and interpolates that into one format string. So `cannot update` means the repository has a `Cargo.lock` that no longer matches `Cargo.toml`, and `cannot create` means the repository has no lockfile at all, which is a different conversation about whether a binary crate should be committing one.

The wording changed recently enough that most of the advice online quotes the old one. Up to and including cargo 0.93, which ships with Rust 1.93, the message reads "the lock file ... needs to be updated but --locked was passed to prevent this", followed by a sentence beginning "If you want to try to generate the lock file". From cargo 0.94 it reads "cannot update the lock file ... because --locked was passed to prevent this", followed by a line prefixed `help:`. We checked the boundary by ancestry: the commit that introduced the new wording is not in the 0.93 release and is in 0.94.

One form you may have seen is not a cargo message at all. Cargo interpolates a single flag variable into both slots of that sentence, so it always names the same flag twice. A version that says `--locked was passed` in the first clause and `remove the --frozen flag` in the second was written by a person, not printed by cargo, and matching your log against it will mislead you about which flag your CI is actually passing.

| Cargo version | First line | Second line begins |
| --- | --- | --- |
| 0.93 and earlier (Rust 1.93 and earlier) | `the lock file <path> needs to be updated but <flag> was passed to prevent this` | `If you want to try to generate the lock file` |
| 0.94 and later (Rust 1.94 and later) | `cannot update the lock file <path> because <flag> was passed to prevent this` | `help: to generate the lock file` |
| Any version, no lockfile present | the same sentence with `create` in place of `update` | unchanged |

> Wordings read from `write_pkg_lockfile` in [cargo src/ops/lockfile.rs](https://github.com/rust-lang/cargo/blob/master/src/ops/lockfile.rs) and from the commits that changed it, on 21 September 2026.

## What --locked and --frozen actually promise

Cargo documents `--locked` as asserting "that the exact same dependencies and versions are used as when the existing `Cargo.lock` file was originally generated", and says it exits with an error in exactly two situations: the lock file is missing, or "cargo attempted to change the lock file due to a different dependency resolution". The documentation adds that it "may be used in environments where deterministic builds are desired, such as in CI pipelines", which is the whole reason it is in your workflow.

The two flags are not alternatives. `--frozen` is documented as "equivalent to specifying both `--locked` and `--offline`", so a job passing `--frozen` is making two promises at once: do not change the lockfile, and do not touch the network. When such a job fails here, work out which half broke, because a missing crate in the local cache and a drifted lockfile produce different fixes.

The suggestion cargo prints is narrower than it sounds. Swapping `--locked` for `--offline` lets cargo write a lockfile without the network, which only helps if every crate it needs to resolve is already in the local registry cache. In CI that cache is usually empty or restored from a key that predates the change you just made, so the swap frequently fails again a few seconds later.

## Why there is no recorded run on this page

Every input to this failure is in the repository. Two committed files and one cargo version decide it, and a runner contributes nothing except the directory the checkout landed in. A recorded run on our infrastructure would prove that a sample repository we built for the purpose has a drifted lockfile, which is not a fact about anything.

The path in the message is the specific reason a log here would do harm rather than good. It is an absolute path inside a runner workspace, so a published log shows our checkout directory, and a reader searching for that string will never find it in their own build. The line worth matching is the sentence and the flag, and both of those come from the source that prints them.

Reproduce it locally in about ten seconds instead: add a dependency to `Cargo.toml`, do not relock, and run a build with `--locked`. That is the entire mechanism, and you can then watch the fix below make it go away.

## FAQ

### What does "the lock file needs to be updated but --locked was passed" mean?

Cargo resolved your dependencies, produced a different result from the committed `Cargo.lock`, and refused to write the new one because `--locked` forbids it. Something changed in a manifest, in a workspace member, or in a git dependency since the lockfile was generated, and the lockfile was not regenerated and committed alongside it.

### What is the difference between --locked and --frozen?

Cargo documents `--frozen` as equivalent to `--locked` plus `--offline`. So `--locked` only promises not to change the lockfile, while `--frozen` also promises not to touch the network. A job that fails under `--frozen` may have a lockfile problem, a missing crate in the local cache, or both, so establish which before changing anything.

### Should I switch to --offline as the error suggests?

Only if the crates cargo needs are already in the local registry cache. The suggestion lets cargo write a lockfile without the network, which does not help in CI where that cache is typically empty or restored from a key created before your change. Regenerating the lockfile locally and committing it is the fix that works every time.

### Why did this start failing on a commit that touched no dependencies?

Two common reasons. In a workspace, editing any member manifest invalidates the single root lockfile, and the message names only the root. Alternatively a git dependency pinned to a branch resolved to a newer commit than the one recorded, which is a resolution change with no diff in your repository at all.

### Should a library commit Cargo.lock?

For anything that produces a binary, yes, and `--locked` in CI is then meaningful. For a library published to a registry the lockfile does not affect downstream consumers, so teams often keep it for reproducible CI anyway; what you should not do is pass `--locked` in a job against a repository that has no lockfile, because cargo is documented to fail when the file is missing.

## References

- [cargo source: write_pkg_lockfile, the branch that prints this error (verified 2026-09-21)](https://github.com/rust-lang/cargo/blob/master/src/ops/lockfile.rs)
- [The Cargo Book: --locked, --offline and --frozen, defined (verified 2026-09-21)](https://doc.rust-lang.org/cargo/commands/cargo-build.html)
- [The Cargo Book: cargo generate-lockfile (verified 2026-09-21)](https://doc.rust-lang.org/cargo/commands/cargo-generate-lockfile.html)
- [The Cargo Book: Cargo.toml against Cargo.lock, and who commits which (verified 2026-09-21)](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
