AI dependency pinning checklist for app builders

A checklist for the Python, JavaScript, CI and image dependencies behind an AI app. Each item ends as detected, unknown or confirmed by owner evidence.

Timothy Lindblom

Founder, Newly

Published Last updated

To pin Python dependencies for an AI app, commit a lockfile that records file hashes, install from it in a mode that fails when it is out of date, and add a release cooldown so brand-new versions wait. Repeat the idea for npm, GitHub Actions and Docker images. Pinning stops silent upgrades. It does not review the version you pinned.

Why does pinning matter for an AI app in 2026?

Three incidents reached developers through installs that took the newest version, or whatever a tag pointed to that day. Dates and times below come from the primary sources, checked October 24, 2026.

Dated 2026 incidents and what an unpinned install did
DateWhat happenedWhat an unpinned install didSource
March 19 to 20, 202676 of 77 trivy-action version tags, and all setup-trivy tags, were replaced with malicious commitsA workflow that referenced a tag ran the replaced codeGHSA-69fq-xp46-6x23
March 2026Malicious LiteLLM and Telnyx releases on PyPI after an API token exposure from an exploited Trivy dependency. LiteLLM was live 2 hours 32 minutes, with 119,000+ downloadsAny install that resolved the newest version took itPyPI blog
October 21, 2026LiteLLM 1.107.0 on PyPI from 11:39:51 to 13:23 UTC. Docker tags v1.107.0, latest, main-latest and main-stable resolved to the bad image from 11:46:56 to 14:20 UTCAn install of the newest version, or a pull of a moving tag, inside those windowsLiteLLM

The maintainer's report, which was still marked investigation in progress at our check, tells users to pin 1.106.2 or any explicit version published before the incident. PyPI's report adds the limit of that advice: a lock freezes what you run today, and a compromise can still arrive the next time you update. So this checklist has two halves. Freeze what you run, and slow down what you take next.

If you want the incident facts and what to rotate, read the LiteLLM page for app builders. The wider pattern is in supply-chain attacks on AI tooling. This page is the hands-on part.

Where does my project choose which version to install?

Newly builds a real iPhone and Android app with React Native and Expo, and you can download the project code or connect your own repository. Once you have the repository, four places decide what gets installed. A pin in one place does nothing for the others.

Four places where a version gets chosen, and the file that fixes it in each placeYour laptop orAI coding agentuv.lock,package-lock.jsonYour CI runon each pushuses: lines andthe install commandImage buildand deployDockerfile FROM anddeploy image fieldProductionstartSame lock and samedigest as the buildTop row: where a version is chosen. Bottom row: the file that fixes the choice.
Four stages from laptop to production, each with the file that records the version.

Open each one and write down what you find. The table in the next section tells you how to read it.

What does pinned look like, layer by layer?

What pinned and floating look like in each layer
LayerPinned looks likeFloating looks like
PythonA committed uv.lock, installed with uv sync --locked. Or a requirements file where every dependency has == and a hash, installed with --require-hashesA range such as litellm>=1, no lockfile, or a plain pip install litellm in a build script
JavaScriptA committed lockfile for your package manager, and npm ci or its equivalent in CI^ ranges with no lockfile in the repository, or npm install in CI
Expo buildsThe default: EAS installs with the frozen lockfileEAS_NO_FROZEN_LOCKFILE set to 1 in eas.json
GitHub Actionsuses: owner/action@ followed by a full 40-character commit SHA@v1, @main or any other tag
ImagesFROM name:tag@sha256:..., and the same digest in the deploy configlatest, main-stable or a bare tag
Update speeduv exclude-newer set to a duration, and a Dependabot cooldown blockEvery new release is eligible the minute it appears
PublishingTrusted Publishing with short-lived tokensA long-lived publish token stored as a CI secret

A row that matches the floating column is detected. A row you have not opened is unknown. A row stays unknown until you hold the file or log line that shows the pin.

How do I pin Python dependencies, step by step?

  1. Find every place Python installs. Look at requirements*.txt, pyproject.toml, Dockerfiles, CI files, setup scripts and the environment any AI coding agent created on your machine.
  2. Write a lock that records hashes. PyPI's report says a lock file needs checksums of the package archives to be reproducible. With uv, the uv.lock file should be committed. With pip, the hash-checking mode requires requirements pinned with == and a hash for every dependency, and it errors on any that is not spelled out. The standard lock format, PEP 751, is final and requires at least one hash per file entry.
  3. Install in a mode that fails on drift. uv sync --locked raises an error instead of updating an out-of-date lockfile. The --frozen flag skips that check, so use the first in CI and images. For pip, add --require-hashes.
  4. Read the lock's history. Run git log --format=%cI -- uv.lock (or your requirements file). A change inside the 11:39 to 13:23 UTC window on October 21, 2026, that records litellm 1.107.0, is detected. Go to the LiteLLM page. No change in the window is a good sign and still leaves other packages unknown.
  5. Add a cooldown. uv's exclude-newer setting accepts a duration such as 7 days and compares it with the upload time of each file. LiteLLM 1.107.0 was on PyPI for under two hours, so a delay of days would have excluded it. A cooldown cannot help with a bad release that stays unnoticed longer than the delay.

One more reason to pin and to rebuild from scratch after an incident: Python's site module runs lines that begin with import in .pth files when the interpreter starts. Code placed there runs without your project importing it, which is why turning off install scripts does not cover Python.

What about the JavaScript and Expo side?

Commit the lockfile your package manager writes, and install from it in CI. npm ci removes any existing node_modules, never writes to package.json or the lockfile, and fails when they disagree.

For Expo, the EAS Build documentation says Node packages are installed by default with the package manager's immutable lockfile command, and that setting EAS_NO_FROZEN_LOCKFILE to 1 in eas.json turns that off. Search your project for that name. Finding it set to 1 is detected. Not finding it means the default applies.

If a package worm hit your npm dependencies, Red's ChainDrop check covers that case. An AI provider key never belongs in the app bundle; the leaked AI key page shows where those keys escape. AI inside your Newly app uses credits, so a leaked key is also a cost problem.

How do I pin GitHub Actions and Docker images?

Actions

GitHub's hardening guide tells you to pin actions to a commit SHA and to set the default GITHUB_TOKEN permission to read access for repository contents. The Trivy case shows why: a tag can be moved, a full commit SHA cannot. Red's page on fake security-audit workflows covers a related GitHub Actions campaign.

Images

Docker's guidance is that pinning to a digest keeps you on the same image even if the publisher replaces the tag. Its cost is stated in the same place: you opt out of automated security fixes, so let Dependabot's Docker support propose digest updates and read them. A digest copied from a registry identifies fixed content. Copy yours from a time you trust. A digest taken inside an incident window pins the bad image. If you use uv in a container, its Docker guide recommends pinning the uv image by SHA256 and running uv sync --locked.

Add a cooldown to the update bot as well. Dependabot's cooldown option delays version updates by a number of days and does not apply to security updates.

Do I need Trusted Publishing?

Only if you publish a package of your own, such as a shared helper for several apps. PyPI's Trusted Publishers exchange short-lived OpenID Connect tokens that are valid for 15 minutes, in place of a long-lived API token. In GitHub Actions the job needs the id-token: write permission. For npm, the trusted publishing docs require npm CLI 11.5.1 or later and generate provenance automatically. PyPI's report recommends the same move for maintainers, since the March releases followed an exposed API token. To check yours, look for a publish token in the repository's CI secrets. If one is there, it is detected.

What does pinning leave open?

  • A bad version you chose. A hash proves the file is the one you locked. It says nothing about whether that version is good.
  • A lock written on the wrong day. Regenerating a lock inside an incident window records the bad version with a matching hash.
  • Keys already read. If a bad version ran, the fix is rotation. The rotation order for app teams and Red's rotation guide cover it.

Keep the three states apart

Spotting a floating range is detected. Not having looked is unknown. Confirmed by owner evidence means the lockfile, the install line in a CI log and the date of the last lock change are all in your hands. Nothing outside your repository, our scan included, can move an item to confirmed. For the first fifteen minutes of a wider check, start with is my AI-built app secure, and see the security hub for the rest.

Where do you stand?

Mark each item. Everything starts as unknown, because an unchecked item is unknown. Move one to confirmed only when you hold the evidence listed under it.

Your pinning check

  • Detected: You found the problem.
  • Unknown: Not checked yet, or the check could not tell.
  • Confirmed by owner evidence: You checked and kept the proof.
  1. 1. Every Python and JavaScript project in the repository has a committed lockfile, and the Python one records file hashes.

    Evidence: the lockfile paths in the repository, and the sha256 values you can see next to each Python package.

  2. 2. Installs in CI, image builds and deploys fail when the lock is out of date, and EAS builds still use the frozen lockfile.

    Evidence: the install command lines in a CI log (uv sync --locked, pip install --require-hashes or npm ci) and an eas.json with no EAS_NO_FROZEN_LOCKFILE.

  3. 3. No lockfile change landed during a known bad window, or the packages it recorded were checked afterwards.

    Evidence: git log dates for each lockfile, compared with the 11:39 to 13:23 UTC window on October 21, 2026, plus pip show litellm output.

  4. 4. New releases wait a set number of days before they reach your installs or your update pull requests.

    Evidence: the exclude-newer setting for uv, and the cooldown block in your Dependabot file.

  5. 5. Every third-party GitHub Action uses a full commit SHA, and every image reference in a Dockerfile and deploy config carries a digest.

    Evidence: your workflow files, the FROM lines, and the image field in your deployment config.

  6. 6. If you publish a package, it goes out through Trusted Publishing and no long-lived publish token sits in CI secrets. If you publish nothing, there is no publish step.

    Evidence: the publisher settings on PyPI or npm and the secrets list of the repository.

0 detected6 unknown0 confirmed by owner evidence

Your answers stay in this browser tab. Nothing is saved or sent.

Frequently asked questions

I only build with Newly and never touch Python. Does this apply?

The JavaScript half does. Newly builds a real React Native and Expo app, and if you download your project code or connect your own repository, the package manager lockfile in it decides which versions your builds install. The Python half applies only if you also run server code, an AI gateway, scripts or CI that install Python packages.

Would pinning have stopped LiteLLM 1.107.0?

A project that installed from a lockfile written before 11:39 UTC on October 21, 2026 would not have fetched 1.107.0 from PyPI. A project that refreshed its lock or ran an unpinned install inside the window could have. The maintainer's own advice at our check on October 24 was to pin 1.106.2 or any explicit version published earlier. Check your lockfile history rather than assuming.

Is putting == in requirements.txt enough?

No. It fixes the packages you listed, but the packages they depend on can still float. pip's hash-checking mode closes that gap: it requires every dependency to be pinned and hashed, and it fails on anything not spelled out in the file.

Should I then stop updating dependencies?

No. A cooldown delays new releases for a set number of days, so updates still arrive. Dependabot's cooldown setting applies to version updates and not to security updates, so a published fix is not held back by it. Read each update before you merge it.

Do I need Trusted Publishing if I never publish a package?

No. It protects the people who publish to PyPI or npm. If your app project only installs packages, skip that section and mark the self-test item as having no publish step.

Can the red.run.cloud scan see my lockfile or CI?

No. The scans work from outside your live site. They cannot read your repository, workflows or build machines, so everything on this page stays unknown until you hold the evidence listed in the self-test. They can show readable .env and .git paths and keys in shipped bundles.

Sources

Checked October 24, 2026. Vendor statements are the vendor's own.

  1. Security incident, October 21, 2026, LiteLLM
  2. Incident report: LiteLLM and Telnyx supply chain attack, Python Package Index blog, April 2, 2026
  3. GHSA-69fq-xp46-6x23, Trivy supply chain compromise, Aqua Security on GitHub
  4. Secure installs and hash-checking mode, pip documentation
  5. Syncing the environment: --locked and --frozen, uv documentation
  6. Project layout and the uv.lock file, uv documentation
  7. Resolution: the exclude-newer option, uv documentation
  8. Using uv in Docker, uv documentation
  9. PEP 751, a file format to record Python dependencies, Python Enhancement Proposals
  10. site: site-specific configuration hook, .pth files, Python documentation
  11. npm ci, npm documentation
  12. Caching dependencies: immutable lockfile installs, Expo docs
  13. Security hardening for GitHub Actions, GitHub Docs
  14. Dependabot options reference: cooldown, GitHub Docs
  15. Dockerfile best practices: pin base image versions, Docker documentation
  16. Tags and digests, Docker documentation
  17. Trusted Publishers, PyPI documentation
  18. Using a trusted publisher, PyPI documentation
  19. Trusted publishing for npm packages, npm documentation

Continue Learning