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*'
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
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
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
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
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
Run it after the agent stops talking.
chmod +x scripts/pin_proof.sh
./scripts/pin_proof.sh .replay/pin_proof.json
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
- Freeze the git
HEADthe agent actually saw. - Require a lockfile in that same commit.
- Run
scripts/pin_proof.shon the scratch host. - Run the same script on CI or a matching image.
- Merge only when lock hashes match and
npm cistays 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)