You review the Codex PR. package.json now lists zod as a new dependency. The code imports from zod. Everything looks fine — until CI runs npm ci and fails: “lockfile out of sync with package.json.” Codex added the dep to package.json but never ran npm install, so package-lock.json does not contain zod. Worse variants: the agent ran npm install --no-save, or it edited the lockfile by hand and wrote a fake hash.
This is a small surface area but breaks npm ci, breaks reproducible builds, and risks the agent on the next run picking a different resolved version than your teammates have locally.
Common causes
1. Agent skipped the install step
The Codex sandbox added "zod": "^3.22.0" to package.json and moved on. It never executed npm install. The lockfile is untouched.
How to spot it: git diff shows package.json changed but package-lock.json / pnpm-lock.yaml / yarn.lock did not. npm ci fails.
2. Agent ran npm install --no-save
The model thought it was being cautious — install for testing, do not modify files. The package is in node_modules for the agent’s tests but never in the lockfile.
How to spot it: Transcript shows npm install <pkg> --no-save or npm install --no-save. Lockfile is unchanged.
3. Agent ran the wrong package manager
Your repo uses pnpm but Codex ran npm install, which generates package-lock.json while you ship pnpm-lock.yaml. Now you have both lockfiles and no clear source of truth.
How to spot it: New package-lock.json appears next to existing pnpm-lock.yaml. CI of either tool fails.
4. Agent hand-edited the lockfile
The model saw a “lockfile out of sync” error in a previous run, decided to fix it by editing package-lock.json directly. The shasums are now fake / wrong; npm ci fails on integrity check.
How to spot it: package-lock.json diff is large and looks plausible, but npm ci errors with “integrity checksum failed” or “EINTEGRITY.”
5. setup.sh runs install but does not commit the changes
Codex’s .codex/setup.sh runs npm install to set up the environment, but the resulting lockfile changes never make it into the commit because they happen before the agent’s edit phase.
How to spot it: Lockfile diff exists only locally in the agent sandbox, not in the PR. Agent transcript shows install succeeded.
Shortest path to fix
Step 1: AGENTS.md “always install after package.json change” rule
## Dependency rules
After any change to `package.json` (add, remove, version bump):
1. Run the install command for the active package manager:
- `npm install` if `package-lock.json` exists
- `pnpm install` if `pnpm-lock.yaml` exists
- `yarn install` if `yarn.lock` exists
2. Stage both `package.json` and the lockfile in the same commit.
3. Do not run `npm install --no-save`.
4. Do not hand-edit any lockfile. If the lockfile seems wrong, delete it
and re-run `npm install` so the manager regenerates it.
5. Do not introduce a second lockfile. Use only the one already in the repo.
In the PR description, list new packages and their reasons.
A short rule with the exact commands the agent should run.
Step 2: Bake install into setup.sh so changes are picked up
Make sure .codex/setup.sh always runs install in a way that surfaces lockfile changes:
#!/usr/bin/env bash
set -euo pipefail
# Detect package manager
if [ -f pnpm-lock.yaml ]; then
pnpm install --frozen-lockfile=false
elif [ -f yarn.lock ]; then
yarn install
else
npm install
fi
# Show the agent which files changed during setup so it knows to commit them
git status --porcelain
Then in AGENTS.md:
After setup completes, run `git status` and commit any lockfile changes
along with your task changes.
Step 3: CI check that lockfile matches package.json
For npm:
# .github/workflows/lockfile-check.yml
name: Lockfile check
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- name: Verify lockfile is in sync
run: npm ci
- name: Verify no extra lockfiles
run: |
count=$(ls -1 package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null | wc -l)
if [ "$count" -gt 1 ]; then
echo "More than one lockfile present"
ls -la package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null
exit 1
fi
npm ci fails if package.json and package-lock.json are out of sync. Required-status-check this and Codex cannot land without updating the lockfile.
For pnpm:
- name: Verify lockfile is in sync
run: pnpm install --frozen-lockfile
Step 4: “Show lockfile diff in PR” rule
AGENTS.md:
When a PR changes any lockfile, include this section in the PR body:
## Lockfile changes
Run `git diff --stat package-lock.json` (or pnpm-lock.yaml) and paste the
output. Then:
- List packages added
- List packages removed
- List packages whose major version changed
- For each, link the changelog / release notes
Forces the agent to surface dependency churn for reviewers.
Step 5: Block hand-edited lockfiles
Add a pre-commit hook that flags suspicious lockfile changes:
mkdir -p .git/hooks
cat > .git/hooks/pre-commit <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if git diff --cached --name-only | grep -qE '(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$'; then
# If lockfile changes, package.json must also change (unless it is a full re-gen)
if ! git diff --cached --name-only | grep -q 'package\.json$'; then
echo "WARNING: lockfile changed but package.json did not."
echo "If this is intentional (e.g. dedupe), confirm by setting LOCKFILE_REGEN=1."
[ "${LOCKFILE_REGEN:-0}" = "1" ] || exit 1
fi
fi
EOF
chmod +x .git/hooks/pre-commit
Catches the “agent edited the lockfile to silence an error” anti-pattern.
Step 6: Pin the package manager via packageManager field
In package.json:
{
"packageManager": "pnpm@9.0.0"
}
And in .codex/setup.sh, enable corepack so the right manager runs:
corepack enable
corepack prepare --activate
Now Codex cannot accidentally use a different manager and generate a foreign lockfile.
Prevention
- AGENTS.md mandates install after any
package.jsonedit and which manager to use .codex/setup.shruns install and surfaces lockfile changes viagit status- CI runs
npm ci/pnpm install --frozen-lockfileas a required check - Pre-commit hook flags lockfile changes without corresponding
package.jsonchange - Pin
packageManagerinpackage.json; enable corepack in setup.sh - PR body requires a “Lockfile changes” section listing added/removed/bumped packages