A calm BitTorrent client for your terminal. Search torrents, download with live progress, seed, and keep a history — in a fullscreen Bubble Tea UI.
Built on a full BitTorrent engine (anacrolix/torrent — DHT, magnets, web seeds, seeding), with a multi-source search layer and a fullscreen Bubble Tea UI.
Prebuilt binaries (GitHub Releases) — no Go toolchain needed. Download the archive
for your platform from the Releases page
— assets are named shoal_<version>_<os>_<arch>.tar.gz (.zip on Windows), where <os>
is linux/darwin/windows and <arch> is amd64/arm64 — unpack it and put the
shoal binary on your PATH:
tar -xzf shoal_0.10.0_darwin_arm64.tar.gz # pick the asset for your OS/arch
sudo mv shoal /usr/local/bin/ # or anywhere on your $PATH
shoalOr with the GitHub CLI (edit the pattern for your OS/arch):
gh release download --repo StrangeNoob/shoal --pattern 'shoal_*_darwin_arm64.tar.gz'Each release also ships a checksums.txt to verify the download.
Go install (needs Go 1.24+):
go install github.com/StrangeNoob/shoal/cmd/shoal@latestThis drops a shoal binary in your GOBIN (~/go/bin by default — make sure it's on
your PATH). Then just run shoal.
From source (needs Go 1.24+):
git clone https://github.com/StrangeNoob/shoal
cd shoal
make install # go install ./cmd/shoal → $GOBIN/shoal
# or, to build a local binary without installing:
make build # → ./shoal
go build -o shoal ./cmd/shoalgo.mod / go.sum are committed, so the build needs no setup step. (make deps only
exists to re-pin dependencies.)
Downloads land in ~/Downloads/shoal by default — change it in Settings → Save to.
shoal opens fullscreen on the Search pane. The footer always shows the keys for
the current pane, and ? opens the full list. tab cycles the four panes:
Search · Downloads · Seeding · Settings.
Search
| Key | Action |
|---|---|
/ |
focus the search box; type a query and press enter |
↑ ↓ / k j |
move the selection |
← → / h l |
narrow results by media type (All / Movies / TV / Anime / …) |
enter |
open the selected result's details |
d |
download the selected result |
S |
sort results (then ← → pick a column — Size / Seeders / Leechers / Ratio — and ↑ ↓ set direction) |
f |
filter the loaded results by title (type to narrow; esc clears — no re-query) |
z |
hide results with 0 seeders |
tab |
next pane · q / ctrl+c quit |
Results stream in live from all sources (searching… N/M sources) as a sortable table
(Name · Size · Seed:Lch). Press enter for a details screen (size, health, files,
hash, magnet) where d downloads, y copies the magnet, and esc goes back. You can
also paste a magnet link into the search box and press enter to add it directly.
Mouse: every pane is clickable. A single click selects a row (or, in
Settings, an option); a double-click opens it — Search → the result's
details screen, Downloads → the per-file details screen, Seeding →
the file/folder (same as o), and a file row inside the Downloads details
screen → toggles it (same as space). A right-click opens a context menu of
that row's actions, each labeled with its keyboard shortcut — click an item,
or ↑ ↓/enter/esc; clicking anywhere outside the menu dismisses it. The sidebar is clickable to switch panes, and the
scroll wheel moves the selection in any pane.
Downloads — each row shows the name, live progress bar, transferred / total, peers,
download speed, and an ETA (time remaining). Press enter for a details
screen showing per-file progress and trackers — each file has a [✓] (downloading) or
[ ] (deselected) checkbox. Use ↑ ↓ to move between files, space to toggle a file
(unchecked files stop downloading), and o to open the selected file directly. With a
Max active limit set (Settings),
downloads past the limit show ⏳ queued and start automatically as slots free (oldest
first); [ / ] move the selected download earlier / later in that queue. Select a
download with ↑ ↓ and press x to cancel it (a prompt lets you k keep the partial
files or d delete them; esc aborts). Press s to toggle sequential (streaming) piece
priority on the selected download — a ▶ sequential tag appears in its detail line, and the file
becomes playable (e.g. with shoal stream) well before the whole torrent finishes. When a
download finishes, shoal shows a notice and rings the terminal bell / posts a desktop
notification (toggle in Settings). If a download sits at 0 connected and 0 known peers for
more than 2 minutes, its detail line shows ⚠ no peers found — a source's seed count can
be stale, so this checks the engine's own announce results instead of trusting it.
Seeding — completed torrents you're still sharing (name, ratio, uploaded, peers, and upload speed), followed by a History of everything you've downloaded (name, size, and when it finished — persisted across runs).
Settings — theme (Twilight / Tide), color mode, save location, seed ratio, max peers,
listen port, download / upload speed limits (KiB/s; 0 = unlimited), max active
downloads (0 = unlimited), notify on done, and auto-update. ↑ ↓ move, ← →
change an option, enter edits a text field.
A SOURCES group lists every provider with an on/off toggle (← → to change);
turning one off removes it from searches immediately, and the change is shared with
the shoal sources CLI. A SUBTITLES group holds the OpenSubtitles API key
(masked), subtitle language, and auto-fetch toggle — see Subtitles.
Engine settings (speed limits, max peers, listen port, save-to,
seed) take effect when the daemon next restarts.
Settings rows are clickable too: click a row to select it, click one of an
enum row's ● options to set it directly, or click the ‹ › arrows on a
cycling value to change it — the same as ← →.
Default sources: the Internet Archive, a small open-media catalogue, and public indexes — FitGirl, YTS, The Pirate Bay, 1337x, EZTV, SolidTorrents, Nyaa, and SubsPlease. (EZTV has no keyword-search API, so it only appears in empty-query browse, not in a keyword search.)
Besides the fullscreen TUI, shoal has non-interactive subcommands so a script — or an
AI agent — can search and download without the UI. Run shoal with no arguments for the
TUI; with a subcommand for scripting. The listing commands (sources, search,
status, history) print an aligned, headered table for humans; add --json for
machine-readable output:
shoal sources # list providers with on/off state (add --json)
shoal sources enable <name> # turn a provider on
shoal sources disable <name> # turn a provider off
shoal search "big buck bunny" # search every source (--sort, --min-seeders, --json)
shoal download <magnet|url|infohash|id|file.torrent> # background download (--wait to block)
shoal download <target> --files '<glob>' # download only files matching a comma-separated glob
shoal download <target> --sequential # prioritize pieces for streaming (playable early)
shoal files <id> # list a download's files (# / ✓·✗ / SIZE / PATH)
shoal files <id> --only '<glob>' # select files matching a glob (deselect the rest)
shoal files <id> --json # list files for scripting
shoal sequential <id> on|off # toggle streaming piece priority on an existing download
shoal stream <id|magnet> # wait until playable, print the file path (add --files <glob>)
shoal subs <id> [--lang <code>] [--files '<glob>'] # fetch subtitles from OpenSubtitles
shoal status [id] # progress of background downloads (--json, --clear, --follow)
shoal history [--json] # list completed downloads
shoal history rm <id> [--delete-files] # remove a history entry
shoal history clear [--delete-files] # clear the history log
shoal pause <id> # pause a download (requires daemon)
shoal resume <id> # resume a paused download (requires daemon)
shoal remove <id> [--delete-files] # cancel/remove a download (requires daemon)
shoal open <id> # reveal download folder (works without a daemon)
shoal daemon status # show daemon status, uptime, and torrent counts
shoal daemon stop # stop the shared daemon
shoal completion bash|zsh|fish # print a shell completion script-
sourceslists all available providers with their on/off state (enable--jsonfor machines);enable <name>anddisable <name>toggle a provider. Toggles are shared with the TUI (stored inconfig.json) and take effect on the next search — no restart. A one-shot--source <name>still searches a provider even if it's disabled. -
searchqueries all sources concurrently and prints an aligned table (ID · TITLE · SIZE · SEED · LEECH · SOURCE, best-first), or a JSON array with the full magnet for each result via--json. Narrow to one provider with--source <name>, cap results with--limit <N>, drop thin results with--min-seeders <N>, and order with--sort seeders|size|leechers|name(defaultseeders). The shortIDfrom any row is what you pass todownload:ID TITLE SIZE SEED LEECH SOURCE 88594aaa Big Buck Bunny (2008) 1080p BluRay x264 723.0 MiB 412 18 YTS deadbeef big_buck_bunny_480p.mp4 158.0 MiB 3 0 Internet Archive -
downloadaccepts a magnet, a.torrentURL, a 40-char infohash, a short id from your lastsearch, or a path to a local.torrentfile (converted to a magnet client-side, so it works without a new daemon call). It starts the download in the background and returns immediately with a handle; files land in shoal's configured folder (Settings → Save to, orconfig.json). CLI downloads run in a shared backgroundshoal daemon(started automatically on the firstdownload), so multiple downloads andshoal statusall share one engine and one download folder. The TUI runs on the same sharedshoal daemonas the CLI, so downloads and seeding stay in sync between them. Engine settings (listen port, max peers, save-to, seed) configure the daemon and take effect when it restarts. Pass--waitto block until the download completes — it prints a live progress line and exits0on completion (non-zero if the daemon becomes unreachable), so a script doesn't have to pollstatus. Add--files '<glob>'to download only files matching a comma-separated glob pattern (e.g.--files '*.mkv,*.srt') — applied once metadata arrives, so it works for magnets; not yet supported for.torrent-URL targets. Add--sequentialto bias piece order for streaming instead of rarest-first — like--files, this only applies to magnet targets today (a.torrent-URL download prints a note and continues without it); useshoal sequential <id> onafterward for those. -
fileslists a download's files as a table (# / ✓·✗ / SIZE / PATH), with one row per file. Add--only '<glob>'to select files matching a glob pattern and deselect the rest (format: comma-separated patterns, case-insensitive, matched against full path and basename, same as--files). Add--jsonfor machine-readable output (scripts). -
sequentialtoggles sequential (streaming) mode on an existing download:shoal sequential <id> on|off. It's a priority overlay, not a strict in-file-order download: each selected file's first and last pieces plus a rolling early window are prioritized, so the file becomes playable long before the torrent finishes, while the remainder still backfills normally (rarest-first). Pieces are hash-verified exactly as they always are — nothing about integrity changes, only the order pieces are asked for. -
streamis the one-shot way to watch something while it downloads:shoal stream <id|magnet>resolves the target (an existing download, or a fresh magnet), turns on sequential mode, waits for its target file to become playable (first ~8 MiB plus the last piece complete — picked by--files '<glob>'if given, else the largest video file, falling back to the largest file overall when no file has a video extension; files being downloaded win over deselected ones), then prints its absolute path to stdout and exits — the download keeps going in the daemon. Progress goes to stderr, so the canonical use ismpv "$(shoal stream <id>)". -
subsfetches subtitles from OpenSubtitles for a downloaded torrent's video files, printing each written.srtpath. Requires an OpenSubtitles API key (Settings → OS API key, oropensubs_api_keyinconfig.json) — see Subtitles below for how to get one.--lang <code>overrides the configured Subs lang for this run;--files '<glob>'picks specific files (comma-separated glob, same matching rules asdownload/files) instead of the default rule (video files ≥100 MiB). -
statusreports each background download asdownloading | done | seeding | paused;--clearprunes finished (done) torrents, keeping files.--followredraws the table live until you press Ctrl+C. -
historylists all completed downloads (--jsonfor scripts).history rm <id>removes a history entry and optionally deletes its files (--delete-files).history clearwipes the entire history log and optionally deletes all files (--delete-files). By default, deletes are records-only; files stay on disk. History deletion is also available in the TUI (Seeding pane,xon a history row). -
pause,resume,remove— control live downloads. Use an infohash prefix fromshoal statusas<id>. These commands require a running daemon (they don't auto-start it).removecancels a download or stops seeding; by default it keeps files (--delete-filesto delete them too). -
openreveals a download's folder in your file manager, or prints the path if no manager is available. It works without a daemon: it resolves the folder from history (or the data dir) when no daemon is running, falling back to the live daemon's path if one is up. -
daemon status— show whether the shared daemon is running, its uptime, and torrent counts. -
daemon stop— stop the shared daemon (it also stops on its own when idle). -
completion <shell>— print a completion script forbash,zsh, orfish. It completes subcommands and the id-taking commands complete infohash prefixes from your livestatusandhistory. Install it, e.g.:shoal completion zsh > "${fpath[1]}/_shoal" # zsh shoal completion bash > /etc/bash_completion.d/shoal # bash (or source it from ~/.bashrc) shoal completion fish > ~/.config/fish/completions/shoal.fish # fish
The shared daemon (and the TUI/CLI that use it) run on Linux, macOS, and Windows 10 1803+ (which is where Windows gained AF_UNIX support). Windows support is best-effort.
The daemon auto-starts on the first download or when the TUI launches. If the
daemon stops or crashes while the TUI is open, the TUI shows a reconnecting
indicator until the daemon comes back and automatically restarts it, so downloads
resume. (To fully stop the daemon, close the TUI first, then shoal daemon stop.)
The daemon also auto-stops when it's been empty (no torrents) with no connected
clients for daemon_idle_minutes (default 10; set 0 in config.json to
disable). A torrent or any connected client keeps it running.
shoal can fetch subtitles from OpenSubtitles for
downloaded video files, via the CLI (shoal subs) or automatically after a download
completes. You bring your own API key — shoal doesn't ship one. Get a free key from
your OpenSubtitles.com account under API Consumers, then set it in Settings → OS
API key:
- OS API key — your OpenSubtitles API key (masked in the settings pane; the last 4
characters stay visible so you can tell keys apart, empty shows
unset). Stored inconfig.jsonasopensubs_api_key. - Subs lang — a cycling selector over ~16 common languages (default
en/ English);←/→step through them,entertypes any other code by hand (for a language outside the list, e.g.he, shown ashe (custom)). Stored assubs_langinconfig.json.shoal subs --lang <code>overrides it for a single run. - Auto subs — when on, the daemon fetches subtitles automatically for
video files (≥100 MiB) once a download completes;
subs_autoinconfig.json.
Auto subs is an engine setting: like other engine settings, a change takes effect
the next time the daemon restarts. shoal subs reads the key/language settings live, so
CLI runs pick up changes immediately without a restart.
This repo ships a Claude Code skill
(.claude/skills/shoal-download/) that teaches Claude to drive those commands — so you
can just say "find and download Big Buck Bunny" and it searches, picks the best match,
downloads in the background, and reports progress.
No registration or config is required — Claude Code discovers skills from the filesystem. Two ways to use it:
-
Inside this repo: run
claudefrom a shoal checkout and the project-scoped skill in.claude/skills/loads automatically — nothing to install. -
Everywhere (recommended): with the
shoalbinary on yourPATH(see Install), run:shoal skill install # → ~/.claude/skills/shoal-download/SKILL.mdThe skill is embedded in the binary, so this works offline and installs the version that matches your
shoal. Restart Claude Code once (a brand-new skills directory is picked up on next launch), then ask it to find and download something. Re-run with--forceafter ashoal updateto refresh the installed skill.
The skill is plain Markdown with name/description frontmatter: Claude loads that
summary at startup and reads the full instructions on demand when your request matches.
See the Claude Code skills docs for the full
mechanics.
shoal can update itself from GitHub Releases:
shoal update # fetch the latest release, verify its checksum, replace the binary
shoal version # print the installed versionshoal update downloads the archive for your OS/arch, checks its SHA-256 against the
release's checksums.txt, and swaps the binary in place (restart to use it). On release
builds, a launch-time check also shows a ↑ vX available hint in the header. Turn on
Settings → Auto-update to have shoal apply new releases automatically on launch.
shoal/
├── bencode/ # .torrent / tracker bencoding
├── metainfo/ # parse a .torrent, compute the infohash
├── tracker/ # HTTP tracker announce
├── peer/ # peer handshake, wire messages, bitfield
├── download/ # concurrent verified piece downloader
├── internal/
│ ├── source/ # Source interface + default provider set
│ ├── engine/ # Engine interface (anacrolix/torrent backend)
│ ├── history/ # persisted download history
│ ├── config/ # persisted user settings
│ └── ui/ # the fullscreen Bubble Tea interface
└── cmd/
├── shoal/ # the TUI (main binary)
└── shoal-classic/ # a standalone CLI downloader
The UI depends only on the source.Source and engine.Engine interfaces, so the engine
can be swapped without touching the UI. The core packages have offline unit tests.
make run # build and launch the TUI
make test # run the unit tests (offline)
make vet # go vet
make fmt # gofmt -w .
make classic # build the CLI downloader (./shoal-classic)
make help # all targetsShipped:
- Shared-engine daemon — the TUI and CLI drive one background download engine,
so downloads, seeding, and status stay in sync live between them (
shoal daemondaemon stop/status, idle auto-shutdown, self-healing reconnect; Linux, macOS, and Windows 10 1803+).
- Full command-line client — the TUI's actions from the terminal:
search,download,status,history(list +rm/clear),pause,resume,remove, andopen. History deletion works from the TUI too. - Pause / resume with a persisted download queue across restarts.
- Per-source toggles in Settings, shared with the
shoal sourcesCLI. - Self-update —
shoal updateplus opt-in Auto-update. - Tabular CLI output —
sources,search,status, andhistoryprint aligned, headered tables (--jsonstill available for scripts). shoal download --wait— block until the download completes with a live progress line and a meaningful exit code, so scripts don't have to pollstatus.- Search
--sort/--min-seeders— order results by seeders/size/leechers/name and drop thin ones, the sorting the TUI already has, for scripts. shoal status --follow— live-redrawing status table without the full TUI.- Local
.torrentfiles —shoal download ./file.torrent(converted to a magnet client-side). - Shell completions —
shoal completion bash|zsh|fish, with infohash-prefix completion sourced from livestatus/history. - Speed limits — global download/upload rate caps (Settings /
config.json, KiB/s). - ETA column in the Downloads pane, and a completion notification (in-app notice
- terminal bell / OSC 9 desktop notification, toggleable).
- Mouse support — click a row (or Settings option) to select it, double-click to open it (Search → details, Downloads → per-file details, Seeding → open), right-click for a context menu of that row's actions with their keys; the sidebar and Settings are clickable too, and the wheel scrolls the selection in every pane.
- Search quality-of-life — a hide-0-seed toggle (
z) and an in-results filter (f) that narrows loaded results without re-querying sources. - Queue controls — a max-concurrent-downloads limit (extras queue and promote as
slots free), with
[/]to reorder the queue manually. - Active-download details —
enteron a Downloads row shows per-file progress and the tracker list. - Per-file selection — TUI checkboxes in the details screen,
shoal download --files '<glob>', andshoal files <id>to list/select files. - Daemon socket hardening — the socket dir is verified 0700 & user-owned (rejecting a
symlink or a foreign owner) before binding; falls back to
XDG_RUNTIME_DIR. - CI + releases — checks on every push/PR and tag-triggered GoReleaser binaries.
- Streaming — per-torrent sequential piece order (
sin the Downloads pane,shoal download --sequential,shoal sequential <id> on|off) so a file becomes playable long before the torrent finishes, plusshoal stream <id|magnet>to wait for playability and print the file path (mpv "$(shoal stream <id>)"). - Subtitles —
shoal subs <id>fetches matching.srtfiles from OpenSubtitles (bring your own free API key), plus an opt-in Auto subs engine setting to fetch automatically on download completion.
Still planned — contributions welcome:
- More sources behind the existing
source.Sourceinterface. - Homebrew tap as an additional install channel.
BitTorrent itself is neutral infrastructure — Linux distributions, game patches, and large open datasets are all distributed this way. What carries legal risk is the content and the indexing sites, not the protocol. shoal can search general torrent indexes by default; use it only for content you have the right to download and share.
MIT — do what you like.
