SlopScore
00 crowd

TimeShift-BTRFS-Sync

A python app to transfer Timeshift snapshots to a remote target, and cleanup in snapshots on the remote end.
Open repo on GitHubgithub.com/D4rk-5ky/TimeShift-BTRFS-Sync
Python · ★ 1 · 0 forks · MIT · paperwork by the Cap'mmostly ai (inferred)light human (inferred)works-on-my-machine (inferred)other
listed 1 hour ago by D4rk-5ky · last checked 1 hour ago
The owner didn't write this. This repo never submitted itself. The Cap'm found it on a truffle trawl and wrote its paperwork from what GitHub already shows. Picked by hand by the Cap'm on 2026-10-12: A python app to transfer Timeshift snapshots to a remote target, and cleanup in snapshots on the remote end.; its own README says "⚠️ AI-assisted / vibe-coded experimental software". 1 stars; MIT license. The owner did not submit this. Votes count; awards don't until the owner claims it.

I'm not calling your project slop! Geeze, it's a joke... Do you own this repo?

Log in with GitHub as D4rk-5ky. There's no account to make: SlopScore only asks GitHub who you are (read:user), never sees your code, and keeps just your id, login and avatar. Then you can:

  • Keep it, on your terms. Commit your own slopscore.md (spec) and press Refresh. Your paperwork replaces the Cap'm's, and you can submit it for Slop of the Day.
  • Take it down. One click on Remove. It stays gone; the trawl never brings it back.

Log in with GitHub

Can't log in as the owner? Request a takedown. No login needed, and a trawled listing comes down right away.

GitHub says
A python app to transfer Timeshift snapshots to a remote target, and cleanup in snapshots on the remote end.
created
2026-06-23 · pushed 1 hour ago · 93 commits · 2 contributors
release
v0,1,84 · 2026-09-11
languages
Python 100%Shell 0%
paperwork
licensereadme 42% health
dependencies
no dependency graph (no manifest, or disabled) · OSV.dev, checked 1 hour ago

Disclosures, inferred by the Cap'm

slopbucket
vibe-coded
category
other
ai_generated
mostly
human_touch
light
status
works-on-my-machine
language (detected)
pythonshell
license (detected)
mit

The Cap'm's log

The Cap'm wrote this paperwork, not the owner. This repo never submitted itself to SlopScore. The Cap'm picked it by hand: A python app to transfer Timeshift snapshots to a remote target, and cleanup in snapshots on the remote end.; its own README says "⚠️ AI-assisted / vibe-coded experimental software". It carries the MIT license. The disclosures above are his best guess from what GitHub shows.

Is this yours? Commit a real slopscore.md and press Refresh to replace this, or remove the listing in one click. There's no account to make: you log in with GitHub.

README — the repo's own words, folded up so the grading fits on one screen

timeshift-btrfs-sync

timeshift-btrfs-sync backs up Timeshift Btrfs snapshots and can restore either one backup or a complete UUID-validated backup chain into the configured Timeshift repository. It can pull from or restore to another machine over SSH, or operate locally on the same machine. All restore directions use the same Btrfs, metadata, validation, and safety logic; only the selected backup and Timeshift endpoints change.

⚠️ AI-assisted / vibe-coded experimental software. Use at your own risk.

Disclaimer

This project is AI-assisted / vibe-coded software created as a hobby project. It has not been professionally audited and may contain bugs, unsafe behavior, data-loss issues, security problems, or incorrect assumptions.

You are responsible for reviewing the code, testing it in a safe environment, making backups, and understanding what it does before using it on real data. The author is not responsible for damage, data loss, broken systems, security issues, or other problems caused by using this software.

Data Loss Warning

This application can perform destructive operations, including deleting Btrfs subvolumes, snapshots, and backup data. Always test with dry-runs first, check the generated plans, and keep a separate working backup.

License

MIT License. See LICENSE.

What it does

source.mode is used only by normal sync/source commands: ssh reads Timeshift snapshots through SSH and local reads them on the current machine. Restore direction is selected separately by [restore] mode: local, ssh, or ssh-target.

timeshift-btrfs-sync is a destination-pull backup tool for Timeshift Btrfs snapshots. It runs on the backup/destination machine, connects to the source over SSH by default, or uses local source mode on the same machine, and transfers Timeshift snapshots with btrfs send / btrfs receive.

It supports full and incremental backup sends, Timeshift snapshot discovery, copying each snapshot date’s shared Timeshift info.json, writable source snapshots through a read-only send cache, restoring one backup or a full post-common backup chain to Timeshift’s native layout, safe paired retention pruning, optional automatic Timeshift on-demand snapshots, split logs, MQTT notifications, and email notifications with optional log attachments.

Command topology: the most important configuration rule

Normal sync and restore deliberately interpret the configuration differently:

Command Timeshift side Backup side Setting that selects SSH
sync with source.mode = "local" Local Local SSH is not used.
sync with source.mode = "ssh" SSH host Local [source].mode and [ssh].
restore with restore.mode = "local" Local Local SSH is not used.
restore with restore.mode = "ssh" Local SSH host [restore].mode and [ssh]; this is pull restore.
restore with restore.mode = "ssh-target" SSH host Local [restore].mode and [ssh]; this is push restore.

sync never writes the backup destination over SSH. destination.target_root is always local during sync. Therefore a pull-restore profile with restore.mode = "ssh" must not be used to create the backup: during restore that same path is read on the SSH host, but during sync it would have been written locally. The app refuses this command/config combination instead of silently creating a different repository.

For a reversible remote round trip, run both commands on the backup machine with one SSH host:

[source]
mode = "ssh"

[restore]
mode = "ssh-target"

[ssh]
host = "the-timeshift-source.example.lan"

This means: pull Timeshift snapshots from that SSH host into the local backup, then push the local backup back to that same SSH Timeshift host. For pull restore, use a separate restore-pull profile on the Timeshift machine; its SSH host is the backup machine.

Three complete commented config profiles are packaged: config.example.toml for normal sync/local restore, config.restore-pull.example.toml for SSH-backup-to-local-Timeshift restore, and config.remote-roundtrip.example.toml for SSH Timeshift source → local backup → push restore back to the same SSH Timeshift host. Generate one with ts-btrfs init-config --profile ....

Safety model

The safe defaults are intentionally conservative:

  • default_dry_run = true previews changes unless --run is passed. In strict dry-run mode the app does not prepare the destination, create lock/state directories, run btrfs receive, or delete/prune snapshots.
  • Destination pruning only deletes when --run --yes-delete is used.
  • Incremental parents are verified with Btrfs UUID metadata before use.
  • Automatic source-side manual snapshot creation requires source identity and sync-parent viability checks before Timeshift is changed.
  • Restore refuses to overwrite an existing Timeshift date, intersects the configured physical Timeshift and backup timestamp trees newest-first, requires real Btrfs payloads on both sides, prefers exact state/cache Btrfs lineage, securely falls back to matching same-date info.json sys-uuid/type, always creates an exact receiver-side full seed before later incrementals, exposes writable CoW snapshots in Timeshift’s native layout, and commits only after every payload and metadata check succeeds.
  • Normal/user-created Timeshift on-demand snapshots are not pruned unless explicitly enabled.
  • The app does not manage destination Btrfs compression; mount the receiving Btrfs filesystem/subvolume with compression enabled if you want compressed destination storage.

For normal backup, prune, cache cleanup, and Timeshift manual-snapshot operations, the source machine needs passwordless sudo only for Btrfs and Timeshift:

ts-btrfs-sync-user ALL=(root) NOPASSWD: /usr/bin/btrfs *
ts-btrfs-sync-user ALL=(root) NOPASSWD: /usr/bin/timeshift *

This is enough because Timeshift listing/creation, Btrfs send, Btrfs metadata checks, read-only cache creation, and source send-cache cleanup use those two commands. Per-snapshot Timeshift info.json files are read with ordinary non-sudo cat; the SSH/source user therefore needs normal path traversal and file read permission.

The restore command additionally scans physical timestamp directories and control files, then creates and renames an ordinary Timeshift date directory and writes its regular info.json. The simplest supported setup is to run a local restore as root. For an unprivileged restore, grant narrow source-side passwordless sudo for /usr/bin/test, /usr/bin/ls, /usr/bin/base64, /usr/bin/mkdir, /usr/bin/tee, /usr/bin/chmod, /usr/bin/mv, /usr/bin/rm, and /usr/bin/rmdir, plus the configured Btrfs and Timeshift commands. The backup-side endpoint also needs test, ls, and base64 through destination.sudo for its read-only physical scan. For SSH-target restore these source permissions must exist on the remote Timeshift account; for SSH pull the backup scan permissions must exist on the remote backup account. The app prefixes individual commands and does not require passwordless sh or bash; granting a general shell is not recommended.

Remote info.json permissions and persistent source mount

The app runs on the backup/destination machine, but info.json is read on the source machine by the unprivileged account configured as [ssh].user. This is the account the destination uses for the SSH connection. It is not necessarily the local user or root account running ts-btrfs on the destination.

The combined inventory records the effective source account name and UID without opening another SSH connection. If a required info.json cannot be read, the terminal and normal log error include a line like:

remote SSH source account used by this destination: btrbk-source (uid 1001)

That exact account must be able to:

  • traverse every parent directory leading to source.snapshot_root (x/search permission);
  • list source.snapshot_root (r-x), because the combined inventory enumerates the timestamp directories in one shell loop;
  • traverse each timestamp directory;
  • read <source.snapshot_root>/<date>/info.json.

A file can be mode 0644 and still be unreadable when one parent such as /media/<desktop-user> is mode 0750. Root may read it locally while the unprivileged SSH account receives Permission denied. Diagnose the first blocked component on the source host with:

sudo -u btrbk-source -- namei -l \
  /path/to/timeshift-btrfs/snapshots/<date>/info.json

For a reliable unattended backup source, a privileged administrator should mount the Btrfs filesystem at a stable system path through /etc/fstab instead of relying on a private desktop mount below /media/<user>. Example for the Btrfs top-level tree:

sudo install -d -m 0755 /mnt/OS-Root
sudo blkid
sudoedit /etc/fstab

Example /etc/fstab entry:

UUID=<BTRFS-FILESYSTEM-UUID> /mnt/OS-Root btrfs defaults,subvolid=5 0 0

Mount it and grant the configured SSH account only the access it needs:

sudo mount /mnt/OS-Root

# Search-only permission through private parent directories.
sudo setfacl -m u:btrbk-source:--x /mnt/OS-Root
sudo setfacl -m u:btrbk-source:--x /mnt/OS-Root/timeshift-btrfs

# The inventory must be able to list the snapshot dates.
sudo setfacl -m u:btrbk-source:r-x \
  /mnt/OS-Root/timeshift-btrfs/snapshots

# Confirm the exact file is readable as the SSH account.
sudo -u btrbk-source -- cat \
  /mnt/OS-Root/timeshift-btrfs/snapshots/<date>/info.json

Replace btrbk-source with the name printed by the application and use its printed UID when checking ownership or ACL results. The mount does not have to be owned by the backup account; it only has to be accessible to that account by name/UID through normal mode bits or a narrow ACL. Btrfs normally keeps Unix ownership in filesystem metadata, so do not rely on FAT/NTFS-style uid= or gid= mount options to remap ownership. After changing the mount path, update source.snapshot_root and source.cache_root in the config.

Source sudoers and source cleanup

destroy-leftovers --delete-source keeps the source sudoers model narrow. On the source host it uses passwordless sudo btrfs only for Btrfs metadata and subvolume deletion. It does not require passwordless find, test, rm, mkdir, or cat.

Source cache payloads and timestamp containers are Btrfs subvolumes. Cleanup deletes child cache subvolumes first and then deletes the timestamp parent with btrfs subvolume delete. The app does not use ordinary-directory cleanup as a fallback. If a configured cache path is an ordinary non-empty directory, cleanup stops and requires manual inspection.

Destination layout

The destination target_root is the backup job folder. The app creates and owns:

<target_root>/snapshots/                         Btrfs snapshots-root subvolume
<target_root>/snapshots/<date>/                   Btrfs date subvolume
<target_root>/snapshots/<date>/info.json         shared Timeshift control file
<target_root>/snapshots/<date>/@                 received root subvolume, when configured
<target_root>/snapshots/<date>/@home             received home subvolume, when configured
<target_root>/.ts-btrfs-sync/                    state.json, lock file, logs

Timeshift uses one info.json for the whole snapshot date. It sits beside @ and optional @home; there is not a second combined metadata file to create. The app captures that one file once and writes it once after every subvolume configured for the date has completed. This works with subvolumes = ["@", "@home"], subvolumes = ["@"], or subvolumes = ["@home"].

The destination target_root, its managed snapshots/ child, and every snapshots/<date> container must be Btrfs subvolumes. If target_root is missing and destination.create_target_root = true, the app creates it with btrfs subvolume create. Missing snapshots/ is also created with btrfs subvolume create, but unlike state, lock, and optional log helpers it never falls back to mkdir. Each date container is created and exact-probed as a Btrfs subvolume before btrfs receive. Existing ordinary date folders, files, or symlinks directly below snapshots/ are refused as an unsupported layout and must be inspected and moved or removed manually. A bulk destination index miss is not enough to classify a date as ordinary: the app first runs an exact btrfs subvolume show probe and accepts/adds a real subvolume to the current index.

state.json records successfully received snapshots and the metadata needed for incremental sends. Do not manually delete state.json while a job is running. Use the guarded clear-state command when you intentionally want to remove the configured state file after a failed transfer or before a controlled state-recovery run. Do not delete only snapshots/ while keeping the corresponding state file.

State schema version 3 stores every managed path relative to the configured root that owns it:

{
  "source_path": "2026-07-15_05-00-02/@",
  "send_path": "2026-07-15_05-00-02/@",
  "send_path_kind": "source-cache",
  "parent_source_path": "2026-07-15_04-00-02/@",
  "parent_source_path_kind": "source-cache",
  "destination_path": "snapshots/2026-07-15_05-00-02/@"
}

source_path is always relative to source.snapshot_root. send_path is resolved under source.cache_root when send_path_kind = "source-cache", or under source.snapshot_root when send_path_kind = "timeshift-original-readonly". parent_source_path follows the same rule through parent_source_path_kind. destination_path remains relative to destination.target_root.

This lets you move or remount source.snapshot_root, source.cache_root, or the whole destination target, then update those roots in the config. State paths resolve below the new locations. Btrfs UUID identity is still required before a moved send-cache or Timeshift path can be used as an incremental parent. state.json must use schema version 3 and root-relative managed paths; unsupported versions, absolute stored paths, missing ownership kinds, and mismatching snapshot/subvolume suffixes are rejected.

During sync and before standalone prune, mutable Timeshift metadata for already-synced snapshots is refreshed from the latest timeshift --list. This updates snapshot-level tags, comment, created, and path in state.json without re-sending data and without changing Btrfs UUID, parent-chain, send-path, destination-path, or status fields. This lets retention follow Timeshift when it later promotes or changes flags such as O, H, D, W, or M. The metadata refresh uses the fast Timeshift list path and does not run btrfs subvolume show for every already-synced snapshot.

If state.json is missing or empty while destination snapshots already exist, sync tries a conservative state recovery before creating a manual snapshot or sending data. It scans destination snapshots and compares each destination subvolume's Received UUID with the UUID of the matching source Timeshift subvolume or an existing read-only source-cache subvolume. Only exact UUID matches are adopted into in-memory state and, during a real run, written back to state.json. Names alone are never trusted. If an existing destination subvolume cannot be adopted, the app refuses to delete it as an incomplete receive because it may be a valid backup from a missing state file.

A full reset means deleting both the received snapshot subvolumes and .ts-btrfs-sync/. Use destroy-leftovers for a complete app-owned reset. It deletes nested Btrfs subvolumes deepest-first and does not recursively remove ordinary non-empty backup trees.

Restore layout and workflow

The backup layout and Timeshift's native source layout are intentionally different:

Backup destination:
<target_root>/snapshots/<date>/              Btrfs date-container subvolume
├── info.json                                regular file
├── @                                        read-only received Btrfs subvolume
└── @home                                    read-only received Btrfs subvolume, when configured

Timeshift repository after restore:
<source.snapshot_root>/<date>/               ordinary directory
├── info.json                                exact original Timeshift file
├── @                                        writable Btrfs subvolume
└── @home                                    writable Btrfs subvolume, when configured

The command has two restore selections:

  • restore --snapshot <date> restores one selected backup.
  • restore --all finds the newest proven common snapshot and restores every newer backup oldest-to-newest.

Optional pre-restore safety snapshot

Add --create-pre-restore-snapshot to create one Timeshift on-demand/tag O snapshot of the system being restored before any hidden receive directory or Btrfs stream is started:

ts-btrfs restore --config ./config.toml \
  --all \
  --create-pre-restore-snapshot \
  --dry-run

In a real run the snapshot is created only after the restore plan, OS identity, target-path checks, and typed confirmations have succeeded. The app then re-reads timeshift --list, identifies exactly one newly created timestamp, and exact-checks every configured payload such as @ and @home with Btrfs metadata. Failure aborts before transfer.

The safety snapshot always runs through the configured Timeshift source endpoint:

  • local backup → local Timeshift: created locally;
  • local backup → SSH Timeshift: created on the remote Timeshift machine;
  • SSH backup → local Timeshift with [restore] mode = "ssh": created locally.

It is never created on destination.target_root or on the SSH backup host. The fixed comment is TimeShift-BTRFS-Sync pre-restore safety snapshot. It is intentionally left in Timeshift if the later restore fails, providing a rollback point for the pre-restore system state. The [manual_snapshot] section controls automatic snapshots before sync; it does not control this restore-only flag.

Pull a remote SSH backup into local Timeshift

Restore can reverse the normal network direction without a second restore implementation. In pull mode the backup repository is remote and the Timeshift repository is local:

SSH backup host: destination.target_root/snapshots/<date>/@
        btrfs send over SSH
                 ↓
Local machine: source.snapshot_root/<date>/@
        local btrfs receive

Generate the complete restore-only profile instead of building it from a shortened snippet:

ts-btrfs init-config \
  --profile restore-pull \
  --path ./config-restore-pull.toml

The generated file includes every supported top-level, restore, SSH, source, destination, stream, retention, manual-snapshot, MQTT, and mail setting with comments. Its [ssh] section includes key authentication, identity_passphrase/identity_passphrase_file for encrypted private keys, optional remote-account password/password_file, plus port, cipher, compression, ControlMaster, strict host-key checking, timeouts, keepalives, and jump-host examples.

The generated pull profile sets [restore] mode = "ssh", so destination.target_root and state_file are interpreted on the SSH backup host. lock_file, source.snapshot_root, and source.cache_root remain local to the machine running the restore; neither Timeshift path nor the restore lock is probed on the backup host. source.mode is not used to choose restore direction. Use a separate restore config so later normal sync, prune, and destroy-leftovers runs do not accidentally interpret remote restore paths as local backup paths.

Dry-run and real examples:

ts-btrfs restore --config ./config-restore-pull.toml \
  --all \
  --create-pre-restore-snapshot \
  --dry-run

ts-btrfs restore --config ./config-restore-pull.toml \
  --all \
  --create-pre-restore-snapshot \
  --run \
  --i-understand-this-modifies-timeshift

If a local Timeshift repository is accidentally selected as the backup, the app now explains that Timeshift date folders are correctly ordinary directories and points to this transport setting instead of presenting the layout as corrupt.

The remote SSH account needs narrow passwordless permission through destination.sudo for the configured Btrfs command plus read-only test, ls, and base64 operations used to inventory backup timestamp directories, info.json, and state.json. The scanner prefixes those individual commands; it never runs a privileged shell. The account does not need remote lock-file write permission or flock. The local side needs the normal restore permissions for btrfs receive, Timeshift, ordinary date directories, info.json, and the configured local lock_file. The local restore lock is held until all streams and verification complete.

info.json provenance and same-OS warnings

Restore parses Timeshift metadata before changing the source repository. It uses:

  • sys-uuid as the root-filesystem identity recorded when that snapshot was created.
  • type, which must identify Btrfs snapshots.
  • sys-distro as diagnostic context only, because an in-place distribution upgrade can change that text without creating a different root filesystem.

The comparison intentionally ignores snapshot-specific fields that legitimately change between snapshots, including H/D/W/M/O/B tags, comments, creation time, file counts, Timeshift app version, live status, and Btrfs statistics.

The original info.json is restored unchanged. Its sys-uuid is the stable Timeshift filesystem/OS identity recorded for that snapshot. Restore compares sys-uuid and type for the same timestamp as a secure fallback when exact state/cache Btrfs lineage is unavailable. It never compares the whole JSON object: tags, comments, creation time, counters, and other mutable fields may differ. Exact Btrfs lineage remains the stronger proof when available.

Every backup selected by --all must contain one consistent sys-uuid and snapshot type. A mixed backup set is refused. The backup provenance is compared with the readable info.json files in the current Timeshift repository. When no exact UUID common parent exists and no metadata match can be proven, dry-run prints a cross-OS warning and a real restore requires:

--allow-os-identity-mismatch

followed by this exact typed sentence:

I UNDERSTAND THIS BACKUP MAY BELONG TO ANOTHER OS

Common-parent safety

For restore --all, restore first intersects the timestamp directories physically present below the configured source.snapshot_root and destination.target_root/snapshots. It considers candidates newest-to-oldest. timeshift --list contributes tags, comments, and diagnostics, but it is not the authority for physical timestamp existence.

A candidate is usable only when every configured payload such as @ and @home is a real Btrfs subvolume on both sides. Restore then accepts the newest candidate proven by either of these methods:

  1. Exact Btrfs lineage proof: a completed state.json record links the current Timeshift payload UUID to original_source_uuid and the backup payload Received UUID to send_source_uuid; or the retained read-only cache proves current Timeshift UUID → cache Parent UUID and cache send identity → backup Received UUID. A natively read-only Timeshift payload may match the backup Received UUID directly.
  2. Same-date Timeshift identity proof: when exact state/cache lineage is unavailable, the same physical timestamp exists on both sides, all configured payloads are Btrfs subvolumes, and that timestamp's source and backup info.json files have the same stable sys-uuid and type. Mutable tags, comments, creation time, statistics, and Timeshift version are deliberately ignored.

The second method is safe for choosing an already-present timestamp because restore does not use the visible Timeshift payload as the receive parent. It full-receives the exact backup copy of the common timestamp into a hidden receiver-side chain before sending newer incrementals. A matching date alone, a repository-wide info.json match from another date, or names in state.json without live payloads are never accepted.

state.json is therefore preferred evidence, not a mandatory dependency. Missing, unreadable, symlinked, malformed, or unsupported state is never trusted; restore records the validation warning, uses empty state for planning, and continues only if independent retained-cache or matching same-date info.json proof succeeds. destroy-leftovers --delete-destination may intentionally remove the destination and its state while keeping source.cache_root; a later fresh backup can be matched through exact retained-cache lineage. If cache/state evidence is unavailable, matching same-date info.json identity still permits the newest physically shared timestamp to be selected.

The read-only repository scan uses the configured privilege prefixes to run only test, ls, and base64 for directory/type/control-file inspection. It never grants or invokes a privileged shell. Local, SSH pull, and SSH-target restore use the same candidate and proof rules.

If the common timestamp is already the newest backup, there is nothing to restore. If no candidate can be safely confirmed, --all prints the newest failed candidate diagnostics and refuses a real full-chain restore unless --allow-no-common-parent and the stronger typed confirmations are supplied. The repository-wide info.json OS-identity warning remains separate and may require --allow-os-identity-mismatch when no exact proof overrides it.

Receiver-side full seed and incremental chain

Read the rest on GitHub

Scan report · 2026-10-12
  • ✓ Prohibited terms or links
  • ✓ Repository eligibility
  • ✓ slopscore.md paperwork
  • ✓ Content policy
  • ✓ Risk review — +10 owner has 0 followers

From the balcony · 0 of 4 clapped

    Crusoe, Schnitzel, Cap'm Slop and Princess read it and passed. Their reasons are on the balcony, with every other verdict.

    Critics are accounts on this site with no GitHub account behind them. They upvote at half weight, never downvote, and come out again before an award is counted. Who they are.

    0 comments

    log in to comment.

    report this listing — log in to report