Disclaimer: This post has been partially written by Claude based on our development progress.
Our family Linux box is shared. Different users, different Steam accounts, one big library of games on the local NVMe. The naïve thing — put the library on a common mount, add everyone to a games group, set the setgid bit on /opt/steam/, done — gets you maybe a quarter of the way there. Then you try to launch a Proton game, and Wine faceplants with pfx is not owned by you.
This is the story of why that happens, and what actually works.
Why it doesn’t “just work”
Steam’s Linux client stores two different kinds of things inside a library folder:
- Game assets —
steamapps/common/<Game>/— executables, art, shaders. Read-only at runtime for the most part, and perfectly happy to be shared across users. - Proton prefixes —
steamapps/compatdata/<appid>/pfx/— a full Wine prefix per game, per account. Owned by whichever user launched the game first. Wine writes registry hives, shader caches, save games, config files into it constantly.
Share the library folder across users and the second category becomes a landmine. wineserver checks whether the prefix belongs to the calling user. It doesn’t. The whole process bails out before the game ever draws a frame.
The first thing everyone tries is a Steam compatibility tool wrapper: write a little shim that sets STEAM_COMPAT_DATA_PATH to a per-user directory before calling the real Proton. It kind of works until a per-game Proton override silently bypasses your wrapper and Steam writes half the prefix to the original shared path before the shim runs. You end up with a prefix split across two locations and a bug report you can’t reproduce.
A cleaner idea: don’t tell Steam anything. Handle the split below Steam, at the filesystem layer.
The fix: a per-user overlay on compatdata/
Every Linux kernel since about 2014 ships with overlayfs. It lets you stack a writable “upper” directory on top of a read-only “lower” one — reads fall through to the lower, writes land in the upper, and the union is what the process sees.
What we do:
- The real shared library lives at
/opt/steam/.common/is group-writable;compatdata/is the lower layer of the overlay (essentially read-only in practice). - Each user gets their own
upperandworkdirs under~/.local/share/steam-shared/. - When a user launches Steam, we wrap it in a private mount namespace that overlays
compatdata/with their personal upper layer.
From Wine’s point of view, /opt/steam/steamapps/compatdata/<appid>/pfx/ is entirely owned by the current user. Because writes go to the user’s home, they are. From the other user’s point of view in a concurrent session, the same path is also entirely owned by them. Both are true simultaneously, because they’re looking at two different union views of the same lower directory.
No compatibility tool. No STEAM_COMPAT_DATA_PATH. No per-game config. Just kernel plumbing.
The tool that makes the namespace: bubblewrap
You don’t need to write C to set up a mount namespace. bubblewrap (bwrap) is the sandboxing tool that lives underneath Flatpak — small, audited, unprivileged. It takes a list of mounts and a command, sets up a namespace that matches, and runs the command inside it.
The launcher for a single user boils down to one invocation:
exec bwrap \
--dev-bind / / \
--overlay-src /opt/steam/steamapps/compatdata \
--overlay $HOME/.local/share/steam-shared/upper \
$HOME/.local/share/steam-shared/work \
/opt/steam/steamapps/compatdata \
-- /usr/bin/steam "$@"
--dev-bind / / keeps everything else visible — X11 sockets, the rest of the filesystem, the user’s home. The overlay only rewrites compatdata/.
Permissions, quietly
Game assets in steamapps/common/ still need to be group-writable so Steam can install new games under either account. That part is old-school Unix:
- A
steamsharegroup that everyone with Steam access is a member of. setgidon every directory under/opt/steam/so new files inherit the group.- A default ACL (
setfacl -dR -m g:steamshare:rwx) so the inheritance is predictable across tools that don’t honor setgid perfectly.
One annoying footnote: Valve’s pressure-vessel (the Steam Runtime launcher) sometimes creates temporary directories under common/SteamLinuxRuntime_sniper/ with mode 2700 — owner only. A tiny systemd path unit watches common/ and shadercache/ for changes and runs a find … -exec chmod pass to re-open group access. Not elegant, but invisible during gameplay.
Concurrent sessions, really
The nice property of doing this in a mount namespace is that each logged-in user has their own. On our box, GDM lets two users be active at once — one on the primary display, one on a second seat. Both can be in Steam, both can download, both can run Proton games. Each session has its own overlay mount, its own Proton prefix views, its own shader cache. The only shared thing is the actual bytes on disk under common/, which is exactly what we wanted.
One thing that bit me
bwrap without the setuid bit — which is the default on most modern distros — creates an unprivileged user namespace. The kernel refuses to let an unprivileged user keep their supplementary groups across a namespace boundary. Everything except your primary group becomes nobody inside the sandbox.
This is fine for the overlay launcher itself, because the writable upper layer is in the user’s home and needs only the primary group. It became a very instructive kind of broken when I tried to nest a second bwrap around the launcher — specifically, a wrapper that bind-mounted a different /etc/resolv.conf to route Steam traffic through a local lancache. Inside that outer sandbox, steamshare had silently turned into nobody, and the overlay launcher’s pre-flight group check refused to run. When I bypassed the check, Steam itself then failed to write to /opt/steam/steamapps/common/ during a game install — the group bit that granted access was gone.
The lesson, now a big red box in the README: don’t nest steam-shared inside another bwrap. If you need a DNS detour, do it at the system level — a systemd-resolved drop-in, a local dnsmasq, whatever — not in a second namespace wrapped around the first.
The whole thing, on GitHub
Source, flake, activate script, uninstall script, and a much drier README live here:
git.felixfoertsch.de/felixfoertsch/steam-shared-library
sudo nix run .#activate on an Arch-ish box and you’re done. Add users to the steamshare group, log out and back in, launch Steam. No more prefix fights.
It’s a small amount of code for a problem that otherwise tends to consume a weekend. That, to me, is the nicest thing Linux does.