How to Use OpenDrop CLI for Automated File Transfers

  1. The Setup: Why I Wanted Headless Transfers
  2. First Attempt: Running the Server Without a Screen
  3. Sending Files from the Command Line
  4. The Device Registry and What Tripped Me Up
  5. Wiring It Into a Cron Job
  6. Where the CLI Falls Short (For Now)

The Setup: Why I Wanted Headless Transfers

I have an old HP laptop running ParrotOS that sits in a closet. It collects log files, runs a few background services, and has no monitor connected. Moving files off it used to involve either SSH and scp, or plugging in a monitor to use the GUI. Neither option was great for routine, scheduled transfers.

The OpenDrop GUI works well on my Windows desktop and MacBook, but launching a full PySide6 window on a headless Linux box isn't practical. I needed something that could run from a terminal, accept scripted input, and exit cleanly when done. That's what the CLI subcommands are for.

OpenDrop's CLI has four main subcommands:

  1. opendrop serve — run the server without the GUI
  2. opendrop send <file> --target="Device Name" — send a file to another device
  3. opendrop devices — list known connected devices
  4. opendrop register-context-menu / unregister-context-menu — add or remove the right-click "Send with OpenDrop" option on your OS

Running opendrop with no arguments launches the GUI, same as double-clicking the app. The CLI subcommands skip the GUI entirely and work in the terminal.

First Attempt: Running the Server Without a Screen

My first try was simple. SSH into the ParrotOS laptop, run opendrop serve, and see what happens.

The command started the FastAPI server on 0.0.0.0:8000 in a background thread, then kicked off the Cloudflare tunnel. Within a few seconds, the terminal printed the local server address and started waiting for the tunnel URL. After about 10-15 seconds, the tunnel connected and printed both the public URL and the shared secret.

The output looked roughly like this:

OpenDrop server starting on 0.0.0.0:8000...
Server running on http://0.0.0.0:8000
Waiting for tunnel... (Press Ctrl+C to stop)
Tunnel connected: https://some-random-words.trycloudflare.com
Secret: a1b2c3d4...

The server stayed running until I hit Ctrl+C. Clean shutdown, tunnel disconnected, process exited. First try worked. I was suspicious.

Then I closed the SSH session. The process died. Of course. I hadn't backgrounded it or used a session manager. Running opendrop serve in a tmux session or behind nohup fixed that. A systemd service file would be cleaner for production use, but tmux was fast for testing.

On the receiving end (my Windows desktop, same network), the ParrotOS server showed up automatically via mDNS. The service type _opendrop._tcp.local. on port 8000 let the GUI find it without any manual configuration. I could send files to the headless box from my phone or desktop just by selecting it in the app.

Sending Files from the Command Line

Receiving files on a headless server is the easy part. Sending files is where the CLI earns its keep.

The send command takes a file path and a target device name:

opendrop send /path/to/backup.tar.gz --target="My Windows PC"

The target name needs to match a device the CLI knows about. I'll get to how device resolution works in a moment, because it's where I spent the most time debugging.

When the send works, the output is straightforward. It prints the filename, file size in MB, creates a session with the target server, uploads the file, and prints a success message. The whole thing runs as a single blocking operation and exits when done. No daemon, no background process. Start, transfer, exit.

The upload itself uses HTTP POST to the target's /upload endpoint. For LAN transfers, this is a direct connection to the target's port 8000. The file goes as a multipart form upload with metadata (filename, chunk index, total chunks). For a single-shot CLI transfer, it's one chunk with chunk_index=0 and total_chunks=1. The transfer has a 10-minute timeout, which is generous enough for most files over LAN.

Where I hit a wall: the --target flag expects a device name, not a URL or IP address. If the CLI doesn't recognize the name you give it, it prints an error and lists available devices. My first few attempts failed because I was typing the device name slightly wrong. The name matching is exact.

The Device Registry and What Tripped Me Up

OpenDrop's CLI resolves target names through a device registry that combines local mDNS discovery results with cloud-synced device information. When you run opendrop devices, it shows you everything the system knows about: device name, URL, and whether the device was found locally or via cloud.

The output looks something like:

Name               URL                                 Source
My Windows PC     http://192.168.1.50:8000            local
MacBook Pro       https://xxx.trycloudflare.com     cloud

The mistake I kept making: I assumed the device name would match whatever I'd set in the GUI's settings. It does, but only after the device has been discovered or synced. On a fresh install, the device list is empty. You need to either have both devices on the same network (so mDNS can find them) or be signed in with a Pro account (so cloud device sync works).

My ParrotOS box wasn't signed in. It was running opendrop serve headlessly. The device registry only had local mDNS entries. Since my Windows desktop was on the same network, it appeared in the list. But my iPhone on cellular didn't show up, because there was no cloud sync without authentication.

For a pure LAN scripting setup, this works fine. Both devices are on the same network, mDNS handles discovery, and the CLI can resolve target names. For remote scripted transfers, you'd need to sign in via the GUI first to populate the cloud device registry, then use the CLI afterward.

Tip

Run opendrop devices before trying to send. It confirms whether the target device is visible and shows you the exact name to use with the --target flag. Typos in device names are the most common reason CLI sends fail.

Wiring It Into a Cron Job

The real goal was automated, scheduled transfers. Every night at 2 AM, push the day's log files from the ParrotOS box to my Windows desktop.

The cron job itself is simple:

0 2 * * * /usr/local/bin/opendrop send /var/log/daily-report.tar.gz --target="My Windows PC" >> /var/log/opendrop-sync.log 2>&1

But getting it to actually work required dealing with a few things cron does differently than an interactive shell.

First, environment variables. Cron doesn't source .bashrc or .profile. If OpenDrop relies on any environment variables (like XDG_CONFIG_HOME for finding its config directory on Linux), you need to set them explicitly in the crontab or in a wrapper script. On my ParrotOS box, the default config path is ~/.config/opendrop/, which worked because $HOME is set in cron. But XDG_CONFIG_HOME wasn't, so if I'd set a custom config path, it would have silently fallen back to the default.

Second, the target device needs to be online when the cron job fires. If my Windows desktop is asleep at 2 AM, the CLI send fails with a connection error. I added a simple retry wrapper:

for i in 1 2 3; do opendrop send /var/log/daily-report.tar.gz --target="My Windows PC" && break || sleep 60; done

Three attempts, one minute apart. If the desktop is set to wake briefly for network access (Windows has this option in power settings), the second or third attempt usually succeeds.

Third, logging. Redirect stdout and stderr to a log file. When something fails at 2 AM and you don't notice until morning, the log is your only diagnostic tool. The CLI prints specific error messages for connection failures, unknown targets, and file-not-found errors, so the logs are usually enough to diagnose the problem.

A more complete wrapper script might look like this:

#!/bin/bash
export HOME=/home/myuser
LOG=/var/log/opendrop-sync.log
FILE=/var/log/daily-report.tar.gz
TARGET="My Windows PC"

echo "$(date): Starting transfer" >> $LOG
for i in 1 2 3; do
  /usr/local/bin/opendrop send "$FILE" --target="$TARGET" >> $LOG 2>&1 && break
  echo "$(date): Attempt $i failed, retrying in 60s" >> $LOG
  sleep 60
done

Where the CLI Falls Short (For Now)

The CLI covers the core use case: send a file to a known device, run a headless server, list devices. But there are gaps.

There's no opendrop receive command that waits for a single incoming file and exits. The serve command runs indefinitely, which is fine for a persistent server but not ideal for a one-shot "accept the next transfer and quit" workflow. You'd need to monitor the receive directory for new files and handle that externally.

Wildcard or multi-file sends aren't built in. If you want to send everything in a directory, you write a shell loop: for f in /path/to/files/*; do opendrop send "$f" --target="Name"; done. Each send is a separate session and upload, which is fine for a handful of files but adds per-file overhead for large batches. Wrapping the files in a tar archive before sending is usually faster.

There's no progress bar in the CLI output. For large files over slower connections, the command blocks silently until it finishes or times out. Adding --verbose output with transfer progress would make debugging easier, especially in unattended scripts where you're reading logs after the fact.

Despite those limitations, the CLI does what I originally needed: move files between machines on a schedule without opening a GUI. The serve command turns any Linux box into a headless file receiver. The send command plugs into bash scripts, cron jobs, and CI pipelines. For my ParrotOS-in-a-closet setup, it works reliably. OpenDrop handles the networking, mDNS discovery, and transfer protocol so the scripts stay short.

Automate Your File Transfers with OpenDrop CLI

OpenDrop's command-line interface works on Windows, macOS, and Linux. Run headless servers, script file sends, and integrate transfers into your existing workflows.

Download OpenDrop Free