One script to mount Google Drive on-demand on any systemd Linux box: rclone mount (VFS, files
stream on access — no full local copy) wired to a systemd --user unit, with dependency install,
OAuth setup, and a verification suite that proves the mount actually works.
Built because every guide older than mid-2026 is now wrong: rclone's built-in shared client_id
is being retired during 2026 (Google began charging for API calls made through it), so bringing
your own OAuth client is mandatory rather than optional.
The key idea: you only do the Google Cloud console work once. One OAuth client can serve every machine you own — only the per-machine token differs. On a second machine you just point the script at the same credentials and complete a browser grant.
→ GOOGLE-OAUTH-SETUP.md — full walkthrough, ~10 minutes, done once ever.
You cannot skip this. rclone's built-in shared client_id is being retired during 2026, so a mount
without your own OAuth client will stop working. The same credentials then serve every machine you
own.
The three things that trip people up, all covered in detail in that guide:
- "Publish app" is greyed out and the banner won't say why. Hover the button — the tooltip reveals that a homepage URL and privacy-policy URL are required, which in turn makes Authorized domains mandatory.
- You must publish. An app left in "Testing" issues refresh tokens that expire after 7 days — your mount works fine, then dies a week later with no obvious cause.
- Don't upload an app logo. A logo forces a verification review; the URLs above do not.
Unverified is fine forever under Google's personal-use exemption (<100 users). You click through one "Google hasn't verified this app" screen during setup and never see it again.
# creds already in Bitwarden (default item name: google-oauth-rclone)
./deploy.sh
# explicit credentials, custom target
./deploy.sh --remote work --mountpoint ~/WorkDrive --cache-size 20G \
--client-id 1234-abc.apps.googleusercontent.com \
--client-secret-file ~/secret.txt
# headless box (no browser): authorize elsewhere, paste the token
./deploy.sh --headless
# safe inspection / maintenance
./deploy.sh --dry-run # print the plan, change nothing
./deploy.sh --verify-only # test an existing install
./deploy.sh --uninstall # full rollbackIdempotent — re-running repairs the unit and re-verifies without disturbing a working token.
Everything, from nothing:
| Dependency | How |
|---|---|
python3, curl, findmnt |
pacman / apt / dnf / zypper / apk, auto-detected |
fuse3 (fusermount3) |
same, with a fuse fallback for older Debian |
/dev/fuse |
checked, not installed — fails loudly in containers/WSL that lack it |
rclone >= 1.68 |
distro package if new enough, else rclone.org/install.sh |
The version gate matters.
--vfs-read-chunk-streamslanded in rclone v1.68, and Debian/Ubuntu stable ship much older builds (Debian 12 has 1.60). A distro package is not automatically good enough; the script detects this and pulls the upstream build instead. Notesort -Vis used for the comparison — a string compare would decide1.9 > 1.68.
bw (Bitwarden CLI) is optional; without it you're prompted, or pass --client-id /
--client-secret-file / RCLONE_GDRIVE_CLIENT_ID + RCLONE_GDRIVE_CLIENT_SECRET.
The client secret never touches argv, shell history, or stdout. It moves via a 600 temp file into
rclone.conf (also 600) through a Python configparser pass. --headless reads a token from
stdin rather than taking it as an argument.
Resolution order: --client-id/--client-secret-file → env vars → Bitwarden → interactive prompt.
Not just "it started" — the script proves the thing actually works:
- unit active + enabled
- mounted with fstype
fuse.rclone - read path: top-level directory count matches the API
- write round-trip — writes a file, polls until it appears server-side via the API, cleans up
- restart survival (install mode) — the failure first-boot success hides
- Google-native Docs/Sheets/Slides cannot be served over FUSE. Google reports size
-1until export, so they read as 0 bytes. Default here is--drive-export-formats link.html, which turns them into small redirect files that open in the browser. Override with--export-format docxif you'd rather have the (broken-on-mount, fine-for-rclone copy) Office names. - Deleting from the mount with a trash-aware file manager creates
.Trash-1000inside Drive, where it consumes quota and syncs to every device. Usermon the mount, or empty it withrclone purge <remote>:.Trash-1000. - First cold tree walk is slow (~30 s for ~11k entries), then ~1 ms warm.
ExecStartPostrunsrclone rc vfs/refresh recursive=trueto pay that cost once, at start, in the background.
Things a careful reader should check, called out rather than buried:
sudois used only to install packages and to runloginctl enable-linger. The mount itself runs entirely as your user — it installs asystemd --userunit, never a system unit. The script refuses to run as root.curl … | sudo bashfallback. Only reached when there is no usable distro package (or the packaged rclone is older than 1.68). It fetcheshttps://rclone.org/install.sh, rclone's own documented installer. If you'd rather not, install rclone yourself first and the script skips it.--rc-no-authis bound to127.0.0.1only. It exists so the unit can callvfs/refreshto warm the cache and so you can usevfs/forget/vfs/stats. Anyone with a shell on the box could drive it — on a shared machine, drop the three--rc*flags from the generated unit, or set--rc-user/--rc-pass.- The client secret never touches argv, shell history, or stdout. It moves through a
600temp file into a600rclone.confvia a Pythonconfigparserpass.--headlessreads the token from stdin. Nothing is echoed. rclone.confholds a live refresh token. Treat it like a private key; it stays600.
MIT — see LICENSE.