A zero-dependency, single-file Go pastebin built for terminals — burn-after-read by default, two independent encryption layers, and a curl one-liner instead of a login form.
I keep ending up in situations where I need to move a small piece of text — a
log snippet, a password, a container's stdout — from one machine to another,
and the clipboard just isn't there. SSH session on a remote box. A locked-down
corporate laptop that won't let me touch the OS clipboard at all. A container
with no shared volume and no browser. Slack is right there, but pasting a
database password into a channel that's archived forever is a special kind of
bad idea.
So I built CPYNET — a paste-sharing tool with
exactly one interface that matters: curl.
echo "hello world" | curl --data-binary @- https://cpynet.com/
# https://cpynet.com/482913
curl https://cpynet.com/482913
# hello world
That's the whole thing. No account, no API key, no clicking around. Two
curl calls and you've moved text between two machines that have nothing in
common except a network path.
Burn-after-read, actually
The paste above is gone the instant that second curl runs. Not "gone in 24
hours" — gone the moment it's read, whether that's one second later or one
minute later. Read it twice (even from the same machine) and the second
request gets a plain 404. It also auto-expires on a timer (2 minutes by
default) even if nobody ever reads it, so an unread secret doesn't just sit
there.
None of this lives on disk. It's a Go map behind a mutex, in memory, for
the lifetime of one process. Restart the server and every paste that hasn't
been read yet is just... gone. That's not a limitation I'm working around —
it's the actual point. A "burn after read" tool that persists to disk somewhere
you're not thinking about isn't really burning anything.
The shell functions, if you don't want to remember the curl flags
curl -s https://cpynet.com/install.sh -o install.sh && bash -n install.sh && . install.sh
That wires up two functions, cpy and pst:
$ journalctl -u myservice -n 200 | cpy
482913
curl https://cpynet.com/482913
# on the other machine
$ pst 482913
Aug 08 14:02:11 myservice[1823]: connection refused: db.internal:5432
...
pst also drops the text on your clipboard if it can find a way to
(pbcopy/wl-copy/xclip/xsel/clip.exe) — pure convenience, never
required. Both functions are plain shell, both talk to the exact same HTTP
endpoints the web UI uses. There's no separate "API" to keep in sync with the
"real" product — the curl pipe is the product.
Two encryption layers, for two different threat models
Password protection (?p= / cpy -password=xxx) is AES-256-GCM with a
key derived via PBKDF2 (210k iterations — OWASP's current minimum). The
server decrypts the text for a moment to verify the password, then re-encrypts
it at rest. This is "I trust the server, I just don't want it sitting there
in plaintext" — protects against a disk dump or a backup leak, not against
the server operator.
True end-to-end encryption (the shield icon, or cpy -e) is a different
thing entirely: the key is generated client-side and never sent to the
server at all. On the web it's WebCrypto AES-256-GCM with the key living in
the link's #key= fragment — browsers never send fragments to a server, by
spec, so it physically can't leak that way. On the CLI it's openssl enc with a random passphrase printed on its own line,
-aes-256-cbc -pbkdf2
deliberately never appended to the URL. Two independent implementations,
neither required to interoperate with the other, both landing on the same
guarantee: the server only ever sees ciphertext.
What actually makes this "zero dependency"
go.mod has one line: go 1.22. No router package, no ORM, no crypto
library beyond crypto/* in the standard library, no frontend build step —
the HTML/CSS/JS are Go string constants rendered with html/template, and
static assets like screenshots are //go:embedded straight into the binary.
The whole server is one main.go file.
That's not a purity flex. It means:
- You can read the entire thing in an afternoon. No dependency tree to audit, no transitive CVE to track down at 2am.
-
docker buildproduces one static binary ondistroless. No shell in the final image, no package manager, nothing to exploit even if something got in. -
It doesn't rot. There's no
npm auditrunning against a pile of frontend packages six months from now.
A few other things it does
- QR code for the link — scan it with a phone camera, it opens and decrypts right in the browser. Generated by a small dependency-free QR encoder that runs entirely client-side; nothing gets sent to a third party to render a code.
- Live "it was just read" notification via Server-Sent Events — the tab that created the paste knows the instant it's gone, no polling.
-
Small file uploads — a text-file-only allowlist (plus a magic-byte
check on the decoded bytes, so a renamed
.execlaiming to be a.txtgets rejected too), downloaded back with the original filename and a sniffed content type. -
A netcat fallback (
nc host port < file) for environments too minimal to havecurlat all — a scratch container, say. - Per-IP rate limiting on both writes and reads — the read limit is what actually keeps a 6-digit code space safe against brute-forcing.
- The web UI itself is optional. It's there because sometimes you don't want a terminal — but the terminal path was never the fallback.
Why not just use pastebin / termbin / privnote?
Short version, longer one's on the site:
pastebin and termbin don't burn after read by default (your paste sits there
until you delete it, or forever); privnote does burn after read but has no
CLI story at all — it's a web form, full stop. CPYNET is built specifically
for the "I'm already in a terminal and don't want to leave it" case, with
burn-after-read as the default rather than an opt-in.
Try it
curl -s https://cpynet.com/install.sh -o install.sh && bash -n install.sh && . install.sh
echo "it works" | cpy
Feedback, issues, and "here's a threat model you didn't think about" are all
welcome.
Top comments (0)