Merge every Steam Proton prefix's home directory into one shared home and replace each prefix with a symlink, so backup tools see a single real directory of game saves
Project description
proton-home-sync
Merge every Steam Proton prefix's home directory into a single shared home, then replace each prefix's home with a symlink to it. Backup tools that don't cooperate well with symlinks (most of them) can target one real directory and get every game's saves.
~/.steam/steam/steamapps/compatdata/
├── 11111/pfx/drive_c/users/steamuser ──► ~/proton-shared-home/steamuser/
├── 22222/pfx/drive_c/users/steamuser ──► ~/proton-shared-home/steamuser/
└── 44444/pfx/drive_c/users/steamuser ──► ~/proton-shared-home/steamuser/
└── (real directory — the only backup target)
The arrows go prefix → shared home. The shared home is one real directory on disk. Once a prefix is linked, it stays linked — there is no deletion tracking to worry about. When Steam creates a brand-new prefix, the daemon picks it up, merges its initial Wine state into the shared home, and turns the prefix's home into a symlink.
Why this shape
- Most backup tools either de-duplicate symlinks to nothing or chase pointers back into your Steam library. With a real shared home, the tool walks one tree and sees real files.
- No deletion tracking. New prefixes just add their initial files to the shared home; after the symlink flip they write straight into it.
- One backup target:
~/proton-shared-home/steamuser/(or wherever you point--central-home).
The cost is that all games now share one Windows user namespace, so two
games that use the same path under %USERPROFILE% would collide. In
practice most games namespace themselves via %APPDATA%\<Studio>\<Game>\,
Documents/My Games/<Game>/, or Saved Games/<Game>/ and don't trip
on each other.
Install
pipx install . # from a cloned checkout
# or directly from GitHub:
pipx install git+https://github.com/vality/proton-home-sync
# or
pip install --user .
# plus, for daemon mode:
pip install 'proton-home-sync[daemon]'
Requires Python ≥ 3.9 and the vdf package (pulled in automatically).
Usage
# First run — merges every existing prefix into the shared home and links them.
proton-home-sync sync
# Use a custom shared home location.
proton-home-sync sync --central-home ~/backups/proton-shared-home
# See what would happen.
proton-home-sync sync --dry-run
# Show every prefix and its link state (✓ linked, · real dir, ↗ manual symlink).
proton-home-sync list
# Health summary — central home size + per-prefix link state.
proton-home-sync status
# Tear down the shared home (does NOT unlink the prefixes).
proton-home-sync clean
# Long-running watcher — auto-migrates new prefixes as games install.
proton-home-sync daemon
Conflict policy
When a prefix's home contains a file that's already in the shared home,
the default is --on-conflict skip — the shared copy wins, the
prefix's copy is left behind (and effectively discarded because the
prefix's home is then replaced with a symlink). Pass
--on-conflict overwrite to make the prefix's copy win instead; this
is useful when re-migrating after a manual rollback.
Hooking up a backup tool
# restic — encrypted, incremental
restic backup ~/proton-shared-home/steamuser
# borg — encrypted, deduplicated
borg create ~/backup::saves-$(date +%F) ~/proton-shared-home/steamuser
# rclone — to a cloud target
rclone sync ~/proton-shared-home/steamuser remote:proton-saves
# rsync — to a USB drive
rsync -a --delete ~/proton-shared-home/steamuser/ /mnt/usb/saves/
That's the entire backup story — one real directory, one command.
Daemon mode
proton-home-sync daemon runs forever, watching each Steam library's
compatdata/ for new prefixes. When one shows up (or Steam re-creates
an existing one), the daemon migrates it.
pip install 'proton-home-sync[daemon]'
proton-home-sync daemon
Useful flags:
# React faster to filesystem events.
proton-home-sync daemon --debounce-ms 500
# Less frequent full rescans (default every 5 minutes).
proton-home-sync daemon --rescan-seconds 900
The daemon logs to stderr. Run it under your service supervisor of choice.
systemd --user unit
# ~/.config/systemd/user/proton-home-sync.service
[Unit]
Description=Merge new Proton prefix home directories into the shared home
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/proton-home-sync daemon
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
systemctl --user enable --now proton-home-sync.service
Configuration
Defaults live in proton_home_sync/config.py:
| Setting | Default | Override |
|---|---|---|
| Steam roots | ~/.steam/steam, Flatpak variants |
--steam-root |
| Shared home | ~/proton-shared-home/steamuser |
--central-home |
| Wine user | steamuser |
--username |
| Conflict policy | skip (shared home wins) |
--on-conflict overwrite |
Safety properties
- Idempotent. Re-running
syncis a no-op once everything is linked. - Atomic flip. When migrating, the prefix's real directory is renamed aside first, the symlink is created, and only then is the backup deleted. If anything fails midway, the prefix is restored.
- Manual symlinks are respected. A prefix whose home already symlinks somewhere else is left alone with a warning.
cleanis paranoid. Refuses~,/, symlinks, or non-directories.
Development
python -m venv .venv
source .venv/bin/activate
pip install -e '.[daemon,test]'
pytest
The test suite builds a fake Steam install on disk (see
tests/conftest.py) — two libraries, three valid prefixes plus one
broken one — and exercises every code path.
Project layout
proton-home-sync/
├── pyproject.toml
├── README.md
├── proton_home_sync/
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli.py # argparse + subcommand dispatch
│ ├── config.py # default paths and tunables
│ ├── daemon.py # long-running watcher (uses watchfiles)
│ ├── merge.py # merge-then-symlink migration logic
│ ├── models.py # ProtonPrefix, MigrationResult, MigrationReport
│ ├── prefix.py # scan libraries for prefixes
│ ├── steam.py # locate Steam root + libraryfolders.vdf parser
│ ├── utils.py # human_path, helpers
│ └── linker.py # deprecation shim → re-exports merge.py
└── tests/
├── conftest.py # fake_steam fixture (two libraries, 3 valid prefixes)
├── test_steam.py
├── test_prefix.py
├── test_merge.py
├── test_linker.py # deprecation marker
├── test_cli.py
└── test_daemon.py
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file proton_home_sync-0.1.1.tar.gz.
File metadata
- Download URL: proton_home_sync-0.1.1.tar.gz
- Upload date:
- Size: 41.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c12f05bb4a23d27a9b545e42fba4e039ca906ae152ebd7be333e9bd190f76e9
|
|
| MD5 |
650f0e74179339579ee0b292d1910d0a
|
|
| BLAKE2b-256 |
c32b7f3d05fe9808e6b7019595c49069067aa84232b3b264d0bfc7f0b46b526d
|
File details
Details for the file proton_home_sync-0.1.1-py3-none-any.whl.
File metadata
- Download URL: proton_home_sync-0.1.1-py3-none-any.whl
- Upload date:
- Size: 28.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3735daf2db85e98561a000762c65710d32a3a0bf8d0b0c6eec6583279368eb32
|
|
| MD5 |
e9d4f00b44d00d06e439895af7559f0d
|
|
| BLAKE2b-256 |
d08548d865acd533062ddac141f6dff9d6bf78b972200bc3b7ed1b4b077dc99c
|