Disclaimer: The investigation, solution and this post was done in a collaboration with Claude.
We have a family gaming PC (Bazzite, but this applies to any Linux) with several local user accounts, each with their own Steam account. We wanted every game installed once, playable by everyone — without breaking Proton. Getting there took some debugging, so here’s the full working recipe including the pitfalls we hit.
The short version: game files can be shared through a common Steam library folder with group permissions + ACLs. Proton prefixes (compatdata) can NOT be shared — Wine refuses to use a prefix it doesn’t own — so we point compatdata through a symlink into the home directory of whoever is currently at the keyboard, switched automatically by a tiny systemd user service + timer.
The Shorter version:
Use a non official Steam Proton version. Apply Step 1, Step 2b, Step 2c. Reboot and play games.
Throughout this post the shared library is /var/home/steam-share (Bazzite/ostree note: /home is a symlink to /var/home; on regular distros just use /home/steam-share) and the group is steam-users. Adjust to taste.
Step 1 — Create the group and the shared library folder
sudo groupadd steam-users
sudo usermod -aG steam-users alice # repeat for each user
# each user must log out and back in for the group to take effect!
sudo mkdir /var/home/steam-share
sudo chown root:steam-users /var/home/steam-share
sudo chmod 2770 /var/home/steam-share
sudo setfacl -m g:steam-users:rwX -d -m g:steam-users:rwX /var/home/steam-share
What this does:
chmod 2770 sets the setgid bit on the directory, so everything created inside inherits the steam-users group.
- The
setfacl line adds a default ACL: every file/dir any user creates in there is automatically group-readable/writable (X = execute only where it makes sense). This is what makes “user A installs, user B updates” work without anyone ever running chmod -R again.
Step 2 — Add it as a library in each user’s Steam
In every user’s Steam client: Settings → Storage → (dropdown) → Add Drive → pick /var/home/steam-share. Steam creates the steamapps structure on first use; the setgid bit + default ACL take care of permissions. Games installed there by one user show up as installed for everyone (each user still needs to own the game on their Steam account, or use Family Sharing).
Pitfall 1 — never chmod -R g+s the library
If you try to fix permissions with a recursive setgid (chmod -R 2770 or chmod -R g+s), the bit also lands on regular files, including the Steam Linux Runtime’s executables. Executing a setgid binary makes the kernel flag the process as “security elevated”, and Valve’s runtime then refuses to start. Every Proton game dies instantly with this in ~/.local/share/Steam/logs/console-linux.txt:
steam-runtime-tools-CRITICAL **: _srt_constructor: assertion '_srt_check_not_setuid ()' failed
pressure-vessel-wrap[1234]: Internal error: _srt_find_myself: assertion '_srt_check_not_setuid ()' failed
Setgid belongs on directories only. If you already made this mistake, this fixes it (keeps the bit on dirs):
sudo find /var/home/steam-share -type f -perm -2000 -exec chmod g-s {} +
Pitfall 2 — Proton prefixes cannot be shared
The first user to play a Proton game works fine. The second user gets an instant crash with this in the log:
wine: '/var/home/steam-share/steamapps/compatdata/1222680/pfx' is not owned by you
Wine hard-requires the prefix directory to be owned by the launching user — group permissions and ACLs cannot bypass an ownership check. So each user needs their own compatdata. Rather than setting STEAM_COMPAT_DATA_PATH=... %command% launch options on every game for every user, we made compatdata in the shared library a symlink into the active user’s home, switched automatically at login and on user switch.
2a. One-time migration (as root) - OPTIONAL - You can also just use a clean prefix directory.
Move any existing prefixes to their owners and create the initial link:
cd /var/home/steam-share/steamapps
for d in compatdata/*/; do
owner=$(stat -c %U "$d")
dest="/var/home/$owner/.local/share/Steam/steamapps/compatdata"
sudo -u "$owner" mkdir -p "$dest"
sudo mv "$d" "$dest/"
done
sudo rmdir compatdata
sudo ln -s "$HOME/.local/share/Steam/steamapps/compatdata" compatdata
2b. The switcher script
/usr/local/bin/steam-compatdata-switch (root-owned, chmod 755):
#!/usr/bin/env bash
# Point the shared library's compatdata at this user - but only if this
# user currently owns the active (foreground) session.
state=$(loginctl show-user "$(id -u)" --property=State --value 2>/dev/null)
[ "$state" = "active" ] || exit 0
target="$HOME/.local/share/Steam/steamapps/compatdata"
link=/var/home/steam-share/steamapps/compatdata
mkdir -p "$target"
[ "$(readlink "$link")" = "$target" ] || ln -sfn "$target" "$link"
The loginctl guard matters: without it, a user logging in briefly steals the link and it never flips back to the person still playing. With the guard + the timer below, the link always follows whoever is actually at the keyboard, within 30 seconds.
2c. Run it at login and every 30s — for all users, with two files
/etc/systemd/user/steam-compatdata-switch.service:
[Unit]
Description=Point shared Steam library compatdata at this user
[Service]
Type=oneshot
ExecStart=/usr/local/bin/steam-compatdata-switch
[Install]
WantedBy=default.target
/etc/systemd/user/steam-compatdata-switch.timer:
[Unit]
Description=Keep shared Steam compatdata pointing at the active user
[Timer]
OnStartupSec=15
OnUnitActiveSec=30
AccuracySec=5
[Install]
WantedBy=timers.target
Enable both globally (that’s the trick — it applies to every current and future user, no per-user setup):
sudo systemctl --global enable steam-compatdata-switch.service steam-compatdata-switch.timer
Because these are systemd user units they run in both desktop and Game Mode (gamescope) sessions. Users not in steam-users just fail to move the link, harmlessly.
OPTIONAL Pitfall 3 — shared Proton needs a permission fixup after every update.
Proton from Steam (“Proton - Experimental” etc.) also installs into a Steam library. If it lives in the shared one, there’s a subtle trap: after each Proton update, Proton’s launcher runs a one-time chmod pass over its own files (steampipe_fixups.json restore — Steam’s download system doesn’t preserve Unix permissions). chmod only works for the file owner, so the first person to launch after an update must be the user whose Steam downloaded it. Everyone else crashes with a Python traceback ending like:
PermissionError: [Errno 1] Operation not permitted:
'.../Proton - Experimental/files/lib/wine/dxvk/i386-windows/openvr_api_dxvk.dll'
Once the fixup has run once, it’s skipped for everybody (there’s a marker file), so day-to-day everything works. Your options, simplest first:
- Live with it: when it happens, the user who owns the Proton files launches any Proton game once — fixed for everyone.
- Keep Proton per-user: let each user keep Proton/compat tools in their home library (or use GE-Proton in
~/.local/share/Steam/compatibilitytools.d/, which is per-user by nature) and share only the games. Costs a couple of GB per user, avoids the problem entirely.
- Automate with a root helper that watches
steampipe_fixups.json and applies the fixup as root, so the first launch after an update works for any user. Full implementation below.
3a. The fixup helper
One security warning first: do not execute Proton’s bundled Python as root — it sits in a group-writable directory, so anyone in steam-users could swap it out and get root. The helper below reimplements the fixup in a root-owned file, treats the manifest as pure data, and refuses paths that escape the Proton directory.
/usr/local/bin/steam-proton-fixups (root-owned, chmod 755):
#!/usr/bin/env python3
"""Apply Proton's steampipe permission fixups as root for shared installs."""
import json, os, stat, sys
COMMON = "/var/home/steam-share/steamapps/common"
def fixup(tooldir):
manifest = os.path.join(tooldir, "steampipe_fixups.json")
marker = os.path.join(tooldir, "files", "steampipe_fixups_mtime")
if not os.path.isfile(manifest):
return
want = str(os.path.getmtime(manifest))
try:
with open(marker) as f:
if f.readline().strip() == want:
return # already done for this Proton build
except OSError:
pass
with open(manifest) as f:
loaded = json.load(f)
root = os.path.realpath(tooldir)
for entry in loaded.get("empty_dirs", []):
p = os.path.join(tooldir, entry)
if not os.path.realpath(p).startswith(root + os.sep):
print(f"skipping suspicious path {entry!r}", file=sys.stderr)
continue
os.makedirs(p, exist_ok=True)
for entry in loaded.get("no_write_paths", []):
p = os.path.join(tooldir, entry)
if not os.path.realpath(p).startswith(root + os.sep):
print(f"skipping suspicious path {entry!r}", file=sys.stderr)
continue
try:
st = os.lstat(p)
except FileNotFoundError:
continue
if not stat.S_ISLNK(st.st_mode):
os.chmod(p, st.st_mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH))
os.makedirs(os.path.dirname(marker), exist_ok=True)
with open(marker, "w") as f:
f.write(want + "\n")
print(f"applied fixups for {tooldir}")
if os.path.isdir(COMMON):
for name in sorted(os.listdir(COMMON)):
fixup(os.path.join(COMMON, name))
It mirrors exactly what Proton’s do_steampipe_fixups() does — recreate empty_dirs, strip write bits from no_write_paths, then write the marker Proton compares against (str(os.path.getmtime(...)) of the manifest) — but as root, so ownership doesn’t matter. It’s idempotent and skips tools whose marker is already current, so it’s safe to run any time.
3b. Trigger it automatically
/etc/systemd/system/steam-proton-fixups.service:
[Unit]
Description=Apply Proton steampipe fixups on the shared Steam library
[Service]
Type=oneshot
ExecStart=/usr/local/bin/steam-proton-fixups
/etc/systemd/system/steam-proton-fixups.path — fires the service the moment a Proton update lands (add a PathChanged= line per shared Proton version you keep):
[Unit]
Description=Watch shared Proton installs for updates
[Path]
PathChanged=/var/home/steam-share/steamapps/common/Proton - Experimental/steampipe_fixups.json
PathModified=/var/home/steam-share/steamapps/common
[Install]
WantedBy=multi-user.target
/etc/systemd/system/steam-proton-fixups.timer — daily catch-all in case the path watch misses something:
[Unit]
Description=Daily catch-all for Proton steampipe fixups
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
Enable everything and run it once immediately:
sudo systemctl daemon-reload
sudo systemctl enable --now steam-proton-fixups.path steam-proton-fixups.timer
sudo /usr/local/bin/steam-proton-fixups
Notes & limitations
- EA / Ubisoft games: launchers like the EA App install inside the prefix, so each user gets their own copy and logs in once on first launch. That’s per-user by design and actually what you want. (Tip: EA App’s first-run VC++ redistributable install can hang under Wine on the old-version uninstall step — if “Launching…” sits there for 5+ minutes with the
VC_redist.x86.exe -uninstall process at 0% CPU, killing that process lets setup continue.)
- One player at a time: the symlink has a single target, so games launched by a backgrounded user while someone else is active will fail until they’re active again. Running games mostly survive a flip, but don’t rely on it. If you genuinely need simultaneous gaming sessions, look at
pam_namespace polyinstantiation (per-session bind mounts over compatdata) instead of a symlink.
- Saves: most saves live in the prefix (= per user, good) or in Steam Cloud. Nothing to do.
- Shader caches (
steamapps/shadercache) share fine — no ownership checks there, and sharing saves compile time.
- Where to look when something breaks:
~/.local/share/Steam/logs/console-linux.txt (launch errors, the errors quoted above all show up here), gameprocess_log.txt (did the process die instantly?), compat_log.txt (which Proton was picked).
Happy shared gaming! Corrections welcome.