Elixir teams lean on Hex and mix.exs to pull in dependencies, and on mix.lock to freeze the exact versions and checksums that get compiled into a release. But mix.lock integrity is easy to take for granted: a stale lockfile, a manually edited hash, or a dependency swapped out from under a package name can all slip past code review unnoticed. Attackers who compromise a Hex package, or intercept a poorly pinned git dependency, rely on exactly this blind spot to inject malicious code into a supply chain that most teams never re-verify after the initial mix deps.get. In this guide, you'll learn how Elixir resolves and locks dependencies, how to pin versions deliberately instead of accidentally, how to detect when mix.lock has drifted from what's actually installed, and how to wire integrity checks into CI so a tampered lockfile fails the build instead of shipping to production.
Step 1: Understand How Elixir Mix Dependencies Are Resolved
Before you can protect mix.lock, it helps to know what it actually represents. When you declare elixir mix dependencies in mix.exs, you're specifying a range, not a fixed version:
defp deps do
[
{:phoenix, "~> 1.7.0"},
{:jason, "~> 1.4"},
{:ecto_sql, "~> 3.11"}
]
end
Running mix deps.get resolves that range against Hex's package index, picks a concrete version for every direct and transitive dependency, and writes the result — version, source, and a cryptographic hash — into mix.lock. A single entry looks like this:
"jason": {:hex, :jason, "1.4.1", "af1504e5..." , [:mix], [...], "hexpm", "fbeef5cb..."},
Those two hashes matter: the first is the checksum of the package's inner contents, the second (hexpm source) is the checksum of the outer tarball Hex serves. Both are what mix checks against when it later re-fetches a dependency. This is the mechanism mix.lock integrity depends on — if either hash doesn't match what's downloaded, mix should refuse to proceed. The problem is that most teams never test that this check actually fires, and it's trivially bypassed if the lockfile itself is edited by hand or generated on a compromised machine.
Step 2: Practice Deliberate Elixir Dependency Pinning in mix.exs
Loose version constraints (~>, >=) are convenient for development but they widen the window an attacker has to slip in a malicious release. For production-facing applications, tighten constraints on anything security-sensitive or with a history of churn:
defp deps do
[
{:phoenix, "== 1.7.14"},
{:jason, "== 1.4.1"},
{:guardian, "== 2.3.2"}
]
end
Exact-version elixir dependency pinning doesn't replace mix.lock — the lockfile still governs what actually gets installed — but it removes ambiguity about intent. When mix.exs says == 1.7.14 and mix.lock says something else, that mismatch is an immediate, greppable signal that something changed outside the normal mix deps.update flow. Reserve ~> for internal tooling or libraries where you genuinely want automatic patch upgrades, and pin exactly for anything that touches auth, crypto, serialization, or network I/O.
Step 3: Verify mix.lock Integrity Before Every Install
This is the core habit: never assume the lockfile checked into git is the one that gets used. Run these checks as a matter of routine, not just when something looks wrong.
First, confirm mix.lock and mix.exs actually agree with what's resolvable:
mix deps.get --check-locked
This flag makes mix fail loudly instead of silently rewriting the lockfile if the current mix.exs constraints can't be satisfied by what's locked — exactly the behavior you want in CI.
Second, force a clean re-fetch and let Mix's built-in checksum verification do its job:
rm -rf deps _build
mix deps.get
mix deps.compile
If a package's hash on Hex no longer matches the hash recorded in your mix.exs.lock entry, mix deps.get will error out with a checksum mismatch rather than silently installing a different artifact. That failure is a feature — treat it as a stop-the-line signal, not an annoyance to mix deps.clean your way past.
Third, diff the lockfile against its last known-good state before merging any dependency change:
git diff origin/main -- mix.lock
A single-line version bump you expect is fine. A diff that touches hashes on packages you didn't intentionally update, or that adds a new transitive dependency from an unfamiliar source, deserves a manual look before merge.
Step 4: Lock Down Git and Path Dependencies
Hex-hosted packages get checksum verification for free; git and path dependencies do not automatically get the same protection unless you pin them precisely:
defp deps do
[
{:my_internal_lib,
git: "https://github.com/yourorg/my_internal_lib.git",
ref: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"}
]
end
Always pin git dependencies to a full commit SHA (ref:), never a branch name (branch: "main"). A branch pointer can be force-pushed or silently updated by anyone with write access to that repository, and mix.lock will happily record whatever commit was current at lock time — giving you a false sense of stability. Path dependencies (path: "../shared_lib") should be treated as untrusted input in monorepo CI unless that path is under the same access control as the consuming app.
Step 5: Enforce Lockfile Checks in CI/CD
Manual discipline doesn't scale across a team. Add a dedicated CI step that fails the build on any lockfile drift or unverified dependency:
- name: Verify mix.lock integrity
run: |
mix deps.get --check-locked
mix deps.get
mix deps.compile
git diff --exit-code mix.lock
The final git diff --exit-code catches the case where mix deps.get silently rewrote mix.lock during the CI run itself — which should never happen on a clean checkout and is a strong indicator of an unpinned or misbehaving dependency. Pair this with mix hex.audit, which checks your resolved dependencies against Hex's list of retired packages (versions pulled for security or correctness reasons):
mix hex.audit
Run both steps before mix compile in your pipeline, not after — you want to catch a bad dependency before it's compiled into your build artifacts.
Step 6: Audit for Drift and Unauthorized Changes
Even with CI checks in place, periodically audit the full dependency tree rather than relying only on diffs:
mix deps.tree
mix hex.outdated
mix deps.tree surfaces the full transitive graph so you can spot dependencies that arrived indirectly and were never reviewed on their own merits. mix hex.outdated flags packages that are behind — useful for prioritizing updates, but also worth cross-referencing against CVE databases for Elixir/Erlang packages before you bump versions blindly. Keep a record (a changelog entry or a signed commit) every time mix.lock changes intentionally, so unexplained changes stand out during audits or incident response.
Troubleshooting and Verification
mix deps.get reports a checksum mismatch. Do not run mix deps.clean and re-fetch to make the error go away — that discards the security signal you were just given. First confirm you're pulling from the expected source (check for typosquatted package names or an unexpected mirror in your Hex config), then verify the package's published checksum on hex.pm directly before deciding whether to accept the new hash.
CI fails on git diff --exit-code mix.lock but you didn't change any dependency. This usually means mix.exs has a loose constraint (~>) that resolved differently due to a new upstream release. Pin the constraint tighter (Step 2) so resolution is deterministic, or explicitly run mix deps.update <package> and commit the resulting lockfile change intentionally.
A teammate's lockfile diff touches dozens of unrelated packages. This is almost always caused by a different Erlang/Elixir/Hex version producing a different resolution, not malicious activity — but verify by checking mix hex.info on a few changed packages and confirming the versions are legitimate, then standardize your team's toolchain via .tool-versions or mise/asdf to prevent recurrence.
You suspect a dependency was compromised after the fact. Pin to the last known-good version from mix.lock's git history, run mix deps.get --check-locked to confirm it still resolves, and treat the incident like any other supply chain compromise: rotate secrets that dependency could have touched, and audit build logs for the window it was active.
How Safeguard Helps
Manually running these checks catches problems, but only if someone remembers to run them on every change, in every repository, every time. Safeguard automates mix.lock integrity verification as part of your software supply chain security posture: we continuously monitor mix.exs and mix.lock across your Elixir services, flag unpinned or loosely-constrained dependencies, and alert on lockfile drift, unexpected hash changes, or git dependencies pinned to mutable branches instead of commit SHAs — before they reach a merge, let alone production.
Beyond static checks, Safeguard correlates your resolved Hex packages against real-time vulnerability and retirement data, so a dependency that's fine today but gets flagged tomorrow doesn't sit silently in your lockfile until someone happens to run mix hex.audit. For teams managing dozens of Elixir services with independently maintained mix.lock files, that continuous, automated layer is the difference between finding a tampered dependency in code review and finding it in an incident postmortem.