Move a file, a folder, some text, or a URL between your laptop and your phone over wifi.
Scan a QR code in your terminal, and the transfer happens in the phone's browser.
No app to install, no account, no cloud.
$ qrshare report.pdf # phone scans, phone downloads the file
$ qrshare ./photos # phone downloads the folder as a zip
$ qrshare # phone uploads files back to your laptop
$ qrshare "some text" # phone shows the text with a copy button
$ echo $API_KEY | qrshareBoth devices sit on the same wifi, so the phone reaches your laptop's LAN address directly. Nothing leaves the local network. There is no relay, no NAT traversal, and no third party in the middle.
- Nothing to install on the phone. The phone only needs a camera and a browser, which it already has.
- No cloud, no account. Bytes go straight from one device to the other over your wifi.
- Four ways to move things. Send a file, send a folder as a zip, receive uploads from the phone, or share text and links.
- It goes both ways. Push a file to your phone, or pull photos off it, from the same tool.
- Big files are fine. Transfers stream, so a 2 GB video never loads into memory on either side.
- The URL is a secret. Every share lives behind an unguessable token. Anyone who did not scan the QR gets a plain 404.
- One self-contained binary. Written in Go, no runtime, no dependencies to chase.
brew install edaywalid/tap/qrshareThis also installs shell completions for you.
go install github.com/edaywalid/qrshare/cmd/qrshare@latestGrab the archive for your platform from the releases page, unpack it, and move qrshare onto your PATH.
git clone https://github.com/edaywalid/qrshare
cd qrshare
make build # binary lands in ./bin/qrshareGo 1.25 or newer is required to build.
Run one of the four modes, then point your phone's camera at the QR. The URL prints under the code as a fallback in case the QR will not scan.
qrshare report.pdfThe file downloads on the phone with its original name. Large files stream from disk, so memory use stays flat no matter the size. The server shuts down once the download finishes, unless you pass --keep-alive.
qrshare ./trip-photosThe folder is zipped on the fly and streamed to the phone as a single archive. Nothing is written to a temp file first.
qrshare
qrshare --dir ~/Downloads --keep-aliveScanning opens an upload page. Files land in the current directory, or wherever --dir points, and an existing file is never overwritten. The phone shows a live progress bar while the upload runs.
qrshare "https://long.example.com/link?token=abc"
echo secret | qrshareThe phone shows the text with a one-tap copy button. When the text is a link, it also gets an "Open link" button.
The served pages match the qrshare look: a text-mode blue with cyan and yellow, built to be fast and readable on a phone held one-handed. The upload page also has a text box that sends a note or link straight back to your laptop's clipboard.
your laptop your phone
┌────────────┐ same wifi (LAN) ┌────────────┐
│ qrshare │ ◀───────────────────▶ │ browser │
│ :49213 │ http + secret token │ + camera │
└────────────┘ └────────────┘
│ ▲
└────────── prints a QR ────────────┘
phone scans it
qrshare finds your real wifi address, binds a port, and mints a random token that becomes part of the URL. It prints that URL as a QR code in the terminal. Your phone scans it, opens the URL over plain HTTP on the LAN, and the transfer runs directly between the two devices.
qrshare picks the mode for you based on the argument:
- an existing file path sends the file
- an existing directory sends a zip
- piped stdin is shared as text
- any other argument is shared as text
- no argument at all receives uploads
Force a mode with --send, --receive, or --text when the guess would be wrong. An argument that looks like a path but does not exist is treated as a likely typo and errors out, rather than being shared as text by accident.
--send treat the argument as a path to send
--receive receive uploads from the phone
--text treat the argument or stdin as literal text
--port N listen port (default: auto-pick a free port)
--host ADDR override the resolved LAN IP
--dir PATH where uploads are saved (default: current dir)
--max-size SIZE reject uploads larger than SIZE, e.g. 500M, 2G
--keep-alive stay up for multiple transfers
--timeout DUR idle shutdown after DUR (default: 10m)
--qr-only print only the QR code
--url-only print only the URL, no QR
-q, --quiet suppress status output
--version
--help
qrshare completion bash > /usr/local/etc/bash_completion.d/qrshare
qrshare completion zsh > "${fpath[1]}/_qrshare"
qrshare completion fish > ~/.config/fish/completions/qrshare.fishThe Homebrew install wires these up automatically.
Transfers are plain HTTP over your LAN, guarded by an unguessable random token in every URL. Anyone who does not scan the QR, or know the exact URL, gets a 404 with no hint that a server is even running. Uploads are confined to the target directory with sanitized filenames and no overwrite. The server accepts connections only while the process is running. Because this is a same-wifi tool, there is no cloud endpoint and no account to secure.
macOS asks to allow incoming connections. Expected. The first time qrshare binds a port, the macOS firewall prompts you. Click Allow. This is not an error.
The phone scans the QR but nothing loads. You are most likely on a guest or "client isolation" network, which blocks devices from reaching each other even on the same wifi. Switch both devices to a normal home network or a personal hotspot.
The QR shows an address my phone cannot reach. If you are on a full-tunnel VPN, your laptop's route to the world runs through the VPN while your phone is on plain wifi. qrshare prefers your real LAN interface and warns when only a VPN or tunnel interface is available. Disconnect the VPN, or pass --host with your wifi IP.
Can I use this across the internet? No, by design. qrshare is LAN-only, with no relay, no NAT traversal, and no account. The QR is the whole discovery mechanism.
make build # build ./bin/qrshare
make test # run the test suite
go test ./... # same thing, directlyThe code is split into small packages under internal/: netaddr resolves the LAN address, qr renders the terminal QR, transfer holds the HTTP handlers, session ties them together, and clipboard handles the reverse-text path. Each package is tested on its own with net/http/httptest, so no real phone or network is needed to run the suite.
MIT. See LICENSE.

