I have already write an update post about bitwarden backup. After running smoothly for months, my automated Bitwarden backup GitHub Action suddenly started failing. The error message was cryptic:

Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/peterbabic/backups/bitwarden/node_modules/open/index.js
from /Users/peterbabic/backups/bitwarden/node_modules/@bitwarden/cli/build/bw.js not supported.

Additionally, I was seeing warnings about Node.js engine compatibility:

npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   package: '@koa/[email protected]',
npm warn EBADENGINE   required: { node: '>= 20' },
npm warn EBADENGINE   current: { node: 'v18.20.8', npm: '10.8.2' }
npm warn EBADENGINE }

Understanding the Root Causes#

There were actually two separate issues at play:

Issue 1: CommonJS vs ESM Compatibility#

The @bitwarden/cli npm package depends on the open package. Recently, the open package was updated to version 10.x, which is ESM-only. However, the Bitwarden CLI is built using CommonJS and tries to use require() to import it, which causes the ERR_REQUIRE_ESM error.

The open package became ESM-only starting from version 9.0.0. The last CommonJS-compatible version was 8.4.2.

Issue 2: Node.js Version Requirements#

Several dependencies of @bitwarden/cli were recently updated to require Node.js 20+:

My GitHub Actions workflow was using actions/setup-node@v4 without specifying a version, which defaulted to Node 18.x.

Solution 1: The Quick Fix (npm overrides)#

The first solution was to use npm's overrides feature to force the use of an older, CommonJS-compatible version of the open package.

I updated package.json:

{
  "dependencies": {
    "@bitwarden/cli": "^2025.10.0"
  },
  "overrides": {
    "open": "8.4.2"
  }
}

And specified Node 20 in the workflow:

- uses: actions/setup-node@v4
  with:
    node-version: "20"

After running npm install, the override was applied:

└─┬ @bitwarden/[email protected]
  └── [email protected] overridden

This fixed both issues, and the workflow started working again.

Solution 2: The Better Fix (Using Official Binary)#

While the override approach worked, it felt like a hack. A cleaner solution is to use the official Bitwarden CLI binary instead of installing it via npm. This is actually the recommended approach for CI/CD environments.

I completely rewrote the workflow to download the official binary:

  steps:
    - uses: actions/checkout@v4
-   - uses: actions/setup-node@v4
-     with:
-       node-version: '20'

-   - run: |
-      npm install
-      ./node_modules/@bitwarden/cli/build/bw.js login --apikey
-      ./node_modules/@bitwarden/cli/build/bw.js export --format json --session $(./node_modules/@bitwarden/cli/build/bw.js unlock --passwordenv BW_PASSWORD --raw) --raw | openssl aes-256-cbc -a -salt -pbkdf2 -k $BW_PASSWORD -out personal.json.enc
-      ./node_modules/@bitwarden/cli/build/bw.js export --format json --session $(./node_modules/@bitwarden/cli/build/bw.js unlock --passwordenv BW_PASSWORD --raw) --raw  --organizationid "$BW_ORGANIZATION_ID" | openssl aes-256-cbc -a -salt -pbkdf2 -k "$BW_PASSWORD" -out organization.json.enc
-      ./node_modules/@bitwarden/cli/build/bw.js lock
-      ./node_modules/@bitwarden/cli/build/bw.js logout
+
+   - name: Install Bitwarden CLI
+     run: |
+       curl -L "https://vault.bitwarden.com/download/?app=cli&platform=linux" -o bw.zip
+       unzip bw.zip
+       chmod +x bw
+       sudo mv bw /usr/local/bin/
+
+   - name: Backup Bitwarden
+     run: |
+      bw login --apikey
+      bw export --format json --session $(bw unlock --passwordenv BW_PASSWORD --raw) --raw | openssl aes-256-cbc -a -salt -pbkdf2 -k $BW_PASSWORD -out personal.json.enc
+      bw export --format json --session $(bw unlock --passwordenv BW_PASSWORD --raw) --raw  --organizationid "$BW_ORGANIZATION_ID" | openssl aes-256-cbc -a -salt -pbkdf2 -k "$BW_PASSWORD" -out organization.json.enc
+      bw lock
+      bw logout

I then removed the npm-related files entirely:

rm package.json package-lock.json

Here's the complete resulting workflow file:

name: Bitwarden.com backup

on:
  workflow_dispatch:
  schedule:
    - cron: "30 0 * * 0"

jobs:
  backup:
    runs-on: ubuntu-22.04
    env:
      BW_CLIENTID: ${{ secrets.BW_CLIENTID }}
      BW_CLIENTSECRET: ${{ secrets.BW_CLIENTSECRET }}
      BW_PASSWORD: ${{ secrets.BW_PASSWORD }}
      BW_ORGANIZATION_ID: ${{ secrets.BW_ORGANIZATION_ID }}
    steps:
      - uses: actions/checkout@v4

      - name: Install Bitwarden CLI
        run: |
          curl -L "https://vault.bitwarden.com/download/?app=cli&platform=linux" -o bw.zip
          unzip bw.zip
          chmod +x bw
          sudo mv bw /usr/local/bin/

      - name: Backup Bitwarden
        run: |
          bw login --apikey
          bw export --format json --session $(bw unlock --passwordenv BW_PASSWORD --raw) --raw | openssl aes-256-cbc -a -salt -pbkdf2 -k $BW_PASSWORD -out personal.json.enc
          bw export --format json --session $(bw unlock --passwordenv BW_PASSWORD --raw) --raw  --organizationid "$BW_ORGANIZATION_ID" | openssl aes-256-cbc -a -salt -pbkdf2 -k "$BW_PASSWORD" -out organization.json.enc
          bw lock
          bw logout

      - if: ${{ !env.ACT }}
        uses: stefanzweifel/git-auto-commit-action@v5

Benefits of Using the Official Binary#

  1. No dependency management: No npm packages to manage or version conflicts to worry about
  2. No CommonJS/ESM issues: The binary has no transitive dependency problems
  3. No Node.js version constraints: Don't need to install Node.js at all
  4. Faster CI runs: Downloading and extracting a binary is faster than npm install
  5. Simpler commands: Just bw instead of ./node_modules/@bitwarden/cli/build/bw.js
  6. More reliable: Official binary is the recommended approach by Bitwarden

Testing Locally with act#

If you use act to test GitHub Actions locally, you can test the workflow with:

act workflow_dispatch

Note: the .secrets file with the actual secrets must be provided to properly run this locally. Keeping this file locally however poses quite a security risk, so it is better to test this only on GitHub where the environment is a little bit more secure.

Conclusion#

When dependencies start causing compatibility issues, sometimes the best solution is to step back and ask: "Do I even need these dependencies?" In this case, switching from the npm package to the official binary eliminated all the problems and resulted in a cleaner, more maintainable solution.

If you're using Bitwarden CLI in your CI/CD pipelines, I highly recommend using the official binary instead of the npm package to avoid similar issues in the future. Enjoy!