<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Emin Buyuk</title>
    <description>The latest articles on DEV Community by Emin Buyuk (@eminbuyuk).</description>
    <link>https://dev.to/eminbuyuk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2101071%2F71b33c6b-56ff-4285-936e-302d78e6af61.png</url>
      <title>DEV Community: Emin Buyuk</title>
      <link>https://dev.to/eminbuyuk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eminbuyuk"/>
    <language>en</language>
    <item>
      <title>Cpynet a pastebin you talk to with curl, that forgets everything you send it</title>
      <dc:creator>Emin Buyuk</dc:creator>
      <pubDate>Sun, 09 Aug 2026 18:21:49 +0000</pubDate>
      <link>https://dev.to/eminbuyuk/why-i-built-a-zero-dependency-single-file-go-clipboard-for-terminal-junkies-2ob6</link>
      <guid>https://dev.to/eminbuyuk/why-i-built-a-zero-dependency-single-file-go-clipboard-for-terminal-junkies-2ob6</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I keep ending up in situations where I need to move a small piece of text — a&lt;br&gt;
log snippet, a password, a container's stdout — from one machine to another,&lt;br&gt;
and the clipboard just isn't there. SSH session on a remote box. A locked-down&lt;br&gt;
corporate laptop that won't let me touch the OS clipboard at all. A container&lt;br&gt;
with no shared volume and no browser. Slack is right there, but pasting a&lt;br&gt;
database password into a channel that's archived forever is a special kind of&lt;br&gt;
bad idea.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://cpynet.com" rel="noopener noreferrer"&gt;CPYNET&lt;/a&gt;&lt;/strong&gt; — a paste-sharing tool with&lt;br&gt;
exactly one interface that matters: &lt;code&gt;curl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello world"&lt;/span&gt; | curl &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @- https://cpynet.com/
&lt;span class="c"&gt;# https://cpynet.com/482913&lt;/span&gt;
curl https://cpynet.com/482913
&lt;span class="c"&gt;# hello world&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. No account, no API key, no clicking around. Two&lt;br&gt;
&lt;code&gt;curl&lt;/code&gt; calls and you've moved text between two machines that have nothing in&lt;br&gt;
common except a network path.&lt;/p&gt;
&lt;h2&gt;
  
  
  Burn-after-read, actually
&lt;/h2&gt;

&lt;p&gt;The paste above is gone the instant that second &lt;code&gt;curl&lt;/code&gt; runs. Not "gone in 24&lt;br&gt;
hours" — gone &lt;em&gt;the moment it's read&lt;/em&gt;, whether that's one second later or one&lt;br&gt;
minute later. Read it twice (even from the same machine) and the second&lt;br&gt;
request gets a plain &lt;code&gt;404&lt;/code&gt;. It also auto-expires on a timer (2 minutes by&lt;br&gt;
default) even if nobody ever reads it, so an unread secret doesn't just sit&lt;br&gt;
there.&lt;/p&gt;

&lt;p&gt;None of this lives on disk. It's a Go &lt;code&gt;map&lt;/code&gt; behind a mutex, in memory, for&lt;br&gt;
the lifetime of one process. Restart the server and every paste that hasn't&lt;br&gt;
been read yet is just... gone. That's not a limitation I'm working around —&lt;br&gt;
it's the actual point. A "burn after read" tool that persists to disk somewhere&lt;br&gt;
you're not thinking about isn't really burning anything.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shell functions, if you don't want to remember the curl flags
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://cpynet.com/install.sh &lt;span class="nt"&gt;-o&lt;/span&gt; install.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bash &lt;span class="nt"&gt;-n&lt;/span&gt; install.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That wires up two functions, &lt;code&gt;cpy&lt;/code&gt; and &lt;code&gt;pst&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myservice &lt;span class="nt"&gt;-n&lt;/span&gt; 200 | cpy
482913
curl https://cpynet.com/482913

&lt;span class="c"&gt;# on the other machine&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pst 482913
Aug 08 14:02:11 myservice[1823]: connection refused: db.internal:5432
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pst&lt;/code&gt; also drops the text on your clipboard if it can find a way to&lt;br&gt;
(&lt;code&gt;pbcopy&lt;/code&gt;/&lt;code&gt;wl-copy&lt;/code&gt;/&lt;code&gt;xclip&lt;/code&gt;/&lt;code&gt;xsel&lt;/code&gt;/&lt;code&gt;clip.exe&lt;/code&gt;) — pure convenience, never&lt;br&gt;
required. Both functions are plain shell, both talk to the exact same HTTP&lt;br&gt;
endpoints the web UI uses. There's no separate "API" to keep in sync with the&lt;br&gt;
"real" product — the curl pipe &lt;em&gt;is&lt;/em&gt; the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two encryption layers, for two different threat models
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Password protection&lt;/strong&gt; (&lt;code&gt;?p=&lt;/code&gt; / &lt;code&gt;cpy -password=xxx&lt;/code&gt;) is AES-256-GCM with a&lt;br&gt;
key derived via PBKDF2 (210k iterations — OWASP's current minimum). The&lt;br&gt;
server decrypts the text for a moment to verify the password, then re-encrypts&lt;br&gt;
it at rest. This is "I trust the server, I just don't want it sitting there&lt;br&gt;
in plaintext" — protects against a disk dump or a backup leak, not against&lt;br&gt;
the server operator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;True end-to-end encryption&lt;/strong&gt; (the shield icon, or &lt;code&gt;cpy -e&lt;/code&gt;) is a different&lt;br&gt;
thing entirely: the key is generated client-side and never sent to the&lt;br&gt;
server at all. On the web it's WebCrypto AES-256-GCM with the key living in&lt;br&gt;
the link's &lt;code&gt;#key=&lt;/code&gt; fragment — browsers never send fragments to a server, by&lt;br&gt;
spec, so it physically can't leak that way. On the CLI it's &lt;code&gt;openssl enc&lt;br&gt;
-aes-256-cbc -pbkdf2&lt;/code&gt; with a random passphrase printed on its own line,&lt;br&gt;
deliberately never appended to the URL. Two independent implementations,&lt;br&gt;
neither required to interoperate with the other, both landing on the same&lt;br&gt;
guarantee: the server only ever sees ciphertext.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually makes this "zero dependency"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;go.mod&lt;/code&gt; has one line: &lt;code&gt;go 1.22&lt;/code&gt;. No router package, no ORM, no crypto&lt;br&gt;
library beyond &lt;code&gt;crypto/*&lt;/code&gt; in the standard library, no frontend build step —&lt;br&gt;
the HTML/CSS/JS are Go string constants rendered with &lt;code&gt;html/template&lt;/code&gt;, and&lt;br&gt;
static assets like screenshots are &lt;code&gt;//go:embed&lt;/code&gt;ded straight into the binary.&lt;br&gt;
The whole server is one &lt;code&gt;main.go&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;That's not a purity flex. It means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You can read the entire thing in an afternoon.&lt;/strong&gt; No dependency tree to
audit, no transitive CVE to track down at 2am.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker build&lt;/code&gt; produces one static binary on &lt;code&gt;distroless&lt;/code&gt;.&lt;/strong&gt; No shell in
the final image, no package manager, nothing to exploit even if something
got in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't rot.&lt;/strong&gt; There's no &lt;code&gt;npm audit&lt;/code&gt; running against a pile of
frontend packages six months from now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A few other things it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;QR code for the link&lt;/strong&gt; — 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live "it was just read" notification&lt;/strong&gt; via Server-Sent Events — the tab
that created the paste knows the instant it's gone, no polling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small file uploads&lt;/strong&gt; — a text-file-only allowlist (plus a magic-byte
check on the decoded bytes, so a renamed &lt;code&gt;.exe&lt;/code&gt; claiming to be a &lt;code&gt;.txt&lt;/code&gt;
gets rejected too), downloaded back with the original filename and a
sniffed content type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A netcat fallback&lt;/strong&gt; (&lt;code&gt;nc host port &amp;lt; file&lt;/code&gt;) for environments too minimal
to have &lt;code&gt;curl&lt;/code&gt; at all — a scratch container, say.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-IP rate limiting&lt;/strong&gt; on both writes and reads — the read limit is what
actually keeps a 6-digit code space safe against brute-forcing.&lt;/li&gt;
&lt;li&gt;The web UI itself is optional. It's there because sometimes you don't
&lt;em&gt;want&lt;/em&gt; a terminal — but the terminal path was never the fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not just use pastebin / termbin / privnote?
&lt;/h2&gt;

&lt;p&gt;Short version, longer one's &lt;a href="https://cpynet.com/compare" rel="noopener noreferrer"&gt;on the site&lt;/a&gt;:&lt;br&gt;
pastebin and termbin don't burn after read by default (your paste sits there&lt;br&gt;
until you delete it, or forever); privnote does burn after read but has no&lt;br&gt;
CLI story at all — it's a web form, full stop. CPYNET is built specifically&lt;br&gt;
for the "I'm already in a terminal and don't want to leave it" case, with&lt;br&gt;
burn-after-read as the default rather than an opt-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://cpynet.com/install.sh &lt;span class="nt"&gt;-o&lt;/span&gt; install.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bash &lt;span class="nt"&gt;-n&lt;/span&gt; install.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; install.sh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"it works"&lt;/span&gt; | cpy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feedback, issues, and "here's a threat model you didn't think about" are all&lt;br&gt;
welcome.&lt;/p&gt;

</description>
      <category>go</category>
      <category>devops</category>
      <category>productivity</category>
      <category>sysadmin</category>
    </item>
  </channel>
</rss>
