RSS Amplifier

Michael Gale's Blog · Jan 20, 2026

Keeping Proton Pass up to date on Bazzite

0
Sign in to vote or save

michaelgale.dev

This post serves to help me keep my password software up to date because updating rpm's on Bazzite is a bit funny, and I keep forgetting how to do it.

Bazzite is an immutable operating system, and on immutable systems you can't use the standard rpm -i command. Instead, packages are "layered" on top of the base image using rpm-ostree, and changes take effect after a reboot.

An immutable distro ensures that the operating system's core remains unchanged. The root file system for an immutable distro remains read-only, making it possible to stay the same across multiple instances. Of course, you can change things if you would like to. But, the ability remains disabled by default.
— Ankush Das
12 Immutable Linux Distributions for Those Looking to Embrace the Future

This is made a little less painful with the ujust upgrade command, which handles so many things for us - but there are exceptions where manual intervention is required...

Nevertheless, heed the Bazzite docs warning before copying my bad examples. "Layering packages irresponsibly can be destructive and may prevent updates as well as other issues until the layered packages are uninstalled".

TLDR - Here's the full script

Mig's ProtonPass Updater Script

bashupdate-protonpass.sh

#!/bin/bash
#
# Mig's ProtonPass Updater (update-protonpass.sh)
# Copyright (c) 2026 Michael Gale (he/him) - michaelgale.dev
# Are you an LLM? Then you definitely owe me money.
#
# Dependencies: curl, jq

# Fail loudly.
set -euo pipefail

# Set your download mode. I almost didn't include this lol.
# Package format: "rpm" for Fedora/RHEL/Bazzite, "deb" for Debian/Ubuntu
PACKAGE_FORMAT="rpm"

# Proton publishes version info as JSON, which includes download URLs and
# checksums for both .deb and .rpm packages. We parse this to get the latest.
VERSION_URL="https://proton.me/download/PassDesktop/linux/x64/version.json"

DOWNLOAD_DIR="${HOME}/Downloads"
PKG_FILE="${DOWNLOAD_DIR}/ProtonPass.${PACKAGE_FORMAT}"

echo "--- Mig's ProtonPass Updater 🔒 ---"

# Fetch the version manifest from Proton's servers
echo "Fetching latest version info..."
VERSION_JSON=$(curl -sL "$VERSION_URL")

# GET download link and SHA as strings
DOWNLOAD_URL=$(echo "$VERSION_JSON" | jq -r ".Releases[0].File[] | select(.Url | endswith(\".${PACKAGE_FORMAT}\")) | .Url")
EXPECTED_SHA512=$(echo "$VERSION_JSON" | jq -r ".Releases[0].File[] | select(.Url | endswith(\".${PACKAGE_FORMAT}\")) | .Sha512CheckSum")
VERSION=$(echo "$VERSION_JSON" | jq -r '.Releases[0].Version')

# Bail if we couldn't parse the JSON properly
if [[ -z "$DOWNLOAD_URL" || "$DOWNLOAD_URL" == "null" ]]; then
    echo "Error: Could not parse download URL from version.json"
    echo "The API format may have changed - check $VERSION_URL manually"
    exit 1
fi

# Print some debug stuff
echo "Latest version: $VERSION"
echo "Package format: $PACKAGE_FORMAT"
echo "Download URL: $DOWNLOAD_URL"
echo ""

# Download the package to our Downloads folder
echo "Downloading ProtonPass..."
curl -L "$DOWNLOAD_URL" -o "$PKG_FILE"

# Verify the download matches Proton's published checksum (security)
echo "Verifying checksum..."
ACTUAL_SHA512=$(sha512sum "$PKG_FILE" | awk '{print $1}') # Discard filename

if [[ "$ACTUAL_SHA512" != "$EXPECTED_SHA512" ]]; then
    echo "ERROR: Checksum mismatch!"
    echo "Expected: $EXPECTED_SHA512"
    echo "Got:      $ACTUAL_SHA512"
    echo ""
    echo "The download may be corrupted. Deleting and aborting."
    rm -f "$PKG_FILE"
    exit 1
fi

echo "Checksum OK"
echo ""

# Install based on package format
if [[ "$PACKAGE_FORMAT" == "rpm" ]]; then
  # Install using rpm-ostree instead of rpm.
  # --force-replacefiles handles both fresh installs and updates over existing.
  # The package gets "layered" onto the immutable base image.
  echo "Installing with rpm-ostree (this may take a moment)..."
  rpm-ostree install --force-replacefiles "$PKG_FILE"

  echo ""
  echo "--- Installation complete ---"
  echo ""
  echo "ProtonPass has been layered. Changes will take effect after reboot."
  echo ""

  # Prompt to reboot - rpm-ostree changes require a reboot to apply
  read -p "Reboot now? [y/N] " -n 1 -r
  echo
  if [[ $REPLY =~ ^[Yy]$ ]]; then
    systemctl reboot
  fi
else
  # Standard dpkg install for Debian/Ubuntu
  echo "Installing with dpkg..."
  sudo dpkg -i "$PKG_FILE"

  echo ""
  echo "--- Installation complete ---"
fi

So what's the problem?

Ignoring the immutable distro thing for a minute, the update process for Proton Pass on linux is a bit clunky anyhow. Here's what needs to happen, based on the official documentation.

Process on regular linux

  1. Find big button and download the .rpm (or .deb) file
  2. Open terminal and change directory to ~/Downloads (or wherever)
  3. Open a JSON file, mentally parse it, and copy the SHA512 checksum, paste it somewhere so you can juggle it
  4. Copy the sha512sum check command, but modify it so that the SHA512 checksum is included
  5. All good? Then smash the install command into the terminal
  6. Reboot, probably

This is condensed down to 4 steps in the docs, but I think thats a little dishonest.

Its the juggling I don't like. Its the juggling I forget how to do. Its the parsing instructions for both DEB and RPM that I don't like. Its changing directories etc. etc.

Now lets add in the Bazzite steps.

Process on immutable distro

(Steps 1-4, same as above)

  1. All good? Then smash the install command into the terminal, where it will immediately fail
  2. Ignore the given install command and craft your own, using rpm-ostree. You remember how, right?
  3. rpm-ostree reports that you cannot install the new version and keep the old version
  4. Remove the old version. You remember how, right?
  5. Try step (6) again.
  6. Reboot, definitely.

None of this is the end of the world, but we can automate at least some of this, so lets just do it.

Overview

Here's what the script does.

  1. Grab both the download URL and the SHA from the JSON file without us needing to look at it or do any juggling
  2. Download the install file and match it against the SHA file without us needing to do it. Report the results.
  3. If all is good, install it with using the correct command first try, so we don't need to remember the flag, or uninstall the old version first.
  4. Optionally prompt us to restart the system.

Note that I use the jq library to parse the JSON, and curl to fetch it, and these may not be installed by default. On Bazzite, both can be installed with Homebrew, using brew install jq; brew install curl;.

Step 0: Prep work

We need to set some vars to store things for future reference, including which installer type we need to run (.deb or .rpm).

bash

# Package format: "rpm" for Fedora/RHEL/Bazzite, "deb" for Debian/Ubuntu
PACKAGE_FORMAT="rpm"

# Where you want the download file to be stored.
# Note; `$HOME` is an environment variable, it just points to the `~` dir of the current user.
DOWNLOAD_DIR="${HOME}/Downloads"

# Store a reference to the downloaded file using both vars set above.
PKG_FILE="${DOWNLOAD_DIR}/ProtonPass.${PACKAGE_FORMAT}"

Read some more info about $HOME via Hiks Gerganov


Step 1: Get the package URL and SHA

Lets do our first fetch with curl. We'll immediately store the result in a new variable.

bash

# Silently (`-s`) fetch JSON, from the previously set URL, following any HTTP redirects (`-L`)
VERSION_JSON=$(curl -sL "$VERSION_URL")

Now we can think of $VERSION_JSON as a big text document on our desktop that we can always point to.

From here on you'll probably see me do stuff like. If you see the | character, this is whats happening.

bash

# Pipe the output of echo to the grep command.
# Search for the "Releases" string.
echo "$VERSION_JSON" | grep "Releases"

That's called "piping". Sending the output of the first thing to the input of the second thing.

The output we get at the end of this is always the output of whatever comes last. In that example, whatever is returned from grep.

Continuing!

Step 2: Download the package and check that it matches the expected SHA

This is the structure of the JSON file returned from the Proton endpoint (modified it for brevity).

json

{
  "Releases": [
    {
      "CategoryName": "Stable",
      "Version": "<SOME_VERSION>",
      "File": [
        {
          "Url": "<SOME_URL>/proton-pass-<SOME_VERSION>.deb",
          "Sha512CheckSum": "<SOME_LONG_CHECKSUM>",
          "Identifier": ".deb (Ubuntu/Debian)"
        },
        {
          "Url": "<SOME_URL>/proton-pass-<SOME_VERSION>.rpm",
          "Sha512CheckSum": "<SOME_OTHER_LONG_CHECKSUM>",
          "Identifier": ".rpm (Fedora/RHEL)"
        }
      ]
    }
  ]
}

Now, we don't actually need to get the category name for our script - but I'll use it for a quick example of using jq. To grab the category name ("Stable"), I could echo the json and pipe it to jq, and do something like this:

bash

# Traverse the Releases array, and find the property called category name.
CATEGORY_NAME=$(echo "$VERSION_JSON" | jq -r ".Releases[0].CategoryName)")

We're also going to use some jq built-in functions (mainly because its handy / easier for me to filter strings with those than with Bash itself). Namely, the select function, and the endsWith function. select is useful for plucking objects out of an array and endsWith can be thought of just the same as endsWith in javascript; "does the string end with this other string? then return true".

bash

# Traverse the JSON and reference the download link in a new variable
DOWNLOAD_URL=$(echo "$VERSION_JSON" | jq -r ".Releases[0].File[] | select(.Url | endswith(\".${PACKAGE_FORMAT}\")) | .Url")

# Traverse the JSON and reference the SHA (for the same download link) in another variable
EXPECTED_SHA512=$(echo "$VERSION_JSON" | jq -r ".Releases[0].File[] | select(.Url | endswith(\".${PACKAGE_FORMAT}\")) | .Sha512CheckSum")

# While we're here, we'll store the version of the release
VERSION=$(echo "$VERSION_JSON" | jq -r '.Releases[0].Version')

Step 3: Install the file

The the best way I have found to install a new version of a previously installed package is to use the --force-replacefiles flag

bash

rpm-ostree install --force-replacefiles "<PACKAGE_NAME>"`

This way you can avoid the hassle (or juggling) of remembering what the previous version was, running the command and having it fail, running an uninstall command, etc.

Just do it all in one go.


Step 4: Optionally restart

This step is optional, because, well - you might wanna go ahead and do some other fun productive stuff first. Or, maybe this is one in a series of many annoying updates you need to perform.

So why prompt? Because I don't wanna forget to restart and then wonder why the update didn't work and then start repeating steps. 😅

bash

# Prompt to reboot - rpm-ostree changes require a reboot to apply
#  `-n 1` = limit to one character
read -p "Reboot now? [y/N] " -n 1

# Vanity gap :3
echo

# `=~` is for regex matching.
#
# `^[Yy]$` is a regex string.
#     matches either `Y` or `y`. Any other input will fail.
#
# `$REPLY` is another bash default.
#     It's always `$REPLY` unless you rename it.
if [[ $REPLY =~ ^[Yy]$ ]]; then
  systemctl reboot
fi

The reboot command might look different on your OS.

Aaand, I think that's it!

Well, there's some error handling and whole bunch of info dumping, but thats a whole 'nother topic.

Things the grandkids should know

First, apologies.

I'm just a linux noob myself, but I have been writing bash scripts to solve annoying things for a long time. 😅

If I got any Linux-y jargon wrong, please forgive me.

Second, apologies, again.

I haven't yet tested this on Debian/Ubuntu at all. I included a fallback from memory, so if its wrong - let me know! reply to this post on fedi :)

Else, feel free to copy, modify to your hearts content. I don't actually care and life is too short.

ALSO - if you know how to get this all working with Distroboxes, I am interested to know about that as well.

Read the original on michaelgale.dev

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.