Share a Steam library between users on CachyOS

Share a single Steam library between multiple Linux users, giving each their own Proton compatibility data via a seat-aware symlink daemon.

Share

Sharing a single Steam library between multiple Linux users breaks Proton: each user needs their own compatdata directory, but a shared library leaves only one path for it, and switching users makes games fail with a Proton traceback. This setup puts the library on a shared btrfs subvolume with group permissions, then uses a small daemon that watches logind and repoints compatdata to the active user's home directory on every seat switch.

Create a subvolume for the library

The library lives on its own BTRFS subvolume rather than a plain directory, so filesystem snapshots (e.g. Timeshift) can exclude hundreds of gigabytes of game data while still capturing the rest of the root filesystem.

  1. Find your root device:
    findmnt -n -o SOURCE /
  2. Create mount location:
    sudo mkdir -p /mnt/btrfs-root
  3. Mount the top-level subvolume:
    sudo mount -o subvolid=5 <device> /mnt/btrfs-root
  4. Create a BTRFS subvolume for the library:
    sudo btrfs subvolume create /mnt/btrfs-root/@steam
  5. Find your filesystem UUID:
    sudo btrfs filesystem show /
  6. Create mount location:
    sudo mkdir -p /opt/SteamLibrary
  7. Edit /etc/fstab, add the following entry:
    UUID=<UUID> /opt/SteamLibrary btrfs subvol=/@steam,defaults,noatime,compress=zstd:1,nofail 0 0
  8. Check your configuration for errors:
    findmnt --verify
  9. Reload systemd units:
    sudo systemctl daemon-reload
  10. Mount the library:
    sudo mount /opt/SteamLibrary
  11. Create a group that will be used to access the library:
    sudo groupadd steam
  12. Add your user to the group:
    sudo usermod -aG steam $USER
  13. Set the permissions and ACLs:
sudo chown -R :steam /opt/SteamLibrary/

sudo chmod -R g+rwX /opt/SteamLibrary/
sudo chmod -R g+s /opt/SteamLibrary/

sudo setfacl -d -m g:steam:rwx /opt/SteamLibrary/
sudo setfacl -m g:steam:rwx /opt/SteamLibrary/
  1. Reboot and log back in.

Add the library to Steam

  1. Open Steam and click Steam > Settings > Storage,
  2. Click the drop down menu next to the selected storage device, click Add Drive
  3. Click the drop down menu on the pop-up,
    select Let me choose another location, click Add
  4. Browse to /opt/SteamLibrary, click Open.
  5. Click the drop down menu next to the selected storage device,
    click the newly added device.
  6. Click the overflow menu, click Make Default.
  7. Edit /SteamLibrary/libraryfolder.vdf,
    update "label" "" to "label" "/opt/SteamLibrary"

Create a login script to update CompatData

  1. Create /opt/SteamLibrary/steam-link.sh with the following contents:
#!/bin/bash

# Points the shared Steam library's compatdata at this user's personal folder.
# Meant to run at login, before Steam starts.

shared="/opt/SteamLibrary/steamapps/compatdata"
personal="$HOME/.local/share/Steam/steamapps/compatdata"

# Show a message via zenity popup when a graphical session is available,
# falling back to stderr-only for terminal/headless use.

notice() {
    local kind="$1"
    local msg="$2"
    local detail="$3"

    echo "$msg" >&2

    if command -v zenity &>/dev/null && [ -n "${DISPLAY}${WAYLAND_DISPLAY}" ]; then
        local text
        if [ -n "$detail" ]; then
            text=$(printf '%s\n\n%s' "$msg" "$detail")
        else
            text="$msg"
        fi
        zenity "--$kind" --title "Steam CompatData" "--text=$text" 2>/dev/null &
    fi
}

if [ "$(id -u)" -eq 0 ]; then
    notice error "Cannot run as root." "This script must run as your normal user."
    exit 1
fi

# Swapping compatdata while Steam runs risks corrupting prefixes.
if pgrep -x steam >/dev/null; then
    notice warning "Steam is currently running." "Skipping compatdata swap — it will retry at next login."
    exit 1
fi

if ! mkdir -p "$personal"; then
    notice error "Cannot create: $personal" "Check disk space and permissions on ~/.local/share/Steam."
    exit 1
fi

# Already correct? Quietly exit — makes every login idempotent and race-free.
if [ -L "$shared" ] && [ "$(realpath "$shared")" = "$(realpath "$personal")" ]; then
    exit 0
fi

# Clear whatever currently occupies the shared path.
if [ -d "$shared" ] && [ ! -L "$shared" ]; then
    # Real directory: only remove if empty — never destroy existing prefixes.
    if ! rmdir "$shared" 2>/dev/null; then
        notice error "$shared is a non-empty directory." \
            "Move its contents into $personal (or delete them), then log out and back in."
        exit 1
    fi
else
    rm -f -- "$shared"
fi

# Create the symlink.
if ! ln -s "$personal" "$shared"; then
    notice error "Cannot create $shared" "Check permissions on its parent directory (/opt/SteamLibrary/steamapps)."
    exit 1
fi

# We only get here when the symlink was newly created, so this fires once per machine rather than every login.
notice info "compatdata linked." "$shared now points at your personal folder."
  1. Make it executable:
    chmod +x /opt/SteamLibrary/steam-link.sh
  2. Open System Settings > Autostart.
  3. Click Add New, click Login Script.
  4. Browse to /opt/SteamLibrary/steam-link.sh, click Open.

The login script repoints a single shared symlink, which works well for sequential logins. With fast user switching, though, both users are logged in at once and one symlink can only serve one of them, so games running in the background session start failing when the symlink flips underneath them.

The fix is to stop sharing the path at all: give each user's session its own view of compatdata using mount namespaces. Every logged-in user sees their own home-directory compatdata at /opt/SteamLibrary/steamapps/compatdata simultaneously. There is no symlink to flip, so fast user switching can't break the background session.

Create the mount helper

  1. Create /usr/local/libexec/steam-mount-compatdata:
#!/bin/bash
# Bind-mount a user's personal compatdata over the shared Steam library
# path, INSIDE that user's mount namespace so only their processes see it.
#
# Usage: steam-mount-compatdata <uid> [--inside]
set -euo pipefail

SHARED_STEAMAPPS="/opt/SteamLibrary/steamapps"
SHARED_COMPATDATA="${SHARED_STEAMAPPS}/compatdata"

uid="$1"

# --- Resolve the user (fails loudly on bogus UIDs) -------------------------
if ! entry="$(getent passwd "$uid")"; then
    echo "steam-mount-compatdata: UID ${uid} not found" >&2
    exit 1
fi
user_name="$(cut -d: -f1 <<<"$entry")"
user_home="$(cut -d: -f6 <<<"$entry")"
primary_gid="$(cut -d: -f4 <<<"$entry")"

personal_compatdata="${user_home}/.local/share/Steam/steamapps/compatdata"

# --- Step 1: join the user manager's mount namespace -----------------------
# A system-level unit runs in the init namespace by default; mounting there
# would be global. nsenter into user@<uid>.service's namespace first.
if [[ "${2:-}" != "--inside" ]]; then
    mgr_pid="$(systemctl show -p MainPID --value "user@${uid}.service")"
    if [[ -z "$mgr_pid" || "$mgr_pid" == 0 ]]; then
        echo "user@${uid}.service is not running" >&2
        exit 1
    fi
    exec nsenter --target="$mgr_pid" --mount \
        "$0" "$uid" --inside
fi

# Everything below runs inside the user's mount namespace.

# --- Step 2: create + own the backing directory -----------------------------
install -d -o "$uid" -g "$primary_gid" -m 700 "$personal_compatdata"
# install -d only sets owner/mode at creation; enforce it on pre-existing dirs.
chown "$uid:$primary_gid" "$personal_compatdata"

# --- Step 3: prepare the shared mountpoint ---------------------------------
mkdir -p "$SHARED_STEAMAPPS"
# Remove a leftover symlink from the login-script approach, if present.
if [[ -L "$SHARED_COMPATDATA" ]]; then
    echo "Removing legacy compatdata symlink" >&2
    rm "$SHARED_COMPATDATA"
fi
# The stub directory is the mountpoint. It's root-owned and empty; it only
# matters in namespaces where nothing is mounted over it.
install -d "$SHARED_COMPATDATA"

# --- Step 4: idempotent bind mount -------------------------------------------
# findmnt with a bare path only succeeds if that exact path IS a mountpoint,
# so re-running (restarts, logins after logout) is a no-op rather than a
# nested-mount pile-up.
if findmnt -rn "$SHARED_COMPATDATA" >/dev/null; then
    exit 0
fi

mount --bind "$personal_compatdata" "$SHARED_COMPATDATA"
echo "compatdata for ${user_name} (uid ${uid}) mounted at ${SHARED_COMPATDATA}"
  1. Make it executable:
    sudo chmod +x /usr/local/libexec/steam-mount-compatdata

Create the service unit

Create /etc/systemd/system/[email protected]:

[Unit]
Description=Bind personal compatdata for user %i over shared Steam library
After=user@%i.service
Requires=user@%i.service
PartOf=user@%i.service

[Service]
Type=oneshot
ExecStart=/usr/local/libexec/steam-mount-compatdata %i
RemainAfterExit=yes

The unit is static: you don't enable or start it yourself. It gets pulled in automatically whenever a user's session manager (user@<uid>.service) starts, i.e. at login. The mount disappears again when the user logs out, because the namespace holding it is destroyed, so no unmount code is needed.

Pull the unit in at login

Create /etc/systemd/system/[email protected]/10-steam-compatdata.conf:

[Unit]
Wants=steam-compatdata@%i.service

Then reload systemd:
sudo systemctl daemon-reload

The template unit declares the After= ordering (it needs the user manager running to find its PID), while the user manager merely wants the mount unit. Declaring After= on both sides would create an ordering cycle. In practice the mount lands long before the desktop finishes loading.

Give each user manager its own mount namespace

On many distros, [email protected] already runs with slave mount propagation, but not all. This step is silently fatal to the whole approach if skipped: when the user managers share the host namespace, the first user's bind mount leaks into everyone's view and the second user's unit no-ops against it (green status, wrong result). Verify rather than assume:

sudo readlink /proc/1/ns/mnt
pid=$(systemctl show -p MainPID --value user@$(id -u).service)
sudo readlink /proc/${pid}/ns/mnt

If those two IDs match, create /etc/systemd/system/[email protected]/50-mount-namespace.conf:

[Service]
MountFlags=slave

And reload again:
sudo systemctl daemon-reload

slave (rather than fully private) propagation keeps the session receiving host mounts that appear later, so USB drives plugged in after login still show up, while preventing anything inside from leaking back out.

Every user who was already logged in needs a full logout for their manager to restart with the new flags; MountFlags is only read at start.

Clean up any leftovers

If the shared path was left mounted in the host namespace from earlier experiments, clear it from a root shell:

sudo umount /opt/SteamLibrary/steamapps/compatdata

If you previously used the login-script approach, remove the Login Script autostart entry. The helper removes the old symlink on first run anyway, but nothing should be fighting over the path.

Verify

  1. Log in as one user and run:
    findmnt /opt/SteamLibrary/steamapps/compatdata
    It should show that user's home directory as the mount source.
  2. Fast-user-switch to a second user and run the same command from their session; it should show their home directory.
  3. From a root shell (outside any user session), the path shows an empty root-owned directory. That's expected; see below.

With both users logged in simultaneously, a game running in the background user's session keeps its mount for as long as its session lives. Switching seats changes nothing for it.

Caveats

  • Non-session contexts see the stub. SSH logins, cron jobs, and anything running outside the user session resolve the path to the empty root-owned directory, not any user's compatdata. If you launch games from cron or over SSH, keep the symlink approach instead.
  • Namespace debugging takes adjustment. ls on the shared path shows different contents depending on which session you're in. findmnt (or findmnt -o SOURCE,TARGET) from the right session is the way to check what's really mounted where.

Add a new user to the library

The daemon handles switching automatically, but a new user needs permissions, a compatdata directory, and a Steam library location before their first login.

  1. Add the user to the steam group:
    sudo usermod -aG steam <username>
  2. Create their compatdata directory with correct ownership:
    sudo mkdir -p /home/<username>/.local/share/Steam/steamapps/compatdata
    sudo chown -R <username>:<username> /home/<username>/.local/share/Steam
  3. Have the new user log in and complete the same steps as an existing user: open Steam > Settings > Storage, add /opt/SteamLibrary as a library location, and make it the default.
  4. Add the Login Script.
  5. Switch seats to the new user (log out of the current user first). The daemon detects the new active seat and repoints the compatdata symlink into their home directory automatically. Verify with:
    readlink /opt/SteamLibrary/steamapps/compatdata

Which should now return /home/<username>/.local/share/Steam/steamapps/compatdata.

Steps 2 and 4 are optional on paper – the daemon creates the directory on first switch – but doing them upfront avoids any first-run race where Steam starts before the daemon finishes its switch.

Source

Has anyone found a straightforward way to share a Steam Library between two users?
I spent a few hours today trying to figure out why this wasn’t working for me. I setup a BTRFS volume, setup permissions, but when I switched over to the other user the game would fail with something like: Traceback (most recent call last): File ”/steam-library/steamapps/common/Proton - Experimental/proton”, line 2100, in <module> g_session.init_session(sys.argv[1] != “runinprefix”) File ”/steam-library/steamapps/common/Proton - Experimental/proton”, line 1979, in init_session g_com…