English · Русский
Automatic cascade cleanup for your self-hosted media stack.
CleanArr listens for Jellyfin ItemDeleted webhooks and cascades deletion to Radarr, Sonarr, Seerr, and supported torrent clients — automatically, safely, and without touching files it doesn't own.
Quick start · Linux packages · Screenshots · How it works · Configuration · Torrent clients · Compatibility · Safety · Troubleshooting · Operations · Roadmap · Contributing
When you delete something in Jellyfin, you usually have to manually clean up the same item in Radarr, Sonarr, Seerr, and your torrent clients. CleanArr automates this entire chain:
- Jellyfin fires an
ItemDeletedwebhook - CleanArr resolves the item in Radarr/Sonarr using strict ID matching (TMDB → IMDB → path)
- Torrent hashes are routed to qBittorrent, Transmission, Deluge, and rTorrent — only when Arr history proves ownership
- The entry is removed from Radarr/Sonarr
- Matching requests, issues, and media records are cleaned up in Seerr
Pack torrents, shared files, and anything that can't be safely attributed are always skipped.
- Cascade deletion — one webhook triggers a full cleanup chain: Jellyfin → Radarr/Sonarr → torrent clients → Seerr
- Multi-instance routing — every enabled Radarr, Sonarr, and torrent-client profile participates without numeric ID collisions
- Strict ID matching — resolves items by TMDB/TVDB/IMDB ID and path; no fuzzy guessing
- Conservative guardrails — pack torrents and files shared between items are never deleted; CleanArr logs the reason and skips
- Confirmed preflight — enabled before every manual deletion; shows exact media IDs, Arr instance, torrent client/hash/path, downstream mutations, and safety skips
- Durable background cleanup — manual jobs, partial results, and retry state survive process restarts and report live step-by-step progress
- Idempotent execution — completed Jellyfin deliveries are suppressed for seven days, partial failures remain retryable, and one safety lock serializes all destructive work in a CleanArr instance
- Live health monitoring — probes all connected services every 30 s; status visible on the dashboard
- Webhook auto-configure — one-click setup of the Jellyfin Webhook plugin directly from the UI
- Activity log — every processed event is stored with full action breakdown; searchable by title, system, action, or status
- Guided setup wizard — first-run wizard walks you through connecting each service step by step
- Multi-profile — save multiple service definitions per type, pick one as the active runtime target
- Local and SSO authentication — local password login plus strict OpenID Connect validation, PKCE, nonce, and explicit user/group/claim access policies
- Dark / light mode — follows system preference
git clone https://github.com/mambastick/Cleanarr.git
cd Cleanarr
# Start (review environment variables in the compose file first)
docker compose -f deploy/docker-compose.yml up -dOpen http://localhost:8089 — the setup wizard walks you through the rest.
Before an image upgrade, create and export a verified SQLite backup:
docker compose -f deploy/docker-compose.yml exec -T cleanarr python3 -c 'import sqlite3; source=sqlite3.connect("/config/cleanarr.db"); backup=sqlite3.connect("/config/cleanarr.pre-upgrade.db"); source.backup(backup); print(backup.execute("PRAGMA integrity_check").fetchone()[0]); backup.close(); source.close()'
docker compose -f deploy/docker-compose.yml cp \
cleanarr:/config/cleanarr.pre-upgrade.db ./cleanarr.pre-upgrade.dbThe check must print ok. To roll back, pin the previous image, stop the
service, copy the verified backup back to /config/cleanarr.db, and start it
again. Keep the failed database under a different name until the restore is
verified.
docker run -d \
--name cleanarr \
-p 8089:8089 \
-e DRY_RUN=true \
-v cleanarr-config:/config \
ghcr.io/mambastick/cleanarr:latestEvery release provides .deb and .rpm packages for amd64 and arm64. They install CleanArr under /opt/cleanarr, create a dedicated system user, and provide a hardened systemd service.
# Debian / Ubuntu
sudo apt install ./cleanarr_<version>_amd64.deb
# Fedora / RHEL-compatible distributions
sudo dnf install ./cleanarr-<version>-1.x86_64.rpm
sudo systemctl enable --now cleanarrThe default configuration is stored in /etc/cleanarr/cleanarr.env; application data is stored in /var/lib/cleanarr. Packages require systemd and Python 3.12. See the complete native package guide, including upgrade, removal, backup, and checksum instructions.
kubectl apply -f deploy/k8s/namespace.yaml
kubectl apply -f deploy/k8s/pvc.yaml
# Edit secret.example.yaml with your values first
kubectl apply -f deploy/k8s/secret.example.yaml
kubectl apply -f deploy/k8s/deployment.yaml
kubectl apply -f deploy/k8s/service.yaml
kubectl apply -f deploy/k8s/ingress.yamlThe deployment uses strategy: Recreate because the config PVC is ReadWriteOnce.
All settings can be changed at runtime from the Settings tab. Environment variables provide defaults on first start.
| Variable | Default | Description |
|---|---|---|
DRY_RUN |
true |
Set to false to enable real deletions |
LOG_LEVEL |
INFO |
DEBUG, INFO, WARNING, ERROR |
HTTP_TIMEOUT_SECONDS |
15 |
Timeout for calls to downstream services |
DB_PATH |
/config/cleanarr.db |
SQLite database path — must be on a persistent volume |
CONFIG_STATE_PATH |
/config/runtime-config.json |
Legacy runtime-config migration path |
ADMIN_SHARED_TOKEN |
— | Optional static token that bypasses session auth (useful for automation) |
WEBHOOK_SHARED_TOKEN |
auto-generated | Shared secret verified on every inbound webhook. Auto-generated on first start; rotate from Settings → General |
UI_LANGUAGE |
en |
Initial UI language: en or ru |
JELLYFIN_LANGUAGE |
en |
Preferred metadata language for Jellyfin integration |
SSO_MODE |
password_only |
Authentication mode: password_only, both, or sso_only |
SSO_ENABLED |
false |
Enables the OpenID Connect integration |
SSO_ISSUER_URL |
— | OpenID Connect issuer URL |
SSO_CLIENT_ID |
— | OpenID Connect client ID |
SSO_CLIENT_SECRET |
— | OpenID Connect client secret |
SSO_REDIRECT_URI |
— | Callback URL, usually https://cleanarr.example/api/auth/sso/callback |
SSO_SCOPES |
openid profile email |
OpenID Connect scopes |
SSO_ALLOWED_USERS |
— | Comma-separated usernames/emails/subjects allowed to sign in |
SSO_ALLOWED_GROUPS |
— | Comma-separated group values allowed to sign in |
SSO_GROUP_CLAIM |
groups |
ID-token claim containing group values |
SSO_REQUIRED_CLAIM |
— | Optional additional ID-token claim required for access |
SSO_REQUIRED_VALUE |
— | Required value; configure together with SSO_REQUIRED_CLAIM |
SESSION_COOKIE_SECURE |
auto | Force Secure on/off; set true when TLS terminates at a reverse proxy not trusted for forwarded headers |
Important:
DB_PATHmust point to a persistent volume. Without it, all service configurations and activity history are lost on restart.
Existing jellyseerr profiles are migrated in place to the canonical seerr
configuration on startup. The legacy JELLYSEERR_URL /
JELLYSEERR_API_KEY variables and /api/config/jellyseerr routes remain
backward-compatible aliases.
SSO remains disabled until at least one explicit user/group allowlist or a
required claim/value pair is configured. See the complete OIDC and reverse
proxy guide before enabling both or sso_only mode.
The easiest way is to use Auto-configure in the Jellyfin service editor (click the pencil icon on the Jellyfin card in the Dashboard). It installs the correct config into the Jellyfin Webhook plugin automatically.
Manual setup: install the Webhook plugin in Jellyfin → Dashboard → Plugins → Catalog, then add a Generic destination:
- URL:
http://your-cleanarr-host:8089/webhook/jellyfin - Method:
POST - Header:
X-Webhook-Token: <your-token> - Notification type:
Item Deletedonly - Template:
- Resolve in Radarr by
tmdb_id → imdb_id → path(strict, no fuzzy matching) - Collect torrent hashes from Radarr download history
- Delete safe hashes in every owning torrent client, optionally together with local data
- Delete the Radarr entry
- Delete matching Seerr requests, issues, and media records
- Resolve in Sonarr by
tvdb_id → tmdb_id → imdb_id → path - Delete torrent hashes exclusively owned by the series
- Delete the Sonarr series entry
- Delete all Seerr requests, issues, and media for the series
- Resolve parent series in Sonarr
- Unmonitor all episodes in the target season
- Delete only episode files and hashes fully covered by the season scope
- Update or remove matching Seerr season requests
- Resolve parent series in Sonarr
- Unmonitor the target episode range
- Delete episode file and hash only when fully isolated
- Delete matching Seerr episode issues; retain a season-scoped request unless the event provably covers the complete season, then update or remove it
Guardrails: pack torrents (multiple series/seasons in one archive) and shared files are never deleted — CleanArr logs the reason and skips destructive actions.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/webhook/jellyfin |
X-Webhook-Token header |
Main ingestion endpoint |
GET |
/api/dashboard |
session | Dashboard snapshot for the SPA |
GET |
/api/config |
session | Runtime configuration |
PUT |
/api/config/general |
session | Update general settings |
GET |
/api/config/export |
session | Export a credential-free configuration document |
POST |
/api/config/import |
session | Merge a redacted configuration in fail-safe mode |
GET |
/api/support/bundle |
session | Redacted operational support snapshot |
GET |
/metrics |
session or admin token | Privacy-safe Prometheus metrics |
POST |
/api/config/jellyfin/setup-webhook |
session | Auto-configure the Jellyfin Webhook plugin |
POST |
/api/auth/login |
— | Admin login |
GET |
/api/auth/status |
— | Current authentication capabilities and session state |
GET |
/api/auth/sso/login |
— | Start the OpenID Connect login flow |
GET |
/health/live |
none | Liveness probe |
GET |
/health/ready |
none | Readiness probe |
cleanarr/
├── backend/ # Python 3.12 / FastAPI
│ └── src/cleanarr/
│ ├── api/ # Routes, schemas, dashboard, auth
│ ├── application/ # Cascade deletion logic, configuration service
│ ├── domain/ # Models, config, errors
│ └── infrastructure/ # HTTP clients, SQLite stores, settings
├── frontend/ # React 19 + Vite + TypeScript + shadcn/ui
│ └── src/
├── deploy/
│ ├── Dockerfile # Multi-stage build (node:24 → python:3.12-slim)
│ ├── docker-compose.yml
│ └── k8s/ # Kubernetes manifests
├── packaging/ # DEB/RPM metadata, systemd unit, build scripts
└── docs/
├── LINUX_PACKAGES.md # Native Linux package guide
├── OPERATIONS.md # Metrics, support bundle, config transfer
├── RELEASING.md # Bilingual release policy
└── screenshots/
| Layer | Technology |
|---|---|
| Backend | Python 3.12, FastAPI, httpx, Pydantic v2, uvicorn |
| Frontend | React 19, Vite, TypeScript, shadcn/ui, Tailwind CSS v4, Sonner, Motion |
| Storage | SQLite (config + activity log) |
| Container | Multi-stage Docker build — node:24-bookworm-slim → python:3.12-slim |
# Backend
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Frontend (build static assets served by the backend)
cd frontend
pnpm install && pnpm build
# Run backend with hot reload
cd backend
uvicorn cleanarr.api.app:app --host 0.0.0.0 --port 8089 --reloadFor full hot-reload on both sides, run the frontend dev server in parallel — it proxies /api, /health, and /webhook to port 8089:
# Terminal 2
cd frontend
pnpm devcd backend && pytest
cd frontend && pnpm build # also runs tscRelease notes are maintained in both Russian and English. See Release process.






