tailtoss
v0.4.1
Published
A terminal UI for Tailscale's Taildrop — send and receive files over your tailnet with the tailscale CLI.
Maintainers
Readme
English · 한국어
Tailtoss
A clean terminal UI (TUI) for Tailscale's Taildrop.
Built for people who send/receive files with the open-source tailscale CLI instead of a GUI client.
Pick files, choose a target device, and receive — all from the keyboard.
Send/receive files over your tailnet from the terminal — file browser, device picker. Command:
tailtoss.
🚀 Tailtoss tailscale file transfer › Menu
──────────────────────────────────────────────
What would you like to do?
Receive folder: /Users/you/Downloads
❯ 📤 Send files
📥 Receive files
⚙️ Settings
❔ Help
🚪 Quit
──────────────────────────────────────────────
↑↓ move · Enter select · Ctrl+C quitWhy Tailtoss
Taildrop is Tailscale's built-in way to move files between your own devices — no cloud, no
accounts. Tailtoss exists to make it easy to actually use: a keyboard-driven terminal
UI over the tailscale CLI, so you don't have to memorize tailscale file cp flags or dig
up device names. Setup stays light — Node plus the tailscale CLI you already run, nothing
more.
On top of the basics, it adds things neither the official CLI nor the GUI offers:
- Folder sending — folders are zipped automatically before transfer (the CLI sends only files)
- Free-form multi-select — pick any mix of files and folders in one pass
- Pre-send name checks — names the receiver would reject (leading/trailing spaces, over-long names, forbidden characters) are caught before sending, telling you which file and why, instead of the bare "transfer failed" the official tools give you
Features
- Send files — pick from the built-in file browser → choose an online device → transfer
- Single file → sent as-is
- Multiple files → send each separately or bundle into one zip (chosen in settings)
- Folder → always compressed to
.zipbefore sending - Target list is auto-populated via
tailscale file cp --targets(online, receivable devices only)
- Receive files — save files from the staging inbox into your downloads folder
- macOS (Homebrew tailscaled): usually saved as your user with no sudo or auth needed — but if a permission error occurs, follow the
sudocommand shown on screen - Linux and other permission-restricted setups: the required
sudocommand is shown on screen - Name collisions are auto-numbered via
--conflict=rename
- macOS (Homebrew tailscaled): usually saved as your user with no sudo or auth needed — but if a permission error occurs, follow the
- Settings — downloads folder · language · multi-file transfer mode (all persisted for next run)
- Help — usage instructions available right on screen
Supported platforms
| Platform | Status | Notes |
| ----------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| macOS | ✅ Fully supported | Receiving usually needs no sudo (Homebrew tailscaled); if a permission error occurs, the sudo command is shown |
| Linux | ✅ Supported | Receiving may need sudo to access the root-owned inbox (command is shown) |
| Windows | ⚠️ Experimental | Paths/commands are handled, but testing focuses on macOS & Linux. Folder/bundle sends rely on the built-in tar.exe (Windows 10 1803+); this path is untested on real hardware |
Requirements
This tool targets the open-source
tailscale/tailscaled(CLI + daemon). It drives whatevertailscalebinary is on yourPATH, and the primary audience runstailscaledthemselves — the Homebrew build on macOS (brew install tailscale), or a native Linux install. ThetailscaleCLI bundled with the official macOS GUI client has also been confirmed to work here; that path is not guaranteed, and other platforms and distributions are untested.tailscaledusers are the intended target.
Node.js 18+
tailscaleCLI (open-source build, withtailscaledrunning) — if missing, the app shows install guidance and exits- macOS:
brew install tailscale, thensudo brew services start tailscaleto runtailscaledas a launchd service (auto-starts at boot — no need to launch it by hand), andsudo tailscale upto log in. See Tailscaled on macOS. - Linux: https://tailscale.com/download/linux
- macOS:
An archiver — only needed for folder transfers / bundling multiple files into a zip. Sending a single file never needs one. Either of these works, and the app picks whichever it finds:
zip(Info-ZIP) — bundled with macOS; on Linuxsudo apt install zip, etc.bsdtar— libarchive'star, which writes.zipdirectly. Bundled with macOS, and with Windows 10 1803+ / 11 astar.exe. Note that GNU tar cannot write zip, so a Linux box with only GNU tar still needszip.
If neither is found the app warns on the menu (it does not block) and Settings › Check for archiver re-detects after you install one — no restart needed.
npm package dependencies (
ink,react,execa, etc.) are handled bynpm install. The required runtime tool (tailscale) is checked automatically at startup.
Installation
Install from npm:
npm install -g tailtossThen run it from any directory:
tailtossTo update later, run npm install -g tailtoss@latest.
From source (for development)
git clone https://github.com/dooee/tailtoss.git
cd tailtoss
npm install # install dependencies
npm run build # compile to dist/
npm install -g . # register the global `tailtoss` command
npm linkworks too and gives you the exact same globaltailtoss— only the linking differs:npm install -g .installs a copied snapshot of the build, whilenpm linksymlinks this working directory. So if you plan to edit the source, prefernpm link: afternpm run buildyour changes show up intailtosswith no reinstall.
Usage
Run tailtoss → move through the menu with the arrow keys, select with Enter.
📤 Send files
File browser controls:
| Key | Action |
| ------- | ------------------------------------------------------------------------ |
| ↑ ↓ | Move between items |
| → | Enter folder |
| ← | Parent folder |
| Space | Select / deselect (multiple allowed) |
| Enter | Start transfer (if nothing is selected, sends the item under the cursor) |
| . | Toggle hidden files |
| Esc | Cancel |
Transfer rules:
- Single file → sent as-is
- Multiple files → send each separately or bundle into one zip, per settings
- Folder → always compressed to zip before sending
Then pick the target device from the list and it's sent.
📥 Receive files
Choosing Receive files pulls files from the inbox into your downloads folder.
In a headless tailscaled setup, incoming files do not pop up automatically like the GUI app. Run
Receivewhen you want them. If a permission error occurs (Linux, or occasionally macOS depending on your setup), the requiredsudocommand is shown on screen.
⚙️ Settings
- Change downloads folder — navigate the folder browser, then
Enterto select that folder - Language — English / Korean
- Multi-file transfer mode — send each separately / bundle into one zip
Settings are saved to the paths below and persist across runs.
| Platform | Config file |
| -------- | --------------------------------------------------------------------- |
| macOS | ~/Library/Application Support/tailtoss/config.json |
| Linux | $XDG_CONFIG_HOME/tailtoss/config.json (default ~/.config/...) |
| Windows | %APPDATA%\tailtoss\config.json |
Development
npm run dev # run the source directly with tsx
npm run build # compile to dist/ + set executable bit
npm run start # run the build outputProject structure
src/
├── cli.tsx # entry point (shebang) · renders after checking the tailscale dependency
├── app.tsx # screen routing + language context
├── config.ts # settings load/save (per-platform paths)
├── i18n.ts # UI strings · help · context
├── tailscale.ts # listTargets / sendPaths / receive · CLI detection
├── zip.ts # zip compression (Info-ZIP or bsdtar) for folders / multiple items
└── components/
├── Frame.tsx # shared frame (title bar + horizontal rule + help)
├── MainMenu.tsx
├── FileBrowser.tsx # arrow-key navigation + multi-select (files/folder modes)
├── TargetPicker.tsx # pick a target from the --targets list
├── Send.tsx · Receive.tsx · Settings.tsx · Help.tsxTroubleshooting
tailscale CLI not found— install tailscale and confirmtailscale statusworks. It's auto-detected from/opt/homebrew/bin,/usr/local/bin,/usr/bin, andPATH.- Receive fails on Linux (permissions) — run the
sudo tailscale file get ...command shown on screen. No archiver foundon folder/bundle transfers — installzip(or use a system withbsdtar), then re-detect via Settings › Check for archiver. Sending a single file
works without any archiver.- When using nvm — the global install lives in the current Node version's bin, so after
nvm usere-runnpm link(ornpm install -g .) on that version.
Notes
Tailscale is an open-source, WireGuard-based mesh VPN that links your devices into a private network (a tailnet). Taildrop is its built-in device-to-device file transfer feature.
Tailtoss is an independent, unofficial front-end — not affiliated with or endorsed
by Tailscale Inc. It isn't a full CLI wrapper: it drives only the Taildrop file-transfer
commands (tailscale file cp / file get) behind a TUI, leaving the rest of the CLI
alone. It shells out to a tailscale binary you install yourself, so without it the app
cannot run — and it says so at startup.
License
MIT
