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.
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.
- Find your root device:
findmnt -n -o SOURCE / - Create mount location:
sudo mkdir -p /mnt/btrfs-root - Mount the top-level subvolume:
sudo mount -o subvolid=5 <device> /mnt/btrfs-root - Create a BTRFS subvolume for the library:
sudo btrfs subvolume create /mnt/btrfs-root/@steam - Find your filesystem UUID:
sudo btrfs filesystem show / - Create mount location:
sudo mkdir -p /opt/SteamLibrary - Edit /etc/fstab, add the following entry:
UUID=<UUID> /opt/SteamLibrary btrfs subvol=/@steam,defaults,noatime,compress=zstd:1,nofail 0 0 - Check your configuration for errors:
findmnt --verify - Reload systemd units:
sudo systemctl daemon-reload - Mount the library:
sudo mount /opt/SteamLibrary - Create a group that will be used to access the library:
sudo groupadd steam - Add your user to the group:
sudo usermod -aG steam $USER - 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/
- Reboot and log back in.
Add the library to Steam
- Open Steam and click Steam > Settings > Storage,
- Click the drop down menu next to the selected storage device, click Add Drive
- Click the drop down menu on the pop-up,
select Let me choose another location, click Add - Browse to /opt/SteamLibrary, click Open.
- Click the drop down menu next to the selected storage device,
click the newly added device. - Click the overflow menu, click Make Default.
- Edit /SteamLibrary/libraryfolder.vdf,
update"label" ""to"label" "/opt/SteamLibrary"
Create a login script to update CompatData
- Create
/opt/SteamLibrary/steam-link.shwith 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."
- Make it executable:
chmod +x /opt/SteamLibrary/steam-link.sh - Open System Settings > Autostart.
- Click Add New, click Login Script.
- Browse to /opt/SteamLibrary/steam-link.sh, click Open.
Optional: per-user bind mounts instead of the symlink
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
- 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}"
- 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
- Log in as one user and run:
findmnt /opt/SteamLibrary/steamapps/compatdata
It should show that user's home directory as the mount source. - Fast-user-switch to a second user and run the same command from their session; it should show their home directory.
- 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.
lson the shared path shows different contents depending on which session you're in.findmnt(orfindmnt -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.
- Add the user to the
steamgroup:sudo usermod -aG steam <username> - 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 - 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.
- Add the Login Script.
- Switch seats to the new user (log out of the current user first). The daemon detects the new active seat and repoints the
compatdatasymlink 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

