bwsec syncs secrets between a Bitwarden/Vaultwarden vault and local files.
It manages a bw serve daemon, reads a declarative bwsec.toml config, fetches vault items and attachments in parallel, and writes files to disk with strict path containment and 0600 permissions.
- Full round-trip: scaffold with
init, upload withpush, fetch withpull, audit withdiff, wipe withclean - Single binary, zero runtime deps — no
jq,base64, or extra scripts required - Managed
bw servedaemon — no manualbwsession handling needed - Parallel attachment downloads,
copy_tofor writing one secret to multiple paths - Conflict-safe push — name-keyed entries always create a new vault item (never overwrite an existing one) and are auto-pinned to the vault UUID; UUID-keyed entries update the matched item by ID
- Non-interactive auth for CI —
--password-file,--password-env, or$BW_PASSWORD - Multi-account —
--bw-config/$BWSEC_BW_CONFIGsets$BITWARDENCLI_APPDATA_DIR - Pre-push backups — old attachments saved to
.bwsec-backups/<timestamp>/before replacement - Strict path containment — relative paths only, no
.., destinations verified under project root - Secure file permissions — secrets written to disk are created at
0600(owner read/write only) on Unix
If your team already keeps project secrets as Bitwarden or Vaultwarden vault attachments and pulls them with ad-hoc bw/jq/base64 shell scripts, bwsec replaces that with a declarative config and a single binary.
bwsec is a thin wrapper around the bw CLI — its capability ceiling is whatever bw exposes. If you are not already using the vault as a file store, a purpose-built alternative may be a better fit:
- Bitwarden Secrets Manager — machine-account-based secrets platform with its own CLI; models secrets as key-value pairs, not files.
- SOPS + age — encrypted secrets committed to git; no external daemon or vault account required.
- Bitwarden CLI (
bw) installed and on$PATH - A Bitwarden or Vaultwarden account with your project secrets as vault items
brew install pisand/tap/bwseccargo install bwsecDownload the latest archive for your platform from the GitHub Releases page, extract, and place the binary on your $PATH. Builds are provided for macOS (Apple Silicon + Intel), Linux (x86-64 + ARM64, musl), and Windows (x86-64).
git clone https://github.com/pisand/bwsec.git
cd bwsec
cargo build --release
# binary: target/release/bwsecbwsec init # scaffold bwsec.toml, review in $EDITOR
bwsec push # upload local files as vault attachmentsCreate a bwsec.toml in your project root and run bwsec:
collection_id = "5cd3af3c-0afe-415d-963b-3ce5a783af48"
[items]
[items.".env"]
path = ".env"After running bwsec push, the name key ".env" is automatically rewritten to the vault item's UUID in bwsec.toml.
See config.example.toml for the full configuration reference.
Run bwsec <command> --help for the full flag list. All commands accept --bw-config <DIR> and --bw-port <PORT> as global flags.
| Command | Description |
|---|---|
pull (default) |
Fetch secrets from the vault and write them to local files |
diff |
Compare vault content against local files; exits 1 if any differ or are missing |
init |
Interactively scaffold bwsec.toml and open it in $EDITOR (TTY required) |
push |
Upload local files to the vault as attachments; backs up old ones first (TTY required) |
clean |
Delete all secrets written locally by bwsec (no vault access needed) |
Common flags: -n/--dry-run, -v/--verbose, -q/--quiet, --no-sync, -j/--max-parallel (default: 8), --password-env, --password-file.
bwsec.toml lives in the project root. All item paths are resolved relative to the directory containing the config file.
# Optional: restrict fetch to a single collection (faster, narrows scope)
# Must be a valid UUID (36 characters, 4 dashes)
collection_id = "5cd3af3c-0afe-415d-963b-3ce5a783af48"
# Optional: skip vault sync before operations (default: sync = true)
# sync = false
# Optional: Bitwarden CLI data directory for multi-account setups
# bw_config = "~/.config/bw-work"
[items]
# Name key — on `push`, always creates a new vault item and rewrites this
# key to the vault UUID automatically. On `pull`/`diff`, matches by name.
[items.".env"]
path = ".env"
# copy_to: also write to a second path after fetching
[items."sentry.properties"]
path = "android/sentry.properties"
copy_to = "ios/sentry.properties"
# UUID key — set automatically after the first push. On `push`, updates the
# existing item by ID. On `pull`/`diff`, also matches by ID.
[items."abc12345-0afe-415d-963b-3ce5a783af48"]
path = "special.env"See config.example.toml for a complete reference including all fields and defaults.
bwsec resolves the master password in this order:
--password-file <FILE>— first non-empty line of the file--password-env <VAR>— value of the named environment variable$BW_PASSWORD— detected automatically- Interactive prompt — requires a TTY; fails in non-interactive environments
For CI, combine --password-env with --yes (skip confirmation) to run fully non-interactively:
bwsec pull --password-env VAULT_PASS --yes --no-sync --quietFor multi-account setups, point bwsec at a specific Bitwarden CLI data directory via --bw-config, $BWSEC_BW_CONFIG, or the bw_config config field — all three set $BITWARDENCLI_APPDATA_DIR. For parallel CI jobs on the same host, use --bw-port to give each job an isolated daemon port.
- Walks up from cwd to find
bwsec.toml. The config's parent directory becomes the project root. - Spawns a
bw servedaemon and unlocks it with the resolved master password. - Optionally syncs the vault, then fetches all items in one request (scoped to
collection_idif set). - pull/diff: matches each config item by UUID (exact) or by name (case-sensitive). push: UUID-keyed entries update the matched item by ID; name-keyed entries always create a new vault item, then rewrite the config key to the returned UUID. Downloads attachments in parallel.
- Verifies every destination path stays within the project root, then writes secret files at mode
0600on Unix. - Terminates the daemon on exit.
bwsec delegates all crypto and API access to the bw CLI — it makes no direct network requests, uses no unsafe Rust, and links against no crypto or networking libraries. bwsec.toml contains only item names and local paths, never secret values.
| Property | How |
|---|---|
| File permissions | Secrets written by pull and push are created at 0600 on Unix; backup copies in .bwsec-backups/ are also written at 0600 and the backup directory is created at 0700 |
| No secret logging | Only item names and paths are printed, never content |
| Path containment | Paths must be relative and resolve within the project root; .. rejected |
| Symlink protection | Destination paths canonicalized and verified immediately before each write |
| Disk flush | All writes call sync_all() |
Known limitations: file permissions are not enforced on Windows (including backup copies in .bwsec-backups/); secret content is not zeroed from process memory; BW_SESSION is passed as an environment variable (standard bw behavior); a narrow TOCTOU window exists between the path-containment check and the file open.
- First attachment only. When a vault item has multiple attachments, only the first is fetched; any extras are silently ignored.
pushnever matches existing vault items by name. Name-keyed config entries (e.g.[items.".env"]) always create a new vault item — they never update an item that happens to share the same name. This prevents accidental overwrites when multiple projects use the same secret name. After creation, the config key is rewritten to the vault UUID.pushattachment replacement matches by filename. For UUID-keyed items, if the existing vault item already has an attachment with the same filename, the old one is backed up and replaced. A different filename uploads as a new attachment alongside existing ones. Renaming a local file beforepushtherefore uploads a new attachment; remove the stale one from the vault manually.pushrewritesbwsec.tomlin place. Two edits can occur: selecting a collection interactively writescollection_id, and creating new vault items rewrites the name key to the vault UUID. Both are non-atomic string edits; complex custom comments near the affected lines may be affected.inituses the file basename as the item key. Selecting two files with the same name (e.g.foo/.envandbar/.env) generates a key collision in the draft TOML. Fix the duplicate keys in$EDITORbefore continuing.cleanprompts before deleting.bwsec-backups/. After removing the paths listed inbwsec.toml,cleanasks whether to also delete the local backup directory. Answern(or press Ctrl-C) to keep backups; in quiet or non-interactive mode the prompt is skipped and backups are left intact. Add.bwsec-backups/to your.gitignoreto prevent accidentally committing plaintext secrets:echo '.bwsec-backups/' >> .gitignore
Report security issues privately via GitHub Security Advisories.
| Problem | Solution |
|---|---|
bw not found |
Install the Bitwarden CLI and ensure it's on $PATH |
| "Not logged in" | Run bw login first |
| Vault locked with no TTY | Use --password-env or --password-file |
| Items not found | pull/diff: check names are exact and case-correct, or use a UUID key. push: name keys always create new items; use UUID keys to update an existing item by ID |
| Slow fetch | Set collection_id to limit the query scope |
| Port conflict between CI jobs | Use --bw-port to assign each job a unique port |
| Command hangs indefinitely | Kill the process; run pkill -f 'bw serve' to clear any stuck daemon; retry |
Plaintext files in .bwsec-backups/ |
Run bwsec clean and answer y at the backup prompt, or delete the directory manually; add .bwsec-backups/ to .gitignore |
Issues and pull requests are welcome on GitHub.