Skip to content

[Bug]: hermes update runs npm install instead of npm ci, dirtying package-lock.json after every update #4048

@daneroo

Description

@daneroo

Bug Description

Summary

hermes update runs npm install --silent as part of its Node.js dependency update step. This allows npm to re-resolve the dependency graph and rewrite package-lock.json, leaving a dirty working tree after every update.

For a deployment/update path, the correct command is npm ci, which installs exactly from the committed lockfile without mutating it.

Context

Observed on Ubuntu 24.04 with npm 10.x after a normal hermes update run.
The dirty diff added @askjo/camoufox-browser transitive dependencies to package-lock.json,
suggesting the committed lockfile was not fully consistent with package.json at that point.
npm ci would have surfaced this inconsistency immediately rather than silently rewriting the file.

Steps to Reproduce

cd ~/.hermes/hermes-agent
git status # clean
hermes update
git status # package-lock.json now modified
git diff --stat -- package-lock.json

Expected Behavior

Expected: working tree remains clean after hermes update
Actual: package-lock.json is rewritten with ~2000+ line diff due to dependency re-resolution

Actual Behavior

cd ~/.hermes/hermes-agent
git status # clean
hermes update
git status
modified: package-lock.json

Affected Component

Setup / Installation, Other

Messaging Platform (if gateway-related)

No response

Operating System

Ubuntu 24.04

Python Version

3.11.15

Hermes Version

Hermes Agent v0.6.0 (2026.3.30)

Relevant Logs / Traceback

Root Cause Analysis (optional)

In hermes_cli/main.py around lines 3027–3032:

if (PROJECT_ROOT / "package.json").exists():
    import shutil
    if shutil.which("npm"):
        print("→ Updating Node.js dependencies...")
        subprocess.run(["npm", "install", "--silent"], cwd=PROJECT_ROOT, check=False)

npm install re-resolves the dependency graph and may rewrite package-lock.json.

Proposed Fix (optional)

Proposed fix

Replace:

subprocess.run(["npm", "install", "--silent"], cwd=PROJECT_ROOT, check=False)

With:

subprocess.run(["npm", "ci", "--silent"], cwd=PROJECT_ROOT, check=False)

npm ci:

  • Installs exactly from package-lock.json
  • Does not mutate the lockfile
  • Is the standard command for reproducible installs in CI/CD and deployment contexts
  • Will fail fast if package-lock.json is inconsistent with package.json, which is the correct behaviour (surface the inconsistency rather than silently mutate)

It is important that the working package-lock.json be committed

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions