Pull a file from a URL into a temp directory on a remote host and get its path back.
Written to hand screenshots to terminal AI agents running over SSH, but it takes any file type. One script, no dependencies beyond a stock server image, nothing left running.
A terminal agent (Claude Code, Codex, opencode) running on a remote host over SSH can read any file on that host, but has no access to the clipboard on your workstation. Pasting an image does nothing: the clipboard and the process live on different machines.
Every workaround falls into one of three shapes:
| Shape | Requires on the remote host |
|---|---|
| Clipboard shim + reverse tunnel | a PATH shim intercepting xclip, a persistent tunnel |
| Terminal clipboard protocol (OSC 5522) | a specific terminal emulator, passthrough config |
| Signed URL + fetch | curl |
dl is the third shape. Nothing is installed on the remote host beyond this script, no
inbound port is opened, no credential is stored there, and no process runs in the
background. The trade is one extra step at the keyboard.
mkdir -p ~/.local/bin
curl -fsSL https://github.com/banochkin/dl/releases/latest/download/dl -o ~/.local/bin/dl
chmod +x ~/.local/bin/dlMake sure ~/.local/bin is on PATH:
grep -q '.local/bin' <<< "$PATH" || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrcPin a version instead of tracking latest by swapping latest/download for
download/v1.0.0.
Two forms. The prompt is the one to build a habit around; the argument is there for scripting and for pasting into a session where a prompt would be awkward.
$ dl
url: <paste><Enter>
######################################################## 100.0%
image/png, 84K
/tmp/dl/SCR-20260729-abcd.png
$ dl 'https://example.com/a.png?sig=…'
/tmp/dl/a.png
Hand the printed path to the agent, or use it directly:
unzip "$(dl)"
wc -l "$(dl)"dl -V print version
dl -h print usage
- stdout carries the resulting path and nothing else, so
$(dl)is safe in substitution. - stderr carries the prompt, the progress bar, a
type, sizeline, and warnings. - exit 0 success, exit 1 download or input failure, exit 2 bad usage.
On any failure the partial file is removed. There is never a stale file left behind that could be mistaken for a fresh one.
The prompt is preferred over the argument. Signed URLs contain &, so an argument
that slips through unquoted gets torn apart by the shell and sent to background. An
argument also lands in ~/.bash_history, where a signed URL outlives the object it points
at. read does no shell parsing and leaves no trace, so the URL is taken verbatim. Both
forms are supported because the argument is genuinely useful in pipelines, but reach for
the prompt by default.
Filenames come from the URL. The query string and fragment are stripped, the basename
is kept, and anything outside [A-Za-z0-9._-] collapses to _. A screenshot named
SCR-20260729-abcd.png on the capture side arrives under the same name. On collision a
-$RANDOM suffix is inserted before the extension instead of overwriting.
Failures are loud. A text/html response usually means an expired signature or a login
wall rather than the file you wanted, so it is reported even though the download
technically succeeded. Silent success on the wrong bytes is the worst failure mode here:
an agent will happily reason about whatever it was given.
Permissions. The directory is 700, files are created under umask 077, so 0600.
This matters on shared hosts where /tmp is world-readable.
Self-cleaning. Files older than a day are purged on the next run. No cron, no timer.
bash 4+, curl, file, find, du, coreutils. Present on any stock server image.
dl is only the receiving half. For turning a macOS screenshot into a short-lived signed
URL backed by private object storage, see docs/shottr-r2.md.
MIT. See LICENSE.
banochkin.com DAO <mail@banochkin.com>