Nicolas Hognon_

Two-way Google Drive sync on Linux with rclone bisync

How I keep a local folder and my Google Drive in sync both ways on Linux with rclone bisync and a systemd timer, and what three incidents taught me about it.

Christmas 2025, I finally decided to move my personal PC to Linux. It is a desktop from 2015 or 2016 that was still running Windows 10: the perfect candidate. Looking back, I wonder why I did not do it sooner. I hesitated with a classic Debian, and went with Linux Mint Debian Edition in the end.

I found my way around quickly. It is easy now that almost everything happens in a web browser. The rest is mostly VS Code and Unity.

That left Google Drive. (Yes, I use Google products. I still have to migrate away from them, but I have not done it yet.)

Google has never shipped a Drive client for Linux. I wanted the same thing I had on Windows: a plain folder on my desktop PC that mirrors my Drive both ways, works offline, and needs no attention. This post describes my setup: rclone bisync, a systemd user timer, and a desktop notification when something breaks.

A note: I started writing scripts for all this right after installing Linux. It worked… almost. I let it drag on for a while, and finally picked it all up again once I had an imaginary friend. I am not entirely at peace with myself on that one, full-on cognitive dissonance. But since it is done, I might as well share what came out of it.

Why bisync

There were three options on the table:

  • rclone mount shows the Drive as a filesystem, but files only exist locally while cached. Without a network, there is nothing to open.
  • Insync and similar paid clients do the job, but it is one more subscription for something a free tool can do.
  • rclone bisync keeps a real local copy and reconciles both sides on each run. Changes made offline go up on the next run.

I went with bisync. The price is that it is not live: it runs on a schedule, and between two runs the two sides can diverge. For documents edited on one machine at a time, that is fine.

The pieces

FileRole
~/.config/rclone/rclone.confThe Drive remote, created with rclone config
~/Scripts/google.drive.sync.shRuns one bisync
~/Scripts/bisync-filters.txtWhat not to sync
~/.config/systemd/user/google.drive.sync.timerStarts the service every 15 minutes
~/.config/systemd/user/google.drive.sync.serviceRuns the script, calls the failure service when it fails
~/.config/systemd/user/google.drive.sync.failure.serviceRuns the notify script
~/Scripts/google.drive.sync.notify.shSends one desktop notification per outage

The scripts below are mine, anonymized and occasionally trimmed.

The rclone remote

Create a gdrive remote with rclone config. One piece of advice from the rclone docs is worth following from day one: make your own OAuth client ID instead of using rclone's shared one, which every rclone user hits at the same time. Even so, the first full sync is the heavy one: my first runs hit 403 rate-limit errors, and bisync lost its state and needed a --resync. Expect the initial run to be slow.

The sync script

~/Scripts/google.drive.sync.sh

#!/bin/sh
# Two-way sync Google Drive <-> ~/GoogleDrive via rclone bisync.
# Run by the systemd --user timer google.drive.sync.timer.
# The very first run (or any run after editing the filter file) must be done by
# hand with --resync before this script (without --resync) can work.
# On failure, systemd starts google.drive.sync.failure.service (desktop alert).

REMOTE="gdrive:/"
LOCAL="$HOME/GoogleDrive"
LOG="$HOME/.cache/google-drive-sync.log"
FILTERS="$HOME/Scripts/bisync-filters.txt"
FAILED_FLAG="$HOME/.cache/google-drive-sync.failed"
LOG_MAX_BYTES=10485760   # 10 MB
LOCK_BUSY=75             # exit code used by flock when a sync is already running

# Keep the log bounded: once it passes the limit, keep only its tail.
if [ -f "$LOG" ] && [ "$(stat -c %s "$LOG")" -gt "$LOG_MAX_BYTES" ]; then
  tail -n 5000 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
fi

/usr/bin/flock -n -E "$LOCK_BUSY" /tmp/google-drive-sync.lock \
  /usr/bin/rclone bisync "$REMOTE" "$LOCAL" \
    --drive-skip-gdocs \
    --filter-from "$FILTERS" \
    --max-delete 25 \
    --log-file="$LOG" -v
rc=$?

# A previous sync is still running: not an error, skip this round.
[ "$rc" -eq "$LOCK_BUSY" ] && exit 0

# Back to normal after an outage: clear the flag and say so.
if [ "$rc" -eq 0 ] && [ -f "$FAILED_FLAG" ]; then
  rm -f "$FAILED_FLAG"
  notify-send -a "Google Drive sync" -i emblem-default \
    "Google Drive sync is back" "rclone bisync succeeded again."
fi

exit "$rc"

Two flags deserve a word:

  • --drive-skip-gdocs: native Google Docs and Sheets are not files. Exporting them on every run is slow and pointless, so they stay on the Drive.
  • --max-delete 25: bisync aborts if a run would delete more than 25 % of the files on either side. This is the safety net, and it fired for real (see below).

The filters

Two kinds of folders do not belong in the sync: folders other people shared with me, and, more subtly, files that someone else uploaded into a shared folder of mine. I cannot delete either on the Drive side (see the shared folders below).

~/Scripts/bisync-filters.txt

# folders shared with me, not mine
- Some Shared Folder/**
- Another Shared Folder/**
# files uploaded by someone else into my folder
- My Shared Folder/Their Uploads/**

The systemd units

~/.config/systemd/user/google.drive.sync.timer

[Unit]
Description=Google Drive two-way sync, every 15 minutes

[Timer]
OnCalendar=*:0/15
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true means a run missed while the PC was off happens as soon as it is back on.

~/.config/systemd/user/google.drive.sync.service

[Unit]
Description=Google Drive two-way sync
OnFailure=google.drive.sync.failure.service

[Service]
Type=oneshot
ExecStart=%h/Scripts/google.drive.sync.sh

%h is systemd's specifier for the user's home directory. $HOME would not work here: systemd does not expand variables in the program path of ExecStart.

~/.config/systemd/user/google.drive.sync.failure.service

[Unit]
Description=Notify that the Google Drive sync failed

[Service]
Type=oneshot
ExecStart=%h/Scripts/google.drive.sync.notify.sh

The notify script sends one critical notification per outage, not one every 15 minutes, with the last error lines of the log so the notification already says what went wrong:

~/Scripts/google.drive.sync.notify.sh

#!/bin/sh
# Desktop alert for a failed Google Drive sync.
# Run by google.drive.sync.failure.service (OnFailure= of google.drive.sync.service).
# Alerts once per outage: google.drive.sync.sh removes the flag on the next success.

LOG="$HOME/.cache/google-drive-sync.log"
FAILED_FLAG="$HOME/.cache/google-drive-sync.failed"

[ -f "$FAILED_FLAG" ] && exit 0
date -Iseconds > "$FAILED_FLAG"

# Last error lines, without the timestamp prefix.
reason=$(grep -E "ERROR|NOTICE: Bisync" "$LOG" 2>/dev/null | tail -n 3 | cut -c 21-220)

notify-send -a "Google Drive sync" -u critical -i dialog-error \
  "Google Drive sync failed" "${reason:-No details, see $LOG}

Log: $LOG"

The first run

Before enabling the timer, run the first sync by hand, with the script's flags plus --resync. bisync needs that first run to record the state of both sides, and refuses to run without it. Start with a dry run:

$ rclone bisync gdrive:/ ~/GoogleDrive \
    --drive-skip-gdocs \
    --filter-from ~/Scripts/bisync-filters.txt \
    --max-delete 25 \
    --resync --dry-run -v

If the list looks right, run it for real, without --dry-run:

$ rclone bisync gdrive:/ ~/GoogleDrive \
    --drive-skip-gdocs \
    --filter-from ~/Scripts/bisync-filters.txt \
    --max-delete 25 \
    --resync -v

Then enable the timer:

$ systemctl --user daemon-reload
$ systemctl --user enable --now google.drive.sync.timer
$ systemctl --user list-timers

What went wrong

The setup above is the result of three incidents in the first two months.

The silent six weeks

In September a big folder was renamed on the Drive, about 650 files. bisync does not see a rename. It compares file listings, so it saw 624 deletions on one side and about 650 new files on the other. That crossed the --max-delete threshold, and bisync refused to run. It did exactly what it should: nothing was lost.

Except nobody was told. The service failed every 15 minutes for eleven days, quietly, in a log nobody reads. Then the PC was shut down in the middle of a run, and bisync lost its state: every run after that failed with cannot find prior Path1 or Path2 listings. I noticed six weeks after the rename.

That is why the failure service exists. A sync that can stop without telling anyone is worse than no sync, because you keep trusting it.

The disk

The night I fixed it, the sync failed again: the disk was full, and rclone could not even save its config. My home directory sits on the system partition, which fills up faster than you think. Freeing some space fixed it, and disk space is now on my list of things the sync depends on.

The shared folders

Some files in my Drive are not mine: folders other people shared with me, and files that someone else uploaded into a folder I share. When bisync had to delete one of them on the Drive side, the Drive refused with insufficientFilePermissions, and that one failed deletion was enough to fail the whole run.

Those files cannot be managed from my side, so they are out of the sync: that is what the filter file is for.

The repair procedure

When the log says Must run --resync to recover:

  1. Stop the timer: systemctl --user stop google.drive.sync.timer.
  2. Find out why it broke first: grep -E "ERROR|Safety abort" ~/.cache/google-drive-sync.log | tail.
  3. If a folder was renamed on the Drive, rename or move aside the old local folder before resyncing. Otherwise --resync merges both sides and uploads the old folder again as a duplicate.
  4. Run the same two commands as for the first sync: the dry run, read it, then the real run.
  5. Start the timer again: systemctl --user start google.drive.sync.timer. The next successful run clears the failure flag and tells you so.

And to check that everything is fine:

$ grep "Bisync successful" ~/.cache/google-drive-sync.log | tail -1

Takeaways

  • Set a deletion threshold. --max-delete turned a rename into a blocked sync instead of a mass deletion.
  • Make failures loud. A safety stop nobody hears about just moves the problem.
  • Renames are deletions as far as bisync is concerned. Renaming a big folder, on either side, trips --max-delete: plan a resync.
  • Diagnose before --resync. It fixes the state, not the cause, and it can duplicate a renamed folder.

I left two things for later: a push notification to my phone, and an alert when the disk gets low. So far the desktop notification has been enough.