Stop Immich from re-uploading photos you permanently deleted.
⚠️ Read the warnings before using this. This tool intentionally destroys file contents. The code is AI-generated. Use at your own risk.
¿Español? Lee el README.es.md.
When you permanently delete a photo in Immich (emptying the trash, or after the 30-day trash period), the server erases every record of the asset — including its SHA1 checksum. On its next backup cycle, the mobile app compares your phone's library against the server, doesn't find the photo, and uploads it again. Delete it again, it comes back again. Forever, as long as the photo exists on your phone.
This has been an open request since 2023 (discussion #4282, see also #23897, #22507). There is no native fix. On Android there is an experimental Sync Remote Deletions option (which deletes the photo from your phone); on iOS there is nothing.
A small zero-dependency Python script runs from cron and keeps a blocklist of checksums:
- Capture — every asset sitting in the trash gets its SHA1 checksum recorded. Deleting to trash = "I don't want this on the server".
- Control — when an active asset appears whose checksum is blocklisted (= the app re-uploaded it), the script turns it into a tombstone:
- The asset is archived (
visibility: archive): it disappears from the timeline, but its checksum stays in the database. The app's pre-upload check now answersduplicate— it stops re-uploading that file forever. - The original file (and the transcoded video, usually the biggest offender) is truncated to 0 bytes on disk. The already-generated thumbnails are kept, so the tombstone is still recognizable in the Archive view at preview quality while using almost no space.
- The asset is archived (
- Recovery — want a photo back? Un-archive it in the Immich UI. The script unblocks the checksum and deletes the empty husk; your phone re-uploads the full-quality version on its next backup.
- Restoring a photo from the trash (before purging) also unblocks it — the script only ever touches checksums that went through the trash.
Why truncation works: Immich computes the checksum once, at upload time, and never re-verifies file contents. Truncating instead of deleting keeps the Repair page free of missing-file warnings.
| mode (config) | re-upload loop stops | disk space freed | needs filesystem access |
|---|---|---|---|
tombstone (default) |
✅ | ✅ (thumbnails remain, ~150 KB/photo) | ✅ runs on the host that stores the media |
archive |
✅ | ❌ (full files remain, hidden in Archive) | ❌ API only, runs anywhere |
delete |
❌ (infinite loop) | ✅ | ❌ API only |
- Immich v3 (tested on v3.0.1; needs the
visibilityfield and thetrashedAftersearch filter) python3(any recent version, standard library only) and cron on the machine that runs the script- For
tombstonemode: run it on the machine that has direct access to Immich'sUPLOAD_LOCATION(the Docker host, or a host mounting it via NFS/SMB) - An Immich API key for your user (Account Settings → API Keys) with asset read/update/delete and search permissions (or full access)
git clone https://github.com/JaTabs/immich-tombstone.git
cd immich-tombstone
sudo ./install.shThe installer asks for everything (API URL, key, mode, paths), validates API access, and sets up a cron job (default: every 15 minutes). Unattended install is possible via IT_* environment variables — see the header of install.sh.
Manual install: copy immich_tombstone.py somewhere, create a config.json next to it (see config.example.json, chmod 600), and add a cron line:
*/15 * * * * /usr/bin/python3 /opt/immich-tombstone/immich_tombstone.py >/dev/null 2>&1
Multiple Immich users: the API key scopes everything to one user. Create one config file per user and pass its path as the first argument: immich_tombstone.py /etc/immich-tombstone/alice.json (state files live next to each config).
- Delete photos normally (trash → empty trash, or let the 30 days pass). Nothing else to do — re-uploads become tombstones within one cron interval.
- Tombstones live in the Archive view. To really recover one, un-archive it and wait for the next phone backup.
- The log (
tombstone.log, next to the config) tells you everything:BLOCKED,TOMBSTONED,UNBLOCKED, plus a per-runOKsummary line. - Optional ntfy notifications when something is tombstoned or revived.
- Blind window: if you trash and purge a photo within one cron interval (default 15 min), its checksum is never captured and that photo will keep re-uploading. Empty the trash at least 15 minutes after deleting, or lower the interval.
- Re-uploads are visible in the timeline for up to one cron interval before being tombstoned (the approach is reactive — nothing can reject the upload itself server-side).
- Tombstoned videos won't play (only their thumbnail survives). Photos remain viewable at preview quality.
- Assumes Immich's standard storage layout (
library/,encoded-video/,thumbs/underUPLOAD_LOCATION). - The script talks to undocumented-ish API surface (
search/metadataquirks change between majors — e.g. v3 silently ignoresisTrashed). A future Immich version may need adjustments. - If Immich ever gains a native deleted-assets registry, use that instead of this.
- This tool permanently and irreversibly destroys the content of files it tombstones. That is its purpose. If it misidentifies an asset (bug, API change, race condition), that photo's full-quality content is gone unless it still exists on a phone or backup.
- The author is not responsible for any data loss. No warranty of any kind. Use with caution, at your own risk, and only if you understand what it does. Test it with a throwaway photo first (upload → trash → empty trash → let it re-upload → watch the log).
- This code was generated by an AI (Claude) under human direction and tested against a single real-world instance (Immich v3.0.1, ~15k assets). Review it before running it — it's ~250 lines of straightforward Python.
- Keep backups of your Immich library and database. You should be doing this anyway.
- Not affiliated with or endorsed by the Immich project.
0 comments
log in to comment.