<?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: Im Woojin</title>
    <description>The latest articles on DEV Community by Im Woojin (@rakkunn).</description>
    <link>https://dev.to/rakkunn</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3860147%2F8f75007d-4931-48ca-881c-915ce86430f2.jpeg</url>
      <title>DEV Community: Im Woojin</title>
      <link>https://dev.to/rakkunn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rakkunn"/>
    <language>en</language>
    <item>
      <title>I made my Markdown Editor "AI-Ready": MarkSmith v0.3.0</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Thu, 28 May 2026 18:51:39 +0000</pubDate>
      <link>https://dev.to/rakkunn/i-made-my-markdown-editor-ai-ready-marksmith-v030-2pdb</link>
      <guid>https://dev.to/rakkunn/i-made-my-markdown-editor-ai-ready-marksmith-v030-2pdb</guid>
      <description>&lt;p&gt;Hey DEV community! 👋 &lt;/p&gt;

&lt;p&gt;A few days ago, I built a VS Code extension called &lt;strong&gt;Marksmith&lt;/strong&gt; to fix the most annoying parts of writing Markdown (like pasting Excel tables and syncing preview scrolls). &lt;/p&gt;

&lt;p&gt;But recently, I noticed a huge shift in my own workflow: &lt;strong&gt;Half the Markdown I write isn't for humans anymore.&lt;/strong&gt; It’s being fed directly into Claude, ChatGPT, or Gemini as prompts and context.&lt;/p&gt;

&lt;p&gt;When you're constantly stuffing docs into context windows, two things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You worry about hitting context limits (or racking up API costs).&lt;/li&gt;
&lt;li&gt;You waste time dealing with AI "hallucinations" when you ask it to generate docs back for you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, for the &lt;strong&gt;v0.3.0 release&lt;/strong&gt;, &lt;br&gt;
I decided to pivot Marksmith into something new: &lt;strong&gt;An Agent AI-Ready Markdown Toolkit.&lt;/strong&gt; 🚀&lt;/p&gt;

&lt;p&gt;Here is what I added to survive the AI era:&lt;/p&gt;




&lt;h3&gt;
  
  
  📊 1. Real-time LLM Token Estimator
&lt;/h3&gt;

&lt;p&gt;Instead of just counting words, Marksmith’s &lt;em&gt;Document X-Ray&lt;/em&gt; sidebar now includes a &lt;strong&gt;Heuristic Token Estimator&lt;/strong&gt; for GPT, Claude, and Gemini. &lt;/p&gt;

&lt;p&gt;Before you copy-paste that massive README into your AI assistant, you can see exactly how "heavy" it is in terms of tokens right inside your editor. No more guessing if you're about to blow past your context limit!&lt;/p&gt;

&lt;h3&gt;
  
  
  ✂️ 2. Copy Optimized for AI (1-Click Minify)
&lt;/h3&gt;

&lt;p&gt;Formatting is great for humans, but LLMs don't need all those extra spaces, perfectly aligned markdown tables, or empty lines. &lt;/p&gt;

&lt;p&gt;I added a &lt;code&gt;CodeLens&lt;/code&gt; button at the top of your files. Click it, and Marksmith instantly &lt;strong&gt;minifies&lt;/strong&gt; your Markdown (compresses tables, strips blanks) and copies it to your clipboard. &lt;br&gt;
&lt;em&gt;Result:&lt;/em&gt; You save significant tokens and API costs without ruining your beautiful local &lt;code&gt;.md&lt;/code&gt; file.&lt;/p&gt;

&lt;h3&gt;
  
  
  🕵️ 3. Hallucination Quick Fix
&lt;/h3&gt;

&lt;p&gt;Ever ask an AI to write documentation, and it leaves behind a bunch of &lt;code&gt;[TODO: Insert link here]&lt;/code&gt; or makes up a fake local image path? &lt;/p&gt;

&lt;p&gt;Marksmith now automatically scans your document and puts a red squiggly line under &lt;strong&gt;AI placeholders and broken local links&lt;/strong&gt;. Click the 💡 icon, and you can instantly strip them out or fix them. It acts as a safety net before you commit AI-generated docs.&lt;/p&gt;

&lt;h3&gt;
  
  
  🛡️ 4. Security Hardening (SSRF / RCE blocked)
&lt;/h3&gt;

&lt;p&gt;Wait, security in a Markdown extension? Yes! &lt;br&gt;
If you are previewing AI-generated Markdown, you are essentially rendering untrusted input. Maliciously crafted Markdown can trigger SSRF (Server-Side Request Forgery) by trying to fetch internal network metadata, or even execute code during PDF exports. &lt;/p&gt;

&lt;p&gt;I completely locked down the extension. It uses strict &lt;code&gt;DOMPurify&lt;/code&gt; sandboxing and actively blocks attempts to ping internal/cloud IPs.&lt;/p&gt;




&lt;h3&gt;
  
  
  🎨 Bonus: A 90s Retro Redesign
&lt;/h3&gt;

&lt;p&gt;Because coding should be fun, I spent the weekend completely rebuilding the landing page using a &lt;strong&gt;Mac OS Classic / Neo-Brutalism&lt;/strong&gt; theme. Let me know what you think of the vibes! 💾&lt;/p&gt;

&lt;h3&gt;
  
  
  🌐 Open VSX / Cursor Support
&lt;/h3&gt;

&lt;p&gt;For all my Cursor, Windsurf, and VSCodium users out there—thank you for the support! The extension has been doing surprisingly well on &lt;strong&gt;Open VSX&lt;/strong&gt;, and it's officially verified there too.&lt;/p&gt;

&lt;p&gt;👇 &lt;strong&gt;Check it out here:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Website &amp;amp; Demo:&lt;/strong&gt; &lt;a href="https://rakkunn.github.io/MarkSmith/" rel="noopener noreferrer"&gt;rakkunn.github.io/MarkSmith&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a href="https://github.com/rakkunn/MarkSmith" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is 100% free and open-source. If you are building with AI or just write a ton of docs, I hope this saves you as much time as it saves me. &lt;/p&gt;

&lt;p&gt;Would love to hear your feedback in the comments! Happy coding! 💻✨&lt;/p&gt;

</description>
      <category>ai</category>
      <category>markdown</category>
      <category>productivity</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Building Marksmith: lessons from making Markdown bearable in VS Code</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Sun, 24 May 2026 10:04:35 +0000</pubDate>
      <link>https://dev.to/rakkunn/building-marksmith-lessons-from-making-markdown-bearable-in-vs-code-a1d</link>
      <guid>https://dev.to/rakkunn/building-marksmith-lessons-from-making-markdown-bearable-in-vs-code-a1d</guid>
      <description>&lt;p&gt;I'll start with the moment that pushed me to build this.&lt;/p&gt;

&lt;p&gt;I was editing a 1,200-line README. Spotted a typo in the preview pane. Scrolled the source to find it. Lost my place. Scrolled more. Pasted a table from a Google Sheet and got back a wall of tab characters. Closed VS Code in frustration and went to make coffee.&lt;/p&gt;

&lt;p&gt;That week I started building &lt;strong&gt;Marksmith&lt;/strong&gt;, a VS Code extension that tries to fix the small things that quietly drain you when writing Markdown. This is a retrospective: what I shipped, the parts that were harder than expected, and the security work that ended up being the largest chunk of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem isn't Markdown — it's the workflow around it
&lt;/h2&gt;

&lt;p&gt;The syntax is fine. The friction lives next to it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pasting a table from Excel or Sheets gives you tab-separated garbage&lt;/li&gt;
&lt;li&gt;Pasting a URL means manually typing &lt;code&gt;[text](url)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Editor and preview scroll independently, so a long file turns into needle-in-haystack&lt;/li&gt;
&lt;li&gt;You have no idea how heavy your doc is until you paste it into Claude or GPT and watch the token count blow up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is small. Together they're death by a thousand context switches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart Paste: making the clipboard do the work
&lt;/h2&gt;

&lt;p&gt;The first thing I built was clipboard interception. When you paste, Marksmith inspects the clipboard before VS Code's default handler runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handlePaste&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TextEditor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Tab-separated, multi-line → Excel/Sheets table&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isTabularData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;convertToMarkdownTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// URL pasted over a text selection → auto-link&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;](&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Excel-to-Markdown conversion is the one users mention most. It's not complicated — split by tabs, normalize column widths, generate the &lt;code&gt;|---|&lt;/code&gt; separator. But the impact on writing flow is disproportionate to the implementation effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bi-directional sync: scrolling that actually works
&lt;/h2&gt;

&lt;p&gt;This one took a few iterations.&lt;/p&gt;

&lt;p&gt;The first version synced scroll position by percentage. Useless for long files, because Markdown blocks have wildly different rendered heights (a one-line image reference is tiny in source, huge in preview).&lt;/p&gt;

&lt;p&gt;The fix was source-mapping. When rendering Markdown to HTML, attach a &lt;code&gt;data-line&lt;/code&gt; attribute to every top-level element pointing back to the source line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;data-line=&lt;/span&gt;&lt;span class="s"&gt;"42"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Bi-directional sync&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;data-line=&lt;/span&gt;&lt;span class="s"&gt;"44"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;This one took a few iterations.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scroll handlers on both sides then walk the DOM and align based on those anchors. Clicking any element in the preview jumps the cursor to that source line. Not novel — most modern Markdown editors do something similar — but getting it smooth inside a VS Code webview took some debouncing to avoid feedback loops where each side's scroll event triggered the other infinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Document X-Ray: knowing the cost before you paste
&lt;/h2&gt;

&lt;p&gt;If you draft in Markdown and feed it to an LLM, you eventually hit context limits. Marksmith has a sidebar showing word count, readability score, and an estimated LLM token count.&lt;/p&gt;

&lt;p&gt;The token estimate is a heuristic — running a real tokenizer in the extension host would be too heavy on every keystroke. Instead it uses a character-and-whitespace approximation calibrated against &lt;code&gt;tiktoken&lt;/code&gt; output on a documentation corpus. It gets within ~5% on doc-style text, which is enough to catch "this prompt is too big" before you actually paste it somewhere.&lt;/p&gt;

&lt;p&gt;I'd like to move this to a real tokenizer in a worker thread eventually. For now the heuristic is good enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I underestimated: security
&lt;/h2&gt;

&lt;p&gt;I thought I was building an editor extension. I ended up spending more time on security than on features. Three classes of vulnerability mattered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XSS in the webview.&lt;/strong&gt; Markdown allows raw HTML, so a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag in someone's README would execute in the webview context. I run all rendered HTML through DOMPurify with a strict allowlist, while preserving the elements I actually want (Mermaid SVG, syntax-highlighted spans):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ALLOWED_TAGS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;defaultTags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;svg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;ALLOWED_ATTR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;defaultAttrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-line&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewBox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xmlns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;FORBID_TAGS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iframe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RCE through PDF export.&lt;/strong&gt; Marksmith uses Puppeteer to render to PDF. In the first version I hadn't verified the Chromium sandbox was actually engaging on user machines. A crafted Markdown file with malicious HTML could, in theory, escape the renderer. I now explicitly verify sandbox mode at launch and fail closed if it isn't available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSRF in URL preview.&lt;/strong&gt; Smart Paste optionally fetches the title of a URL you paste. Without filtering, a user could be tricked into pasting &lt;code&gt;http://169.254.169.254/...&lt;/code&gt; (the AWS metadata endpoint) or &lt;code&gt;http://192.168.1.1/admin&lt;/code&gt;, and the extension would dutifully fetch it. The fix is IP filtering before the request, plus a redirect cap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPrivateIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isLoopback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Blocked: private/loopback address&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manual&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* + cap follows */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this is novel security work. What surprised me was how much attack surface a "simple" Markdown extension exposes once it starts being helpful — fetching URLs, rendering arbitrary HTML, spawning headless browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;A few things in hindsight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Threat-model first, features second.&lt;/strong&gt; I retrofitted protections; should have designed for them from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The token counter should have been a worker from the start.&lt;/strong&gt; Moving it off the main thread later was awkward and broke a few assumptions in the sidebar UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on Windows earlier.&lt;/strong&gt; Path handling for PDF export gave me grief that I could have caught much sooner.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/rakkunn/MarkSmith" rel="noopener noreferrer"&gt;Github&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Homepage: &lt;a href="https://rakkunn.github.io/MarkSmith/" rel="noopener noreferrer"&gt;Marksmith&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;VSCode Marketplace: 
&lt;a href="https://marketplace.visualstudio.com/items?itemName=rakkunn.marksmith" rel="noopener noreferrer"&gt;Marksmith on VS Code Marketplace&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenVSX Marketplace:
&lt;a href="https://open-vsx.org/extension/rakkunn/marksmith" rel="noopener noreferrer"&gt;Marksmith on OpenVSX&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've shipped a VS Code extension and have war stories about webview security or Puppeteer sandboxing, I'd genuinely like to hear them — drop a comment.&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>markdown</category>
      <category>llm</category>
      <category>showdev</category>
    </item>
    <item>
      <title>DualClip v1.2.6 — Stability Fix, Homebrew</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Tue, 14 Apr 2026 07:57:34 +0000</pubDate>
      <link>https://dev.to/rakkunn/dualclip-v126-stability-fix-homebrew-47ef</link>
      <guid>https://dev.to/rakkunn/dualclip-v126-stability-fix-homebrew-47ef</guid>
      <description>&lt;p&gt;It's been a busy couple of days for DualClip. What started as a frustrating crash investigation turned into a series of meaningful improvements — from fixing a deep resource-bundling bug to shipping Homebrew support. Here's a rundown of everything that changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Crash That Wouldn't Quit
&lt;/h2&gt;

&lt;p&gt;Shortly after v1.2.0, DualClip started crashing on launch for some users. The crash report pointed to Bundle.module inside the KeyboardShortcuts dependency — a fatalError triggered because the app couldn't locate its resource bundle at runtime.&lt;/p&gt;

&lt;p&gt;The root cause turned out to be a subtle mismatch in how Swift Package Manager bundles resources for standalone .app builds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPM generates a file called resource_bundle_accessor.swift that looks for resources at Bundle.main.bundleURL — which resolves to the .app/ root directory.&lt;/li&gt;
&lt;li&gt;But macOS codesigning requires all resources to live inside Contents/Resources/, not the app root. Placing bundles at the root causes an "unsealed contents" signing error.&lt;/li&gt;
&lt;li&gt;So the resources were in Contents/Resources/, but the code was looking at .app/ — and finding nothing.
The fix was surgical: after the initial build, we patch the generated accessor to use Bundle.main.resourceURL! (which points to Contents/Resources/), then recompile just the affected module and re-link the executable. SPM regenerates the accessor on every full build, so we had to carefully avoid triggering a rebuild after patching. The final binary is verified with strings to confirm the patch is baked in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This fix ships in v1.2.6 and the crash is fully resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Homebrew Support
&lt;/h2&gt;

&lt;p&gt;DualClip is now installable via Homebrew:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;brew install RAKKUNN/tap/dualclip&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We set up a dedicated Homebrew Tap with a Cask formula that points to the latest signed and notarized release. The CI pipeline automatically updates the formula (version + SHA256) whenever a new release is tagged — zero manual steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes DualClip Different
&lt;/h2&gt;

&lt;p&gt;After analyzing 57 macOS clipboard managers, one thing stood out: every single one is a clipboard history manager. DualClip is the only one that uses dedicated slots.&lt;/p&gt;

&lt;p&gt;The difference matters:&lt;/p&gt;

&lt;p&gt;History managers record everything you copy and let you scroll back through it. Great for recall, but adds complexity — databases, search UI, sync, storage limits.&lt;br&gt;
DualClip gives you 3 fixed slots with instant keyboard access. You decide where to copy, not when to find it later. No history, no scrolling, no search.&lt;br&gt;
It's also one of the few clipboard tools that stores absolutely nothing to disk. Everything lives in RAM and vanishes on quit. No network access, no telemetry, no cloud sync. &lt;/p&gt;
&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;brew install RAKKUNN/tap/dualclip&lt;/code&gt;&lt;br&gt;
Or grab the latest release from GitHub.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/RAKKUNN" rel="noopener noreferrer"&gt;
        RAKKUNN
      &lt;/a&gt; / &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;
        DualClip
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A lightweight macOS app for multi-slot clipboard management
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/RAKKUNN/DualClip/icon.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FRAKKUNN%2FDualClip%2FHEAD%2Ficon.png" alt="DualClip Icon" width="128" height="128"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DualClip&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  &lt;b&gt;English&lt;/b&gt; · &lt;a href="https://github.com/RAKKUNN/DualClip/README-ko.md" rel="noopener noreferrer"&gt;한국어&lt;/a&gt; · &lt;a href="https://github.com/RAKKUNN/DualClip/README-ja.md" rel="noopener noreferrer"&gt;日本語&lt;/a&gt; · &lt;a href="https://github.com/RAKKUNN/DualClip/README-zh.md" rel="noopener noreferrer"&gt;中文&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  A lightweight macOS menu bar app that provides &lt;b&gt;multi-slot clipboard management&lt;/b&gt;.&lt;br&gt;
  Unlike history-based clipboard managers, DualClip gives you instant access to dedicated clipboard slots via customizable keyboard shortcuts
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://rakkunn.github.io/DualClip/" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;🌐 Website&lt;/b&gt;&lt;/a&gt;
   · 
  &lt;a href="https://github.com/RAKKUNN/DualClip/releases/latest" rel="noopener noreferrer"&gt;&lt;b&gt;⬇ Download&lt;/b&gt;&lt;/a&gt;
   · 
  &lt;a href="https://github.com/RAKKUNN/DualClip#installation" rel="noopener noreferrer"&gt;&lt;b&gt;🍺 Homebrew&lt;/b&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/RAKKUNN/DualClip/actions/workflows/ci.yml/badge.svg"&gt;&lt;img src="https://github.com/RAKKUNN/DualClip/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/6bffd51139ffc392eddcd88884109841bddeb7332c97055a073d66df7fadf064/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61634f532d31332532422d626c7565"&gt;&lt;img src="https://camo.githubusercontent.com/6bffd51139ffc392eddcd88884109841bddeb7332c97055a073d66df7fadf064/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61634f532d31332532422d626c7565" alt="macOS 13+"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/2b1ee465df76b12ec13951241072e303f4518d7562c9ae3abcf609d074419bd6/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4170706c6525323053696c69636f6e2d72657175697265642d626c61636b3f6c6f676f3d6170706c65"&gt;&lt;img src="https://camo.githubusercontent.com/2b1ee465df76b12ec13951241072e303f4518d7562c9ae3abcf609d074419bd6/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4170706c6525323053696c69636f6e2d72657175697265642d626c61636b3f6c6f676f3d6170706c65" alt="Apple Silicon"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/8c972106af96ab0d4f2778157576ab9728bc8b0735969ac8963a503781b0e62b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53776966742d352e392532422d6f72616e6765"&gt;&lt;img src="https://camo.githubusercontent.com/8c972106af96ab0d4f2778157576ab9728bc8b0735969ac8963a503781b0e62b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53776966742d352e392532422d6f72616e6765" alt="Swift 5.9+"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/5caa455d8debc46fb23abbadb45a733a937f3910a73fc875c2f7820468e1bb54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e"&gt;&lt;img src="https://camo.githubusercontent.com/5caa455d8debc46fb23abbadb45a733a937f3910a73fc875c2f7820468e1bb54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3 Clipboard Slots&lt;/strong&gt;: Slot A (system default), Slot B, and Slot C&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable Shortcuts&lt;/strong&gt;: No hardcoded key conflicts — configure your own shortcuts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic Paste&lt;/strong&gt;: Seamlessly pastes from any slot without corrupting your system clipboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Menu Bar Popover&lt;/strong&gt;: Quick-glance view of all slot contents with previews&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy First&lt;/strong&gt;: All data lives in RAM only — nothing is persisted to disk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Network Access&lt;/strong&gt;: No telemetry, no analytics, no internet communication&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Demo&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/RAKKUNN/DualClip/test_dualclip.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FRAKKUNN%2FDualClip%2FHEAD%2Ftest_dualclip.gif" alt="DualClip Demo" width="600"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/RAKKUNN/DualClip/test_dualclip_image.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FRAKKUNN%2FDualClip%2FHEAD%2Ftest_dualclip_image.gif" alt="DualClip Image Support Demo" width="600"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Default Shortcuts&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;br&gt;
&lt;thead&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Shortcut&lt;/th&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/thead&gt;
&lt;br&gt;
&lt;tbody&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Copy to Slot B&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;⌥⌘C&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Paste from Slot B&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;⌥⌘V&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Copy to Slot C&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;⌃⌘C&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Paste from Slot C&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;⌃⌘V&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/tbody&gt;
&lt;br&gt;
&lt;/table&gt;&lt;/div&gt;&lt;/p&gt;

&lt;p&gt;All shortcuts are…&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;&lt;br&gt;
Signed and notarized by Apple — no Gatekeeper warnings.

&lt;p&gt;DualClip is open source under the MIT license. Issues, feedback, and contributions are welcome.&lt;/p&gt;

&lt;p&gt;Thank you, have a nice day!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>swift</category>
      <category>showdev</category>
    </item>
    <item>
      <title>DualClip - Update_2026.04.12.</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Sun, 12 Apr 2026 07:48:23 +0000</pubDate>
      <link>https://dev.to/rakkunn/dualclip-update20260412-4pb7</link>
      <guid>https://dev.to/rakkunn/dualclip-update20260412-4pb7</guid>
      <description>&lt;p&gt;Last week, I shared the first round of updates for DualClip, my open-source macOS clipboard manager. Since then, I’ve crossed off three major items from the roadmap—including the one I’m most excited about: you can finally download DualClip without needing to build it from source.&lt;/p&gt;

&lt;p&gt;Here is the breakdown of what’s new in v1.1.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔒 Secure Input Field Detection
&lt;/h2&gt;

&lt;p&gt;This was at the top of my "What’s Next" list for a reason: Privacy.&lt;/p&gt;

&lt;p&gt;When macOS has a password field focused, the system activates a mode called Secure Event Input. DualClip now checks for this state before every copy and paste operation. If you're typing a password into Safari, 1Password, or a terminal sudo prompt, DualClip silently steps back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Swift&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;isSecureInputActive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;IsSecureEventInputEnabled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guard is dead simple—one line in handleCopy and one in handlePaste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Swift&lt;/span&gt;
&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;AccessibilityService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSecureInputActive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s the kind of feature that, when it works correctly, you never notice. And that’s exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  📦 Prebuilt Binaries (No Xcode Required!)
&lt;/h2&gt;

&lt;p&gt;The biggest friction point in my original post was: "There's no prebuilt binary yet." Starting with v1.1.0, every release is signed with a Developer ID certificate and notarized by Apple. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;No more Gatekeeper warnings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No "Open Anyway" dance in System Settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Just download, drag to Applications, and go.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🏗 The CI/CD Saga: 3 Bugs and a Notarization Pipeline
&lt;/h2&gt;

&lt;p&gt;Getting the automated release pipeline working was... an adventure. I’m using GitHub Actions to handle the heavy lifting. The workflow triggers on version tags (v*.&lt;em&gt;.&lt;/em&gt;) and follows this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Build: swift build -c release on a macOS 14 ARM runner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Import: Decode the .p12 certificate from GitHub Secrets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bundle: Assemble the .app structure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sign: codesign with hardened runtime and a timestamp.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Notarize: Submit to Apple, poll for completion, and staple the ticket.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sounds clean on paper. In practice, it took four attempts and taught me three very humbling lessons:&lt;/p&gt;

&lt;p&gt;Bug 1: The One-Character Typo&lt;br&gt;
I spent 10 minutes staring at a 401 Unauthorized error from Apple’s API. It turned out to be a typo in my app-specific password secret: fjhk vs tihk. CI/CD reminds you that your eyes see what they want to see, not what’s actually there.&lt;/p&gt;

&lt;p&gt;Bug 2: The Service Outage&lt;br&gt;
Three consecutive submissions got stuck at "In Progress" for hours. I thought my script was hung. It turns out Apple's notarization service was actually having a global outage. Even the best pipeline can’t fix a broken cloud.&lt;/p&gt;

&lt;p&gt;Bug 3: The awk Failure&lt;br&gt;
My polling script used awk '{print $2}' to extract the status from notarytool info. However, the status string for a pending job is In Progress (two words). awk captured only "In," which matched nothing in my logic.&lt;br&gt;
The Fix: Switched to sed 's/.*status: //' to grab the full string.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔄 Updated Roadmap
&lt;/h2&gt;

&lt;p&gt;Feature Status&lt;br&gt;
Secure input field detection    ✅ Shipped&lt;br&gt;
RAM zeroing on termination  ✅ Shipped&lt;br&gt;
Image/rich text support ✅ Shipped&lt;br&gt;
GitHub Actions CI/CD + Notarization ✅ Shipped&lt;br&gt;
Homebrew Cask distribution  🔜 Next&lt;br&gt;
Sparkle auto-update 📅 Planned&lt;/p&gt;




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

&lt;p&gt;If you’ve been waiting for a downloadable build, it’s ready for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to the Latest Releases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Download DualClip-1.1.0-arm64.zip.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unzip, move to Applications, and launch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grant Accessibility permission when prompted.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🔗 GitHub: &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;https://github.com/RAKKUNN/DualClip&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you find a bug or have a feature request, feel free to open an issue or a PR!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>swift</category>
    </item>
    <item>
      <title>DualClip - Update_2026.04.04.</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Sat, 04 Apr 2026 10:54:46 +0000</pubDate>
      <link>https://dev.to/rakkunn/dualclip-update20260404-39lp</link>
      <guid>https://dev.to/rakkunn/dualclip-update20260404-39lp</guid>
      <description>&lt;h1&gt;
  
  
  DualClip Update — Beyond Text, and a 150ms Correction
&lt;/h1&gt;

&lt;p&gt;A few hours ago, I shared &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;DualClip&lt;/a&gt;, a slot-based clipboard manager for macOS. Thank you to everyone who checked it out!&lt;/p&gt;

&lt;p&gt;Since then, I've shipped a few meaningful updates that I didn't cover in the original post. I also owe you a small correction. Let me walk through both.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 First, a correction: 50ms → 150ms
&lt;/h2&gt;

&lt;p&gt;In my first post, I wrote that the Atomic Paste operation completes "in less than 50ms." That was wrong. The actual restore delay is &lt;strong&gt;150ms&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// 150ms is an empirically safe value to prevent race conditions.&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;restoreDelayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;150ms was chosen empirically to avoid a race condition — if the clipboard is restored before the target application finishes reading it, the paste silently fails. 50ms sounded cooler, but 150ms is what actually works reliably. Sorry about that.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ How Atomic Paste actually works
&lt;/h2&gt;

&lt;p&gt;Since this is the core mechanic of DualClip, I figured it deserves a proper breakdown. When you press the hotkey to paste from Slot B, here's what happens under the hood:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Backup&lt;/strong&gt; — The entire system clipboard (Slot A) is deep-copied into a temporary buffer. Not just text — all &lt;code&gt;NSPasteboardItem&lt;/code&gt; types and their raw data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Swap&lt;/strong&gt; — Slot B's content is written to the system clipboard, replacing whatever was there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Simulate&lt;/strong&gt; — A &lt;code&gt;CGEvent&lt;/code&gt;-based ⌘V keystroke is posted to the HID system. This is why DualClip requires Accessibility permission — it's literally pressing keys on your behalf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Restore&lt;/strong&gt; — After 150ms, the backup is written back to the system clipboard. Your original ⌘C content is untouched.&lt;/p&gt;

&lt;p&gt;The entire flow is invisible to the user. You press a hotkey, text appears, and your clipboard stays exactly as it was.&lt;/p&gt;




&lt;h2&gt;
  
  
  🖼 Multi-content type support
&lt;/h2&gt;

&lt;p&gt;The first post only showed text workflows, which gave the impression that DualClip is text-only. It's not — it now supports &lt;strong&gt;four content types&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plain text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Code snippets, URLs, API keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rich text (RTF)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Formatted text with bold, colors, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Images&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Screenshots, design assets from Figma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File URLs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;File paths copied from Finder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each slot stores the raw &lt;code&gt;NSPasteboardItem&lt;/code&gt; data faithfully, so what you copy is exactly what you get back — formatting, metadata, and all.&lt;/p&gt;

&lt;p&gt;This means you can now keep a screenshot in Slot B and a HEX code in Slot C, then paste them alternately into your editor without touching the mouse.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 RAM Zeroing on termination
&lt;/h2&gt;

&lt;p&gt;In the first post, I mentioned that all data lives in RAM only. That's still true, but I've taken it a step further.&lt;/p&gt;

&lt;p&gt;When DualClip quits, it doesn't just release memory — it &lt;strong&gt;overwrites every byte with zeros&lt;/strong&gt; before deallocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;secureWipe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pasteboardItems&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;withUnsafeBytes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;rawBuffer&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;baseAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rawBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseAddress&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;mutable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UnsafeMutableRawPointer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;mutating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;baseAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;memset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mutable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rawBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is called on &lt;code&gt;applicationWillTerminate&lt;/code&gt; for all three slots. If you temporarily stored an API key or password in a slot, it won't linger in physical memory after the app closes.&lt;/p&gt;

&lt;p&gt;To summarize the privacy model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No disk writes.&lt;/strong&gt; Nothing is persisted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No network.&lt;/strong&gt; Zero external communication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAM zeroing.&lt;/strong&gt; Memory is scrubbed on exit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source.&lt;/strong&gt; You can verify all of the above yourself.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏗 CI pipeline
&lt;/h2&gt;

&lt;p&gt;I've added a GitHub Actions workflow that runs on every push and PR to &lt;code&gt;main&lt;/code&gt;. It builds the project in both Debug and Release configurations on macOS 14 with Swift 5.9.&lt;/p&gt;

&lt;p&gt;It's a simple setup — no notarization or automatic releases yet — but it catches build regressions before they land.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠 Building from source
&lt;/h2&gt;

&lt;p&gt;There's no prebuilt binary yet, but building is straightforward if you have Xcode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/RAKKUNN/DualClip.git
&lt;span class="nb"&gt;cd &lt;/span&gt;DualClip
open Package.swift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit ▶ Run in Xcode. On first launch, macOS will ask for Accessibility permission — this is required for the CGEvent keystroke simulation that powers Atomic Paste.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Secure input field detection (auto-disable in password fields)&lt;/li&gt;
&lt;li&gt;Homebrew Cask distribution&lt;/li&gt;
&lt;li&gt;Sparkle auto-update&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This is still a small, single-purpose tool, but I'm trying to get the details right. If you have feedback or want to contribute, the repo is open.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;https://github.com/RAKKUNN/DualClip&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>swift</category>
    </item>
    <item>
      <title>DualClip: multi-slot clipboard manager for macOS</title>
      <dc:creator>Im Woojin</dc:creator>
      <pubDate>Fri, 03 Apr 2026 23:09:10 +0000</pubDate>
      <link>https://dev.to/rakkunn/dualclip-multi-slot-clipboard-manager-44f5</link>
      <guid>https://dev.to/rakkunn/dualclip-multi-slot-clipboard-manager-44f5</guid>
      <description>&lt;p&gt;&lt;strong&gt;First project with macOS &amp;amp; Swift&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wanted to share &lt;strong&gt;DualClip&lt;/strong&gt;, a native macOS menu bar app I’ve been working on.&lt;/p&gt;

&lt;p&gt;While there are many great clipboard managers like Maccy or Paste, I found that most of them focus on "History"—searching through a vertical list of everything you've copied.&lt;/p&gt;

&lt;p&gt;I built DualClip because I needed something that works more like a "Workbench." Instead of picking from a menu, DualClip gives you dedicated slots (A, B, and C) that you can access instantly via global hotkeys. &lt;/p&gt;

&lt;p&gt;🚀 How it differs from history-based managers:&lt;/p&gt;

&lt;p&gt;No List Selection: You don't have to break your flow to search or click an item from a list. You use ⌥⌘C to save to Slot B and ⌥⌘V to paste it instantly. &lt;/p&gt;

&lt;p&gt;Atomic Paste: When you trigger a secondary slot, the app performs a high-speed "injection"—swapping the system clipboard, pasting, and restoring the original content in less than 50ms. &lt;/p&gt;

&lt;p&gt;Parallel Workflow: Perfect for developers moving IDs and Emails simultaneously, or translators working with source and target text in two separate slots. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpzfbwy1oifgvlbmx1f7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpzfbwy1oifgvlbmx1f7n.png" alt=" " width="566" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo0pbvbliklcfzzhgc7e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo0pbvbliklcfzzhgc7e.png" alt=" " width="714" height="510"&gt;&lt;/a&gt;&lt;br&gt;
🔐 Privacy:&lt;/p&gt;

&lt;p&gt;As a security enthusiast, I designed this with transparency in mind:&lt;/p&gt;

&lt;p&gt;In-Memory Only: Clipboard data is stored strictly in RAM and is never written to disk.&lt;/p&gt;

&lt;p&gt;Zero Network Access: The app has no network permissions. No telemetry, no analytics, no external communication. &lt;/p&gt;

&lt;p&gt;🛠 Tech Stack:&lt;/p&gt;

&lt;p&gt;Language: Swift 5.9+ / SwiftUI &amp;amp; AppKit Hybrid&lt;/p&gt;

&lt;p&gt;The project is licensed under MIT, and I’d love to get some feedback or contributions from this community!&lt;/p&gt;

&lt;p&gt;Thank you for reading my small project!&lt;/p&gt;

&lt;p&gt;🔗 GitHub Repository: &lt;a href="https://github.com/RAKKUNN/DualClip" rel="noopener noreferrer"&gt;https://github.com/RAKKUNN/DualClip&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>swift</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
