Security

How to Use Bitwarden CLI (bw) with Practical Examples

A password pasted into a shell script outlives every rotation policy you have. The Bitwarden CLI (bw) exists so your scripts, cron jobs, and CI pipelines can pull secrets from an encrypted vault at runtime instead of carrying them around in plain text. It is a full vault client for the terminal: everything you can do in the browser extension, you can do from a shell, and pipe into whatever needs the secret.

Original content from computingforgeeks.com - post 170229

This Bitwarden CLI tutorial works through the whole toolchain with real, tested commands: installing bw, logging in with your master password or an API key, managing sessions with BW_SESSION, creating and editing vault items, generating passwords, sharing secrets with Bitwarden Send, exporting backups, and running the built-in REST API for automation. Every command here works against the Bitwarden cloud and against a self-hosted Vaultwarden server, which is what the examples run on.

Tested July 2026 on Ubuntu 26.04 LTS with Bitwarden CLI 2026.6.0 against a self-hosted Vaultwarden server.

Step 1: Install the Bitwarden CLI

The CLI ships as a single self-contained binary, so the cleanest install on Linux is the official zip. It bundles its own Node.js runtime and needs no dependencies:

curl -Lso bw-linux.zip "https://bitwarden.com/download/?app=cli&platform=linux"
unzip bw-linux.zip
sudo install -m 755 bw /usr/local/bin/bw

Confirm the binary runs and note the version, because Bitwarden ships CLI releases on a near-monthly cadence and behavior does change between them:

bw --version

The version prints on a single line:

2026.6.0

Package managers work too. Snap and npm both delivered the same current release when we tested them, and the official GitHub releases page lists every build:

PlatformCommand
Linux (snap)sudo snap install bw
Any OS (npm, needs Node.js)npm install -g @bitwarden/cli
macOS (Homebrew, community-maintained)brew install bitwarden-cli
Windows (Chocolatey)choco install bitwarden-cli
Windows (winget, community-maintained)winget install -e --id Bitwarden.CLI

The npm route is the one to use on arm64 machines, since the prebuilt zip only ships for x64. One caveat on Homebrew: the formula is a GPL build that lacks the enterprise device-approval commands, which only matters if your organization uses SSO with trusted devices.

Step 2: Point the CLI at Your Server

By default bw talks to the Bitwarden US cloud at vault.bitwarden.com. If that is where your account lives, skip this step. Everyone else, and that includes EU cloud accounts and every self-hosted server, must set the server URL before logging in:

bw config server https://vault.example.com

The CLI confirms the change:

Saved setting `config`.

Running the same command with no URL prints the currently configured server, which is the first thing to check when a login fails. EU cloud users point at https://vault.bitwarden.eu. If you run Vaultwarden behind Nginx with Let’s Encrypt or the official Bitwarden server in Docker, use that HTTPS URL here.

A self-hosted server with a self-signed or private-CA certificate needs one extra variable, because bw validates TLS through its bundled Node.js runtime rather than the system trust store:

export NODE_EXTRA_CA_CERTS="/etc/ssl/certs/my-private-ca.pem"

Check where you stand at any time with bw status:

bw status

Before authentication it reports unauthenticated:

{"serverUrl":"https://vault.example.com","lastSync":null,"status":"unauthenticated"}

Step 3: Log In with Your Master Password

Interactive login is the right choice on a workstation. Run it with just your email and let the CLI prompt for the master password, so the password never lands in your shell history or the process list:

bw login [email protected]

After the hidden password prompt (and a two-step login code if your account has one, passed with --method and --code in scripts), the CLI confirms authentication and hands you a session key:

You are logged in!

To unlock your vault, set your session key to the `BW_SESSION` environment variable. ex:
$ export BW_SESSION="P4tHpDULkFR5+NLL1lbfxD43q9NqIS2tmKnG0GMAn/Ft8w4JOipXty4uY4EQ5/gkTXDPGpidXuoC155F65X5sQ=="

You can also pass the session key to any command with the `--session` option.

The CLI does accept the password as a second positional argument. Do not use that form on a shared machine: bw login [email protected] MyPassword writes your master password into shell history and exposes it to anyone who can read /proc while the command runs. The failure mode when you fat-finger the password is at least honest:

Username or password is incorrect. Try again

Here is what a clean run looks like on our lab box, from version check to an unlocked vault:

Bitwarden CLI login with master password, BW_SESSION export and unlocked status on Ubuntu

The full flow on our lab box: version check, login against the self-hosted server, session export, and a status readout showing unlocked.

Step 4: Understand Login vs Unlock, and Sessions

This distinction trips up almost everyone the first time. Login authenticates you to the server and pulls down your encrypted vault. Unlock derives the decryption key from your master password and issues a session key that can actually read the data. The vault status moves through three states: unauthenticated (not logged in), locked (logged in, data encrypted), and unlocked (session key active).

Every read or write command needs an unlocked vault. The session key from login or unlock travels in one of two ways. The environment variable is the usual one:

export BW_SESSION="P4tHpDULkFR5+NLL1lbfxD43q9NqIS2tmKnG0GMAn/Ft8w4JOipXty4uY4EQ5/gkTXDPGpidXuoC155F65X5sQ=="

Or pass it per command with --session "$BW_SESSION". For scripts, --raw makes unlock print nothing but the key, which is exactly what you want to capture:

export BW_SESSION=$(bw unlock --raw)

One behavior to burn into memory before you automate anything: every new unlock invalidates all previous session keys. The CLI’s own help states it plainly (“After unlocking, any previous session keys will no longer be valid”), and we confirmed it in testing. A second script that runs bw unlock silently kills the session of the first. Unlock once, store the key, and reuse it everywhere instead of unlocking per task.

When you walk away from the machine, drop the decryption key from memory:

bw lock

Any vault command that runs against a locked vault in a non-interactive context fails with a clear error. Add --nointeraction in scripts so it fails instead of hanging on a hidden prompt:

Vault is locked.

bw logout goes further than lock: it deletes the local copy of the encrypted vault and forgets the account entirely. Use it on any machine you do not fully control.

Step 5: Log In with an API Key

Interactive prompts and automation do not mix. For servers, containers, and CI runners, Bitwarden issues each account a personal API key: an OAuth client_id and client_secret pair. Find it in the web vault under Settings > Security > Keys, behind the View API key button, which asks for your master password before revealing anything.

Bitwarden web vault Security Keys page with View API key and Rotate API key buttons

The CLI reads the pair from two environment variables, then authenticates without a single prompt:

export BW_CLIENTID="user.f060d6a2-c270-4ab3-94f2-2e228fb5b32d"
export BW_CLIENTSECRET="IOt2gZ5dERtUV3EvQyy1qyYa8CvH9T"
bw login --apikey

Authentication succeeds, and the output hints at the part most people miss:

You are logged in!

To unlock your vault, use the `unlock` command. ex:
$ bw unlock

The API key authenticates you. It cannot decrypt anything. Bitwarden is zero knowledge: the decryption key derives only from your master password, which the server never sees, so after an API key login the vault sits in the locked state and bw list items still fails. This is a feature. A stolen API key alone gets an attacker an encrypted blob, not your passwords.

To finish the non-interactive flow, feed the master password to unlock from an environment variable or a root-owned file. Both flags exist for exactly this:

export BW_PASSWORD='YourMasterPassword'
export BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw)

Or with a password file, locked down before anything else touches it:

chmod 600 ~/.bw-master
export BW_SESSION=$(bw unlock --passwordfile ~/.bw-master --raw)

In CI, put BW_CLIENTID, BW_CLIENTSECRET, and BW_PASSWORD in the platform’s secret store, never in the repository. And if a client secret ever leaks, the Rotate API key button next to View API key invalidates it immediately; rotating does not change your master password or re-encrypt the vault.

Step 6: Read Secrets from the Vault

With an unlocked session in place, pull a fresh copy of the vault before reading. Writes push to the server automatically, but reads work from the local encrypted cache, so a long-lived session drifts unless you sync:

bw sync

Listing returns JSON arrays, which makes jq the CLI’s natural partner:

bw list items | jq -r '.[] | .name'

Our lab vault holds four items:

Old FTP server
PostgreSQL prod
web01 recovery codes
web01 root

Filter server-side with --search, or by container with --folderid, --collectionid, and --organizationid. The get subcommand fetches a single object, and its field shortcuts are what you will use daily:

bw get password "web01 root"
bw get username "web01 root"
bw get notes "web01 recovery codes"
bw get totp "web01 root"

Each prints the bare value with no decoration, ready for command substitution. The TOTP shortcut returns the current six-digit code computed from the item’s stored seed (a paid feature on the Bitwarden cloud, included in Vaultwarden):

989292

The read commands in one terminal session, including a Send created at the end:

Bitwarden CLI get password, get totp, generate passphrase and send commands with output

get accepts a name fragment, but it demands exactly one match. Two of our items contain “example.com” in their URIs, and the CLI refuses to guess:

More than one result was found. Try getting a specific object by `id` instead. The following objects were found:
42ddd407-e7dd-4e6d-a304-c2f658995d2a
105a656c-c1da-4c4f-8689-ab886836123a

In scripts, never rely on name matching. Resolve the ID once and reference it everywhere:

ITEM_ID=$(bw list items --search "web01 root" | jq -r '.[0].id')
bw get password "${ITEM_ID}"

Step 7: Create Folders and Items

Creation follows one pattern everywhere: fetch a JSON template, modify it, base64-encode it, submit it. The bw get template and bw encode commands exist purely for this pipeline. A folder is the simplest case:

bw get template folder | jq '.name="Servers"' | bw encode | bw create folder

The server responds with the created object, ID included:

{"name":"Servers","object":"folder","id":"44355598-1443-4762-95ec-7b6afd46fc95"}

A login item works the same way, just with more fields. This one stores a root login with a URI, a TOTP seed, and a password generated inline so a human never sees or types it. Capture the folder ID first, then build the item:

FOLDER_ID=$(bw list folders | jq -r '.[] | select(.name=="Servers") | .id')

bw get template item | jq --arg pass "$(bw generate -ulns --length 20)" --arg fid "${FOLDER_ID}" \
  '.name="web01 root" | .login={"username":"root","password":$pass,"uris":[{"uri":"https://web01.example.com"}],"totp":"JBSWY3DPEHPK3PXP"} | .folderId=$fid' \
  | bw encode | bw create item

The response echoes the stored item, and it is the last time that password needs to exist outside the vault:

{"type":1,"name":"web01 root","favorite":false,"reprompt":0,"id":"105a656c-c1da-4c4f-8689-ab886836123a","collectionIds":[],"object":"item","folderId":"44355598-1443-4762-95ec-7b6afd46fc95","fields":[],"login":{"uris":[{"uri":"https://web01.example.com"}],"fido2Credentials":[],"username":"root","password":"Cu0w9N^eZi3w4ukDhGV1","totp":"JBSWY3DPEHPK3PXP","passwordRevisionDate":null},"passwordHistory":[],"creationDate":"2026-07-20T17:13:09.696Z","revisionDate":"2026-07-20T17:13:09.697Z","attachments":[]}

Secure notes use .type=2 with a secureNote stanza instead of login. Everything the CLI writes shows up in the web vault instantly, which is also the quickest way to verify a scripted import did what you intended:

Bitwarden web vault showing login items, secure note and attachment created from the CLI

All three items in that screenshot, including the attachment paperclip on web01 root, came from the terminal.

Step 8: Edit Items Without Opening a Browser

Editing is a read-modify-write of the full item JSON. bw edit replaces the object, so pull the current state, patch one field with jq, and push it back. Rotating a password becomes a one-liner:

bw get item "${ITEM_ID}" | jq --arg p "$(bw generate -ulns --length 20)" '.login.password=$p' \
  | bw encode | bw edit item "${ITEM_ID}"

The response proves the rotation happened: the new password is live and the old one moved into passwordHistory:

"login":{"username":"root","password":"2KBiu#H3XLf2E3azb#t2","totp":"JBSWY3DPEHPK3PXP","passwordRevisionDate":"2026-07-20T17:14:11.127Z"},
"passwordHistory":[{"lastUsedDate":"2026-07-20T17:14:11.127Z","password":"Cu0w9N^eZi3w4ukDhGV1"}]

Step 9: Generate Passwords and Passphrases

bw generate deserves use even outside vault workflows. The default is 14 characters of mixed-case letters and numbers with no specials, which is weaker than it should be, so be explicit. Uppercase, lowercase, numbers, specials, 20 characters:

bw generate -ulns --length 20

A fresh value every run:

sOIe^eGgHn4dx$gBDYh#

For anything a human must type, passphrases beat character soup:

bw generate --passphrase --words 4 --separator "-" --capitalize --includeNumber

Which produces something memorable but still strong:

Pauper-Juvenile-Worried-Occultist8

Step 10: Trash, Restore, and Permanent Delete

Deletes are soft by default. The item moves to the trash, where it survives for 30 days on Bitwarden servers (Vaultwarden keeps trash until the admin sets TRASH_AUTO_DELETE_DAYS):

bw delete item 80c29bf1-dca1-442b-9bd6-1c30a55c2419

Confirm where it went, and bring it back if that delete was a mistake:

bw list items --trash | jq '.[] | {id, name}'
bw restore item 80c29bf1-dca1-442b-9bd6-1c30a55c2419

Adding --permanent to the delete skips the trash entirely. There is no restore from that, so treat it like rm on a production box.

Step 11: Attach Files to Vault Items

Key files, kubeconfigs, and recovery PDFs belong in the vault next to the credentials they pair with. Attachments hang off an existing item (file storage is a paid feature on the Bitwarden cloud; Vaultwarden includes it):

bw create attachment --file ./web01-backup.key --itemid "${ITEM_ID}"

Pulling it back down on another machine is symmetric:

bw get attachment web01-backup.key --itemid "${ITEM_ID}" --output ./restored.key

The CLI confirms the write with Saved /home/user/restored.key. This pattern pairs well with tools like chezmoi for dotfile management, which can template secrets straight out of a Bitwarden vault.

Step 12: Share Secrets with Bitwarden Send

Stop pasting credentials into Slack. Bitwarden Send creates an encrypted, self-destructing link, and the CLI builds one in a single command. This Send hides the text behind a reveal click and deletes itself after two days:

bw send -n "Temp DB credentials" -d 2 --hidden "dbadmin / S3cr3t-rotate-me"

The JSON response contains the one thing you share, the accessUrl:

"accessUrl":"https://vault.example.com/#/send/7NhmhbFrRUqhsY46zUtFQQ/QdPDreRP1rGlRS20W9MR_A"

The decryption key rides in the URL fragment after the last slash, which never reaches the server logs. Audit what is outstanding with bw send list, revoke early with bw send delete <id>, and add --password or --maxAccessCount 1 for anything genuinely sensitive. Text Sends are free everywhere; file Sends need a paid plan on the Bitwarden cloud.

Step 13: Export the Vault

An encrypted vault you cannot restore is still a single point of failure. Export supports csv, json, encrypted_json, and zip (the zip includes attachments). Plaintext JSON is for migrations, and it belongs on encrypted storage only, deleted right after use:

bw export --format json --output ./vault-backup.json

For a recurring backup job, use encrypted_json with its own passphrase, so the backup does not depend on your account encryption key and survives even an account reset:

bw export --format encrypted_json --password "Backup-Encryption-2026" --output ./vault-backup.enc.json

One version note from testing: older guides show the master password as a positional argument (bw export MyPassword --format json), and the CLI’s own help text still lists that form. The current release rejects it with error: too many arguments for 'export'. Run the flag-only form above; the CLI prompts for master password confirmation when it needs one.

bw import handles the return trip and understands formats from dozens of other password managers. Run bw import --formats for the full list.

Step 14: Run the Built-In REST API with bw serve

Shelling out to bw for every secret is slow, because each invocation spins up a Node.js runtime. For anything that reads secrets in a loop, bw serve starts a local REST API that holds the unlocked session in one process:

bw serve --port 8087

Every CLI operation maps to an endpoint. Status, search, and TOTP look like this:

curl -s http://localhost:8087/status | jq '.data.template.status'
curl -s "http://localhost:8087/list/object/items?search=web01" | jq '.data.data[].name'
curl -s http://localhost:8087/object/totp/${ITEM_ID} | jq -r '.data.data'

The responses come back as JSON envelopes with a success flag:

"unlocked"
"web01 recovery codes"
"web01 root"
553216

You can even lock and unlock over HTTP with POST /lock and POST /unlock. Now the warning, and do not skip this: the API has no authentication. Any process that can reach the port can read every secret in the unlocked vault. Bind it to loopback (the default), never pass --hostname all, run it as a dedicated user, and treat a shared host as a hostile one. The only guard built in is that requests carrying a browser Origin header get rejected, which stops web pages from probing the port but does nothing against local processes. If you need secrets on another machine, run bw serve there, not across the network.

Step 15: Put It Together in a Script

Everything above condenses into a repeatable pattern for unattended jobs. API key for authentication, master password from a protected file for decryption, one session reused for all reads:

#!/usr/bin/env bash
set -euo pipefail

# BW_CLIENTID and BW_CLIENTSECRET come from the environment (CI secret store)
bw login --apikey --nointeraction

export BW_SESSION=$(bw unlock --passwordfile /root/.bw-master --raw)
bw sync --nointeraction

DB_PASS=$(bw get password "PostgreSQL prod")
pg_dump "postgresql://dbadmin:${DB_PASS}@db01.example.com:5432/app" > /backup/app.sql

bw lock
unset BW_SESSION DB_PASS

We ran this exact flow end to end in the lab: API key login, file-based unlock, sync, and a clean password read with nothing sensitive in history, argv, or the terminal. If one machine needs to run jobs against two accounts, point each at its own state directory with BITWARDENCLI_APPDATA_DIR, because the CLI stores one login per data directory.

Security Checklist Before You Automate with bw

Run down this list before the first cron job goes live. Each line maps to a failure mode we would rather you read about here than discover in an incident review:

  1. The master password never appears as a command argument. Interactive prompt, --passwordenv, or --passwordfile only.
  2. Any password file is chmod 600, owned by the job’s user, and lives outside every git repository.
  3. BW_CLIENTID, BW_CLIENTSECRET, and BW_PASSWORD live in a CI secret store or systemd credentials, never in the unit file or pipeline YAML.
  4. Scripts unlock once and reuse BW_SESSION. A second unlock invalidates every session that came before it.
  5. Every unattended script ends with bw lock and unsets its secret variables; shared or untrusted machines get bw logout instead.
  6. bw serve binds to loopback only, and nothing else untrusted runs on that host.
  7. Plaintext exports are temporary artifacts: created on encrypted storage, consumed, then shredded. Recurring backups use encrypted_json with a dedicated passphrase.
  8. The API key gets rotated in the web vault the moment you suspect exposure, and your account has two-step login turned on regardless, ideally with hardware security keys.

With those in place, the Bitwarden CLI turns the vault from a browser convenience into the secrets backend for your whole shell environment, and every credential your automation touches has exactly one encrypted home. The official CLI documentation covers the remaining corners, including organization confirmation flows and SSO login.

Keep reading

UFW Firewall Commands with Examples on Ubuntu 24.04 / 22.04 Security UFW Firewall Commands with Examples on Ubuntu 24.04 / 22.04 Setup WireGuard VPN on Ubuntu 24.04 / Debian 13 / Rocky Linux 10 Debian Setup WireGuard VPN on Ubuntu 24.04 / Debian 13 / Rocky Linux 10 Install Kali Linux 2026.1 Step by Step [Full Guide] Security Install Kali Linux 2026.1 Step by Step [Full Guide] Best Security Keys for SSH and How to Set Up FIDO2 with a YubiKey Security Best Security Keys for SSH and How to Set Up FIDO2 with a YubiKey ParrotOS vs Kali Linux: Which Pentesting Distro to Choose Security ParrotOS vs Kali Linux: Which Pentesting Distro to Choose Webform Security Best Practices for Small Businesses Security Webform Security Best Practices for Small Businesses

Leave a Comment

Press ESC to close