Skip to main content

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 sync is 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.
  • clean is paranoid. Refuses ~, /, symlinks, or non-directories.

Development

CI status

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

proton_home_sync-0.1.1.tar.gz (41.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

proton_home_sync-0.1.1-py3-none-any.whl (28.5 kB view details)

Uploaded Python 3

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

Hashes for proton_home_sync-0.1.1.tar.gz
Algorithm Hash digest
SHA256 5c12f05bb4a23d27a9b545e42fba4e039ca906ae152ebd7be333e9bd190f76e9
MD5 650f0e74179339579ee0b292d1910d0a
BLAKE2b-256 c32b7f3d05fe9808e6b7019595c49069067aa84232b3b264d0bfc7f0b46b526d

See more details on using hashes here.

File details

Details for the file proton_home_sync-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for proton_home_sync-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3735daf2db85e98561a000762c65710d32a3a0bf8d0b0c6eec6583279368eb32
MD5 e9d4f00b44d00d06e439895af7559f0d
BLAKE2b-256 d08548d865acd533062ddac141f6dff9d6bf78b972200bc3b7ed1b4b077dc99c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page