DEV Community

Jordan Huang
Jordan Huang

Posted on

A Green Install Is Not a Pin: Five Dependency Myths

Did your coding agent just fix the install? I keep seeing that victory line in traces. The command then printed a clean exit code. Then someone copies node_modules like a relic.

That souvenir is not a real dependency pin. It is leftover dust from a scratch disk. Want the rude version? The graph was never frozen.

What this FAQ is actually about

This is not a packaging sermon for librarians. This is about agent loops on scratch hosts. The model edits package.json with a confident range. The host runs some flavor of install. Chat says the work is done.

Did the graph freeze? Did CI receive the same bytes? Usually no. The chat just narrated a feeling.

I wrote this as a myth list you can run. Each myth has a check. Each check has a command you can paste. Steal the script at the end. Then argue with hashes, not vibes.

Myth 1: A package.json edit is a pin

The agent bumped a version range. The range looks adult and responsible. You feel oddly safe.

A range is a wish. A lockfile is a map. Which file would you restore after a fire? If the answer is package.json alone, you will drift.

Evidence you can collect in two minutes

Run these in the repo the agent touched:

git diff --stat -- package.json package-lock.json yarn.lock pnpm-lock.yaml
git status --porcelain -- package.json '*lock*'
Enter fullscreen mode Exit fullscreen mode

Look at the porcelain lines. Did the lockfile move with the manifest? If the lockfile stayed silent, the pin is theater.

  • Manifest-only diff: the agent wrote a wish.
  • Lockfile-only diff: someone regenerated in the dark.
  • Both files in one commit: you might have a pin.

Corrected model: treat the lockfile as the artifact. Treat the manifest as commentary on intent.

Myth 2: Install exit code 0 means the graph is stable

Green text feels like proof. Agents love green text. Humans do too. Why would we not?

Exit code 0 means the installer did not die. It does not mean the tree is replayable tomorrow. Optional deps fail open. Peer deps warn and continue. Platform binaries swap without a speech.

Lifecycle scripts can mutate files after the resolver stops. Did you capture that? Or only the last happy line?

A tiny probe

npm install --dry-run --json > /tmp/install-dry.json
# frozen path, when a lockfile already exists
npm ci --ignore-scripts --dry-run
Enter fullscreen mode Exit fullscreen mode

Read the warnings. Count peer issues. Did a native addon pick a platform? If you only stored agent stdout, you stored a mood. You did not store a graph.

Corrected model: success is a frozen installer command. Add a hashed lockfile. Keep both.

Myth 3: The scratch host's node_modules will match CI

This one bites teams on free servers. The sandbox is some Linux userland. Your laptop is not that userland. CI might use another Node line.

The agent compiled a binary. The binary linked a glibc it found. Your Mac never will. So why copy node_modules off a scratch disk? You are copying an accident with extra steps.

Fingerprint before you trust the tree

uname -s -m
node -v
corepack --version || true
git rev-parse HEAD
sha256sum package-lock.json 2>/dev/null || shasum -a 256 package-lock.json
Enter fullscreen mode Exit fullscreen mode

Put those five lines next to the test output. If CI disagrees on any line, the tree is local color. It is not a release.

I replay some of these checks on MonkeyCode. Disclosure: This article was prepared as part of MonkeyCode's product outreach. It offers free model access and a free server option. That pair is a scratch lane for me. It is not a substitute for CI. I still hash the lockfile. I still refuse to ship the sandbox tree.

Corrected model: the host is a lab bench. CI remains the court of bytes.

Myth 4: Tests passing means you can skip the lockfile

The agent ran the unit suite. All dots went green. Someone deleted the lockfile from the PR. Review got quieter. Supply chain got louder.

I get the temptation. Lockfiles are noisy diffs. Reviewers complain about churn. Tests sample behavior. They do not sample the entire graph. A transitive bump can stay green. It can still change who you trust.

The check I actually want in review

# Fail the job if the lockfile moves after a frozen install
npm ci
git diff --exit-code -- package-lock.json
Enter fullscreen mode Exit fullscreen mode

If that exits 1, the agent did not pin. The PR is incomplete. Green tests with a dirty lock are a failed job. Say that in the template.

Corrected model: a suite is evidence of behavior. A lock hash is evidence of the graph. You need both on the same commit.

Myth 5: Re-running the same prompt rebuilds the same tree

Agents do not replay like compilers. Prompts are not makefiles. Registries move. Resolvers change defaults. Models improvise a second "fix".

You hit retry. You got another story. The second tree is not the first tree. Did you keep the first lockfile hash? If not, you cannot detect the drift. You can only reread the chat.

Capture once, compare forever

mkdir -p .replay
{
  echo "head=$(git rev-parse HEAD)"
  echo "node=$(node -v)"
  echo "lock=$(sha256sum package-lock.json | awk '{print $1}')"
} > .replay/pin.txt
Enter fullscreen mode Exit fullscreen mode

Next run, diff the file. If lock= changed, you did not reproduce. You rolled the dice and named it science.

Corrected model: reproduction is hash equality. Chat similarity is noise with punctuation.

Artifact: a pin-proof script and a decision table

Do not copy the sandbox. Prove the pin. Here is a script I keep as scripts/pin_proof.sh. Treat it as a labeled workflow sample. Adapt the paths. I am not claiming production metrics.

#!/usr/bin/env bash
# pin_proof.sh — record whether an agent install is actually pinned
set -euo pipefail

out="${1:-.replay/pin_proof.json}"
mkdir -p "$(dirname "$out")"

lock=""
for cand in package-lock.json pnpm-lock.yaml yarn.lock; do
  if [[ -f "$cand" ]]; then
    lock="$cand"
    break
  fi
done

if [[ -z "$lock" ]]; then
  echo "no lockfile found" >&2
  exit 2
fi

hash="$(sha256sum "$lock" | awk '{print $1}')"
dirty="$(git status --porcelain -- "$lock" package.json || true)"
head="$(git rev-parse HEAD 2>/dev/null || echo unborn)"

node_v="$(node -v 2>/dev/null || echo missing)"
os="$(uname -s -m)"

install_ok="skip"
lock_after="not_npm"
if [[ "$lock" == "package-lock.json" ]] && command -v npm >/dev/null; then
  if npm ci --ignore-scripts; then
    install_ok="npm_ci_ok"
  else
    install_ok="npm_ci_failed"
  fi
  if git diff --exit-code -- package-lock.json; then
    lock_after="unchanged"
  else
    lock_after="mutated"
  fi
fi

python3 - <<PY
import json, os
doc = {
  "git_head": "$head",
  "os": "$os",
  "node": "$node_v",
  "lockfile": "$lock",
  "lock_sha256": "$hash",
  "lock_dirty_before": """$dirty""".strip(),
  "install": "$install_ok",
  "lock_after_ci": "$lock_after",
}
os.makedirs(os.path.dirname("$out") or ".", exist_ok=True)
with open("$out", "w") as f:
    json.dump(doc, f, indent=2)
print(json.dumps(doc, indent=2))
PY

if [[ "$install_ok" == "npm_ci_failed" || "$lock_after" == "mutated" ]]; then
  echo "pin proof failed" >&2
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Run it after the agent stops talking.

chmod +x scripts/pin_proof.sh
./scripts/pin_proof.sh .replay/pin_proof.json
Enter fullscreen mode Exit fullscreen mode

Commit .replay/pin_proof.json only if your team wants the fingerprint. Do not commit node_modules. Ever. That directory is a host opinion.

Decision table

Observation Trust the tree? Next action
No lockfile No Generate one with a frozen installer
Lock dirty after npm ci No Send the agent back at the lock
npm ci fails, npm install works No The graph is not pinned
OS or Node mismatch versus CI No Replay on the CI image
Lock hash matches CI cache Yes, as a pin Still run the real pipeline
Tests green, lock missing No Tests are not a resolver

Print the table in review. Argue with the row. Do not argue with the chat transcript.

A practical replay workflow

  1. Freeze the git HEAD the agent actually saw.
  2. Require a lockfile in that same commit.
  3. Run scripts/pin_proof.sh on the scratch host.
  4. Run the same script on CI or a matching image.
  5. Merge only when lock hashes match and npm ci stays quiet.

Need a scratch lane for step 3? A free server is enough for the fingerprint. A free model can propose the lock update. Neither one gets merge rights. Tools propose. Proof files dispose.

That is the whole trick. Keep the rights off the chat.

Limitations

This script does not audit the registry. It does not catch a compromised tarball with the same version. It does not replace npm audit. It does not emit a real SBOM.

It assumes a Node repo. Cargo, Go, and pip need the same idea. Swap the frozen command. Keep the hash. Keep the OS line.

npm ci --ignore-scripts skips lifecycle traps. It also skips needed native builds. If your package requires scripts, run a second pass in CI. Do not hide that pass on a laptop and call it policy.

I am not publishing timings. Timings rot by next week. Hashes do not.

Who should not use this

Do not use this as your only release gate. Do not use this if you cannot run npm ci. Do not use this to skip reviewers because the JSON looks official.

If you ship mobile binaries, this Node fingerprint is incomplete. If you vendor native addons, hash those blobs too. If you need a legal provenance chain, use signed attestations. This FAQ is a lab checklist, not a courtroom.

What I want you to remember

The agent did not pin that range. The installer did not freeze the universe. The scratch disk is not your product. Ask one rude question in review. Where is the lock hash?

If the hash is missing, the work is missing. If the hash moved, you did not reproduce. You just got another story with a green exit code.

Run the script on the next agent PR. Paste the JSON in the review. That is the only ask.

Top comments (0)