🧯 qbit-ops
🧯 A tiny qBittorrent CLI and TUI for people who don't want to nuke their seedbox by accident.
Inspect, diagnose and automate qBittorrent at a scale too big to manage by hand - and see exactly what a change will touch before it touches anything.
✨ Featured in Self-Host Weekly by selfh.st.
A live transfer, a filter, and a bulk pause shown before it is applied - then abandoned. Nothing moved.
🚀 Get started
Requires Python 3.12+ and a qBittorrent instance with the Web UI enabled.
🐍 From PyPI
Recommended, with uv:
uv tool install qbit-ops
Or with pipx:
pipx install qbit-ops
With the optional TUI:
uv tool install "qbit-ops[tui]"
pipx install "qbit-ops[tui]"
With the experimental MCP server, to talk to your library through an agent:
uv tool install "qbit-ops[mcp]"
pipx install "qbit-ops[mcp]"
📥 With the install script
For a machine with none of the above yet - no uv, pipx, Homebrew or
Docker, and no Python either. It installs uv,
which brings its own Python when the system has none recent enough.
curl -LsSf https://raw.githubusercontent.com/LECOQQ/qbit-ops/v0.6.0/scripts/install.sh | sh # x-release-please-version
A checksum is published beside the script, if you would rather read it before running it:
curl -LsSf https://raw.githubusercontent.com/LECOQQ/qbit-ops/v0.6.0/scripts/install.sh.sha256 # x-release-please-version
🍺 With Homebrew
brew install LECOQQ/qbit-ops/qbit-ops
The tap ships the TUI, so there is no extra to pick. Upgrades follow the
usual brew upgrade qbit-ops.
🐳 With Docker
Runs the same CLI on amd64 and arm64, with nothing installed on the host:
docker run --rm -it \
-e QBIT_HOST -e QBIT_USER -e QBIT_PASSWORD \
ghcr.io/lecoqq/qbit-ops:latest status
The TUI needs a TTY, which -it already gives you:
docker run --rm -it \
-e QBIT_HOST -e QBIT_USER -e QBIT_PASSWORD \
ghcr.io/lecoqq/qbit-ops:latest tui
For a long-lived setup, see the Compose example.
⌨️ Then turn on completion
qbit-ops --install-completion
For bash, zsh, fish or PowerShell. Commands, options, and the options whose values are a fixed set all complete; an upgrade needs no reinstall. Your own categories, tags and tracker hosts complete too, from a cache that fills as you use the tool.
⬆️ Upgrading
uv tool upgrade qbit-ops
pipx upgrade qbit-ops
brew upgrade qbit-ops
The install script above installs through uv tool install, so
uv tool upgrade qbit-ops upgrades it too. To remove what it
installed:
uv tool uninstall qbit-ops
🔌 Connecting
qbit-ops init
It asks for the host, user and password, tests them before writing,
and saves to ~/.config/qbit-ops/.env with mode 0600. qbit-ops tui
opens the same form when nothing is configured yet, and ctrl+o reopens
it later to point at a different instance without restarting.
Where there is no terminal - a script, a container - set the three
variables instead, and skip init entirely:
QBIT_HOST=http://192.168.1.10:8080
QBIT_USER=admin
QBIT_PASSWORD=…
Pointing at several instances is one variable: QBIT_OPS_ENV_FILE=~/.config/qbit-ops/seedbox.env
takes precedence over a project-local .env, which takes precedence over
the user file.
Then:
qbit-ops status
qbit-ops doctor
qbit-ops tui
🎸 Greatest hits
🧹 Reclaiming space
Reclaim disk without touching what matters.
qbit-ops torrents delete --ratio-min 2 --seeded-for 90d --exclude-tag keep
scanned 2418
matched 407
status PREVIEW (dry-run)
Nothing is deleted: review it, add --no-dry-run, and that exact
selection is what will run.
Know what your library actually weighs.
qbit-ops torrents stats
qbit-ops torrents stats --category sonarr
Size, transfer and seeding time over exactly the torrents you filter for. How qBittorrent's all-time counters are handled is in docs/COMMANDS.md.
🩺 Finding out what hurts
Find out which tracker is hurting you.
qbit-ops trackers status
qbit-ops explain tracker --tracker tracker.example
One line per tracker, health aggregated across every torrent that announces to it, and then the reasoning behind the verdict.
Act on every torrent stuck behind a dead tracker.
qbit-ops torrents list --tracker-health critical
qbit-ops torrents pause --tracker-health critical --no-dry-run
Selects on each torrent's own trackers, and gives the same verdict
explain torrent does - it is the same computation. How a torrent with
mixed tracker health is scored, and why a non-answer never matches, is
in docs/COMMANDS.md.
See what each tracker is actually carrying.
qbit-ops trackers list
qbit-ops trackers list --category sonarr
Torrents, size, transferred bytes, ratio and seeding time per tracker,
plus EXCL - the torrents that tracker is the only home of, which is
the real answer to "what would I lose by leaving?". The columns
deliberately do not sum to your library total; the reason is in
docs/COMMANDS.md.
🔀 Surviving a tracker that moved, or died
Four commands, one shape: name the tracker, preview what it would touch, then apply. Each one walks your whole library so you never have to open a torrent to fix it.
Rotate a leaked passkey everywhere at once.
qbit-ops trackers replace-passkey \
--tracker "https://tracker.example/announce/{passkey}"
{passkey} marks where the secret sits in the URL - you type those nine
characters, never a passkey. It asks for the new one at a hidden prompt.
To drive it from a script, pipe the value in instead, and add --yes
alongside --no-dry-run: stdin is then busy carrying the passkey, so the
confirmation cannot read your answer there too.
pass show tracker/passkey | qbit-ops trackers replace-passkey \
--tracker "https://tracker.example/announce/{passkey}" \
--new-passkey-stdin --no-dry-run --yes
Follow a tracker that changed address.
qbit-ops trackers replace \
--source "old.example" \
--target "https://new.example/announce/{passkey}"
--source takes a bare host: identifying a tracker was never what a
passkey was for. --target needs the placeholder, because a tracker this
tool has never seen cannot have its passkey position guessed.
Keep a dying tracker while you move off it.
qbit-ops trackers add-if-present \
--source "dying.example" \
--target "https://backup.example/announce"
Adds the second tracker next to the first, on the torrents that carry the first and only those. Both announce, so nothing stops seeding while the old host makes up its mind. Torrent filters narrow the scan further if you only want part of the library.
Retire a tracker for good.
qbit-ops trackers remove --tracker "dead.example"
Drops it from every torrent that still lists it.
🔎 Finding it, and unsticking it
Find a torrent without remembering its exact name.
qbit-ops torrents search "iso ubunutu desktp" # typos, word order
qbit-ops torrents pause --hash 3f2a1b # copy the hash, then act
Ranked, tolerant, and read-only by construction: a search result is
never a mutation target. Accents and case are folded before anything is
compared. --name-contains/--name-regex stay the exact, deterministic
option for that.
Unstick a queue that stopped moving.
qbit-ops torrents list --stalled
qbit-ops torrents reannounce --stalled --no-dry-run
🛡️ Safety by default
- 🧪 Dry-run first. Nothing changes unless
--no-dry-runis explicit. - 🎯 Hash-based targeting. Mutations never guess from a fuzzy torrent name.
- 🧊 Frozen plans. The previewed selection is the selection that gets applied.
- 🚫 No silent “all”. Bulk actions require a hash,
--all, or an explicit filter. - ❔ Unknown is never a match. A value qBittorrent didn't report never widens a selection.
- 🔒 Secret-safe output. Tracker passkeys and announce URLs stay redacted in normal output.
- ✅ Honest results. qbit-ops reports what was submitted or observed, not what it cannot prove.
🎯 Target exactly what you mean
Filters compose - repeat one for or, mix different ones for and,
exclude with --exclude-* - and it's the same grammar whether you're
listing or mutating, on the command line or in the TUI.
Full field reference in docs/COMMANDS.md.
🔍 Watch, explain, script
qbit-ops status --watch
qbit-ops explain torrent --hash abc123
explain answers why a torrent or tracker is in the state it is, from
observed evidence - never a guess.
Read commands support machine-friendly output where it makes sense:
qbit-ops torrents list --format json
qbit-ops torrents stats --format json
qbit-ops status --format jsonl
🧭 How is qbit-ops different?
| qbit-ops | qbittorrent-cli | qbit_manage | qbittools | |
|---|---|---|---|---|
| Model | Safe operational toolkit | General-purpose qBittorrent CLI | Rules / background management | Task-oriented utilities |
| Interface | CLI + TUI | CLI | Config + scheduler + Web UI | CLI |
| Targeting | Composable selectors | Command / torrent specific | Rule / workflow specific | Command specific |
| Execution | SELECT → INSPECT → PLAN → APPLY | Direct qBittorrent operations | Apply configured rules | Run specialized commands |
| Safety | Dry-run-first, explicit apply | Command-dependent | Rule-driven automation | Command-dependent |
| Automation | Structured JSON + stable exit behavior | CLI / scripting | Scheduled workflows | CLI / scripting |
| Best fit | Safe bulk ops, inspection & scripting | General qBittorrent control from a terminal | Continuous library management | Specialized maintenance tasks |
qbit-opsis deliberately not a daemon or background rules engine: select precisely, inspect the scope, preview the plan, then apply it.
See PHILOSOPHY.md for the reasoning behind that design.
🖥️ TUI
The optional Textual interface provides an operational overview, torrent browsing, filters, details, explanations, and previewed low-risk bulk actions.
The overview answers "what is this machine doing" without reading a number: a live trace of transfer in both directions, per-tracker activity, and the instance's own counters.
qbit-ops tui
Filters use the same grammar as the CLI, but as a draft: set them across four panes - Organisation, State, Measures, Trackers - and nothing moves until you hit Apply, with the footer keeping count of what's pending, what's invalid, and what actually landed.
| Overview | Torrents |
|---|---|
| Search | Filters |
|---|---|
| Details | Preview before Apply |
|---|---|
Press ? inside the TUI to see the available controls.
🤖 Agent-ready (experimental)
An MCP server exposes a read-only, bounded view of your library, so an agent can explore it conversationally:
"What's the state of my library?" -> "Which torrents look stalled?" -> "Why is this one stalled?"
uv tool install "qbit-ops[mcp]"
Then point an MCP host at the stdio entry point - for Claude Desktop,
in its config file:
{
"mcpServers": {
"qbit-ops": {
"command": "qbit-ops-mcp"
}
}
}
It reads the same configuration the CLI does, so if qbit-ops status
works, this works.
Five tools, no mutation. Start broad with library_summary, narrow
with find_torrents or total a selection with aggregate_stats, then
settle on one torrent with inspect_torrent or explain_torrent --
nothing that pauses, deletes or edits anything. Answers stay small
whatever the library size, so a large one is explored by drilling down
rather than dumped whole.
Experimental and personal. This is a spike, not a supported surface: it may be extended, or removed. See docs/MCP.md.
🧩 Compatibility
Container integration is tested against a fixed set of exact qBittorrent releases - evidence for those versions, never a claim for a range. The matrix is in docs/COMPATIBILITY.md.
Run qbit-ops doctor to compare your instance with the packaged
evidence.
🧰 Commands
qbit-ops --help
qbit-ops --version
qbit-ops version
qbit-ops torrents --help
qbit-ops trackers --help
A compact command overview is available in docs/COMMANDS.md.
🗺️ Roadmap
See ROADMAP.md for where the project is heading.
🧑💻 Development
git clone https://github.com/LECOQQ/qbit-ops.git
cd qbit-ops
make install
make check-fast
make check
See CONTRIBUTING.md.
📄 License
MIT - see LICENSE.
Metadata
Release files for qbit-ops 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qbit_ops-0.6.0.tar.gz | 291.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qbit_ops-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 629.7 kB
Release files / qbit_ops-0.6.0.tar.gz
| Download URL | qbit_ops-0.6.0.tar.gz |
|---|---|
| Size | 291.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ac7d04bd739329b8f0053db2412bfa615f0ce81d2bf4e67c85e15e127a45a340
|
|
BLAKE2b-256 checksum How to use checksums |
07cfe7e47b01ba6e3d830bcaf29f347430ee98483e6a1fae144a8cbd2cbe3c79
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency logRelease files / qbit_ops-0.6.0-py3-none-any.whl
| Download URL | qbit_ops-0.6.0-py3-none-any.whl |
|---|---|
| Size | 338.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
952cc4824b7bb9f373336bfb4ecbaeb9cea78a2cb1dfb9d78e711c6d51a579ac
|
|
BLAKE2b-256 checksum How to use checksums |
5ab545e7619a3f737cc01faefb536acfb783f4997ace15940a290b95fb56a07a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency log