# bitwarden-print Render a Bitwarden `.json` export as a dense PDF: a summary, a page-numbered index, and one compact block per vault item that prints **every** value in the export — known fields first (username, password, TOTP, card, identity, SSH key, custom fields, notes, history, attachments), then everything else alphabetically, including unknown/extra keys and empty objects/lists. Everything runs in a throwaway Podman pod on this machine; the vault never leaves it. ## Requirements - Podman (tested with 6.x) - On macOS: `podman machine start` (the default machine is used) ## Quick start ```sh ./bitwarden-print.sh ~/Downloads/bitwarden_export_20261113.json # -> ~/Downloads/bitwarden_export_20261113.pdf ``` Custom output and options: ```sh ./bitwarden-print.sh vault.json -o ~/Desktop/vault.pdf \ --paper letter --sort name --title "Vault 2026-11-13" ``` Mask secrets instead of printing them: ```sh ./bitwarden-print.sh vault.json --mask-secrets ``` Options: | Option | Meaning | | --- | --- | | `-o, --output FILE` | output PDF (default: next to the export, same basename) | | `--sort folder\|name\|type\|as-is` | item order (default: `folder`, then name) | | `--title TEXT` | document title (also the page footer) | | `--paper a4\|letter` | page size (default: `a4`) | | `--mask-secrets` | mask `login.password`, `login.totp`, `card.number`, `card.code`, `identity.ssn`, `sshKey.privateKey`, …; values keep a length hint | ## What the PDF contains 1. **Header** — source file, item count, value count, timestamp. 2. **Summary** — export metadata, item/type/folder counts, custom field, attachment and password-history totals, created/revised date ranges, plus a collections table if the export has one. 3. **Index** — one line per item (type, name, folder, username) with a real page number, clickable in the PDF. 4. **Items** — for each item a heading (number, type badge, name, folder) and a two-column table of `json.path` → value. Values wrap, newlines in notes are preserved, and empty values (`null`, `""`, `{}`, `[]`) are shown dimmed so nothing silently disappears. Items are not truncated: an export with many entries produces many pages, but each line is only a few millimetres tall. ## How it runs ``` ./bitwarden-print.sh -> podman build -t localhost/bitwarden-print:latest . # Python + WeasyPrint image -> podman pod create --name bitwarden-print-pod -> podman create --pod bitwarden-print-pod ... bitwarden-print-run -> podman cp export.json bitwarden-print-run:/export.json # no bind mounts -> podman start -a bitwarden-print-run # render.py -> /output.pdf -> podman cp bitwarden-print-run:/output.pdf -> pod, container and temp file are removed ``` The input is copied into the container instead of bind-mounted, so arbitrary host paths work (including `/tmp`, which the Podman machine does not share) and no SELinux relabelling is required. Manual equivalent: ```sh podman build -t localhost/bitwarden-print:latest . podman pod create --name bitwarden-print-pod podman create --pod bitwarden-print-pod --name bw localhost/bitwarden-print:latest \ /export.json -o /output.pdf podman cp vault.json bw:/export.json podman start -a bw podman cp bw:/output.pdf ./vault.pdf podman rm -f bw && podman pod rm -f bitwarden-print-pod ``` ## Files - `bitwarden-print.sh` — CLI wrapper that builds the image and drives the pod - `Containerfile` — `python:3.12-slim-bookworm` + WeasyPrint (Pango/Cairo) + DejaVu fonts - `render.py` — JSON → HTML → PDF, no data leaves the container - `examples/sample-export.json` — synthetic vault used for testing (all values fake) Run the renderer directly on a host that has WeasyPrint installed: ```sh pip install weasyprint==62.3 pydyf==0.10.0 ./render.py examples/sample-export.json -o /tmp/sample.pdf --html-out /tmp/sample.html ``` ## Security notes - The generated PDF contains your vault in **plaintext** unless you pass `--mask-secrets`. Treat the PDF like the export itself: keep it local, and delete it when you are done. - `--mask-secrets` only masks well-known secret paths. Secrets stored inside custom fields (`fields[i].value`), notes or file names are printed as-is. - Encrypted exports (`"encrypted": true`) are rejected with a hint: re-export from Bitwarden as `.json` without a file password first. - The container is removed after each run, but the image (`localhost/bitwarden-print:latest`) stays cached for the next run.