<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Development Environment on Nick Liu - Software Engineer</title>
    <link>https://nick-liu.com/categories/development-environment/</link>
    <description>Recent content in Development Environment on Nick Liu - Software Engineer</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <managingEditor>nickboy@users.noreply.github.com (Nick Liu)</managingEditor>
    <webMaster>nickboy@users.noreply.github.com (Nick Liu)</webMaster>
    <copyright>2026 Nick Liu</copyright>
    <lastBuildDate>Fri, 10 Jul 2026 20:05:23 -0700</lastBuildDate><atom:link href="https://nick-liu.com/categories/development-environment/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Blowfish supports four analytics providers. Cloudflare Web Analytics isn&#39;t one.</title>
      <link>https://nick-liu.com/posts/cloudflare-web-analytics-hugo/</link>
      <pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/cloudflare-web-analytics-hugo/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;For six months I assumed nobody could tell whether anyone read this blog, because I had never added analytics. Wiring up Cloudflare Web Analytics by hand taught me two things: the obvious place to paste the snippet would have shipped my Playwright suite&#39;s page views into the dashboard, and the dashboard had been quietly counting my visitors for two months anyway.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;div class=&#34;tested-with&#34; role=&#34;note&#34;&gt;&#xA;  &lt;span class=&#34;tested-with-icon&#34; aria-hidden=&#34;true&#34;&gt;🧪&lt;/span&gt;&#xA;  &lt;span&gt;Tested with &lt;strong&gt;Hugo&lt;/strong&gt; &lt;code&gt;0.163.3&lt;/code&gt; · Blowfish 2.104&lt;/span&gt;&#xA;&lt;/div&gt;&#xA;&#xA;&#xA;&lt;h2 class=&#34;relative group&#34;&gt;Publishing into the void&#xA;    &lt;div id=&#34;publishing-into-the-void&#34; class=&#34;anchor&#34;&gt;&lt;/div&gt;&#xA;    &#xA;    &lt;span&#xA;        class=&#34;absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none&#34;&gt;&#xA;        &lt;a class=&#34;text-primary-300 dark:text-neutral-700 !no-underline&#34; href=&#34;#publishing-into-the-void&#34; aria-label=&#34;Anchor&#34;&gt;#&lt;/a&gt;&#xA;    &lt;/span&gt;&#xA;    &#xA;&lt;/h2&gt;&#xA;&lt;p&gt;The site&amp;rsquo;s &lt;code&gt;hugo.toml&lt;/code&gt; had a &lt;code&gt;googleAnalytics&lt;/code&gt; line commented out since roughly the first commit. I never uncommented it. GA4 wants a cookie disclosure, ships a chunky client, and ad blockers eat it anyway, which felt like a lot of ceremony for a personal blog whose one open question was &amp;ldquo;does anybody visit.&amp;rdquo;&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>My og:image URLs were broken for months. baseURL was the culprit.</title>
      <link>https://nick-liu.com/posts/hugo-baseurl-dual-domain-canonical/</link>
      <pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/hugo-baseurl-dual-domain-canonical/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;Paste one of my post links into a social preview and the card comes up with no image. The site itself renders fine, every page, every browser. The culprit was one character in `hugo.toml`: `baseURL = &#34;/&#34;`, which quietly turns every absolute URL the site emits into a relative one that only a browser can love.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;div class=&#34;tested-with&#34; role=&#34;note&#34;&gt;&#xA;  &lt;span class=&#34;tested-with-icon&#34; aria-hidden=&#34;true&#34;&gt;🧪&lt;/span&gt;&#xA;  &lt;span&gt;Tested with &lt;strong&gt;Hugo&lt;/strong&gt; &lt;code&gt;0.163.3&lt;/code&gt; · Blowfish 2.104&lt;/span&gt;&#xA;&lt;/div&gt;&#xA;&#xA;&#xA;&lt;h2 class=&#34;relative group&#34;&gt;The symptom&#xA;    &lt;div id=&#34;the-symptom&#34; class=&#34;anchor&#34;&gt;&lt;/div&gt;&#xA;    &#xA;    &lt;span&#xA;        class=&#34;absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none&#34;&gt;&#xA;        &lt;a class=&#34;text-primary-300 dark:text-neutral-700 !no-underline&#34; href=&#34;#the-symptom&#34; aria-label=&#34;Anchor&#34;&gt;#&lt;/a&gt;&#xA;    &lt;/span&gt;&#xA;    &#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Share cards without images, that was the visible part. View source on any page and the metadata told the fuller story:&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>A typo fix shouldn&#39;t boot a browser in CI. Mine did for months.</title>
      <link>https://nick-liu.com/posts/content-pr-ci-path-filter/</link>
      <pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/content-pr-ci-path-filter/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;I changed one sentence in a blog post, opened a pull request, and watched CI spend about 85 seconds installing a headless Chromium to confirm my prose still turned into HTML. The obvious fix, telling the workflow to ignore content paths, would have quietly broken every merge instead.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&#xA;&lt;h2 class=&#34;relative group&#34;&gt;Symptom: a browser for a one-line edit&#xA;    &lt;div id=&#34;symptom-a-browser-for-a-one-line-edit&#34; class=&#34;anchor&#34;&gt;&lt;/div&gt;&#xA;    &#xA;    &lt;span&#xA;        class=&#34;absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none&#34;&gt;&#xA;        &lt;a class=&#34;text-primary-300 dark:text-neutral-700 !no-underline&#34; href=&#34;#symptom-a-browser-for-a-one-line-edit&#34; aria-label=&#34;Anchor&#34;&gt;#&lt;/a&gt;&#xA;    &lt;/span&gt;&#xA;    &#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Every pull request on this blog runs two jobs: a &lt;code&gt;lint&lt;/code&gt; job, and a&#xA;&lt;code&gt;build-and-test&lt;/code&gt; job. The second one builds the site with Hugo, link-checks the&#xA;output with htmltest, then installs a headless Chromium and runs a Playwright&#xA;suite against a live &lt;code&gt;hugo server&lt;/code&gt;. End to end, roughly 85 seconds, and almost&#xA;all of it is Playwright.&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>Five Stream Deck keys, N Claude sessions: LRU that keeps the order I see</title>
      <link>https://nick-liu.com/posts/lru-session-eviction/</link>
      <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/lru-session-eviction/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;A Stream Deck has five session keys. I usually have six or seven Claude&#xA;Code sessions running. When a new one shows up, the muscle memory test&#xA;isn&#39;t &#34;does the right session get evicted&#34;, it is &#34;do the four&#xA;survivors stay on the keys they were already on.&#34;&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;nav class=&#34;series-progress&#34; aria-label=&#34;Series progress&#34;&gt;&#xA;    &lt;span class=&#34;series-progress-label&#34;&gt;Building ClaudeDeck · Part 3 of 10&lt;/span&gt;&#xA;    &lt;ol class=&#34;series-progress-dots&#34;&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/claude-code-hooks-reality/&#34; title=&#34;The Claude Code hooks docs are wrong. Here&amp;#39;s what&amp;#39;s actually on the wire.&#34;&gt;1&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/streamdeck-sdk-quirks/&#34; title=&#34;Two Stream Deck SDK quirks that cost me a weekend&#34;&gt;2&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-current&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/lru-session-eviction/&#34; title=&#34;Five Stream Deck keys, N Claude sessions: LRU that keeps the order I see&#34; aria-current=&#34;step&#34;&gt;3&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/statusline-side-channel/&#34; title=&#34;The Claude Code statusline is a per-turn telemetry side channel&#34;&gt;4&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/plan-usage-statusline-pivot/&#34; title=&#34;I polled an undocumented endpoint for 18 hours. The data was on stdin.&#34;&gt;5&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/permission-round-trip/&#34; title=&#34;Holding HTTP open for 590 seconds so a Stream Deck key can approve a tool call&#34;&gt;6&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/pty-wrap-migration/&#34; title=&#34;I split my daemon in two so a Node subprocess could own the PTY&#34;&gt;7&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tcc-cdhash-trap/&#34; title=&#34;TCC pins your Accessibility grant to a cdhash. Every rebuild breaks it.&#34;&gt;8&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tahoe-hotkey-dead-end/&#34; title=&#34;What replaced CGEventPost in my Stream Deck daemon&#34;&gt;9&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/launchd-bootstrap-debugging/&#34; title=&#34;launchctl unload returned 0. The daemon was still running. KeepAlive raced.&#34;&gt;10&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;/ol&gt;&#xA;  &lt;/nav&gt;&#xA;&lt;div class=&#34;tested-with&#34; role=&#34;note&#34;&gt;&#xA;  &lt;span class=&#34;tested-with-icon&#34; aria-hidden=&#34;true&#34;&gt;🧪&lt;/span&gt;&#xA;  &lt;span&gt;Tested with &lt;strong&gt;Claude Code&lt;/strong&gt; &lt;code&gt;2.1.x&lt;/code&gt; · macOS&lt;/span&gt;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;p&gt;(Two bits of context for anyone new to the stack: Stream Deck is Elgato&amp;rsquo;s&#xA;USB grid of programmable LCD keys, and a &amp;ldquo;session&amp;rdquo; here is a single&#xA;Claude Code conversation: &lt;code&gt;claude&lt;/code&gt; running in one terminal tab, with its&#xA;own working directory, its own context window, its own history. LRU&#xA;stands for &amp;ldquo;least-recently used,&amp;rdquo; the standard cache-eviction policy:&#xA;when you need to make room, drop the entry nobody has touched in the&#xA;longest time.)&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>launchctl unload returned 0. The daemon was still running. KeepAlive raced.</title>
      <link>https://nick-liu.com/posts/launchd-bootstrap-debugging/</link>
      <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/launchd-bootstrap-debugging/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;`launchctl unload ~/Library/LaunchAgents/com.nickboy.claudedeck.plist` exited 0. Then `pgrep -f claudedeck-daemon` printed a fresh PID. Three seconds after the &#34;unload succeeded&#34; line. Spoiler: KeepAlive is a polling supervisor, not an event-driven one, and when you tell launchd to tear a job down, there is a window where the supervisor has already noticed the previous PID is gone and started a replacement.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;nav class=&#34;series-progress&#34; aria-label=&#34;Series progress&#34;&gt;&#xA;    &lt;span class=&#34;series-progress-label&#34;&gt;Building ClaudeDeck · Part 10 of 10&lt;/span&gt;&#xA;    &lt;ol class=&#34;series-progress-dots&#34;&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/claude-code-hooks-reality/&#34; title=&#34;The Claude Code hooks docs are wrong. Here&amp;#39;s what&amp;#39;s actually on the wire.&#34;&gt;1&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/streamdeck-sdk-quirks/&#34; title=&#34;Two Stream Deck SDK quirks that cost me a weekend&#34;&gt;2&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/lru-session-eviction/&#34; title=&#34;Five Stream Deck keys, N Claude sessions: LRU that keeps the order I see&#34;&gt;3&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/statusline-side-channel/&#34; title=&#34;The Claude Code statusline is a per-turn telemetry side channel&#34;&gt;4&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/plan-usage-statusline-pivot/&#34; title=&#34;I polled an undocumented endpoint for 18 hours. The data was on stdin.&#34;&gt;5&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/permission-round-trip/&#34; title=&#34;Holding HTTP open for 590 seconds so a Stream Deck key can approve a tool call&#34;&gt;6&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/pty-wrap-migration/&#34; title=&#34;I split my daemon in two so a Node subprocess could own the PTY&#34;&gt;7&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tcc-cdhash-trap/&#34; title=&#34;TCC pins your Accessibility grant to a cdhash. Every rebuild breaks it.&#34;&gt;8&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tahoe-hotkey-dead-end/&#34; title=&#34;What replaced CGEventPost in my Stream Deck daemon&#34;&gt;9&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-current&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/launchd-bootstrap-debugging/&#34; title=&#34;launchctl unload returned 0. The daemon was still running. KeepAlive raced.&#34; aria-current=&#34;step&#34;&gt;10&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;/ol&gt;&#xA;  &lt;/nav&gt;&#xA;&lt;p&gt;(One-paragraph grounding if launchd isn&amp;rsquo;t your daily driver: &lt;em&gt;launchd&lt;/em&gt; is macOS&amp;rsquo;s init system, the equivalent of &lt;code&gt;systemd&lt;/code&gt; on Linux or Windows Services on Windows. It boots PID 1, brings up daemons, restarts them when they crash. A &lt;em&gt;LaunchAgent&lt;/em&gt; is a per-user launchd job, defined by an XML &lt;em&gt;plist&lt;/em&gt; (property list) at &lt;code&gt;~/Library/LaunchAgents/&amp;lt;name&amp;gt;.plist&lt;/code&gt;. &lt;em&gt;KeepAlive&lt;/em&gt; is one of the plist keys; set it to &lt;code&gt;true&lt;/code&gt; and launchd will respawn the job whenever it exits. &lt;em&gt;&lt;code&gt;launchctl&lt;/code&gt;&lt;/em&gt; is the CLI you use to load, unload, and inspect those jobs. The Linux mental model: think &lt;code&gt;systemctl&lt;/code&gt; driving &lt;code&gt;systemd&lt;/code&gt; unit files. The Stream Deck plugin and its daemon are described in &lt;a href=&#34;https://nick-liu.com/posts/tcc-cdhash-trap/&#34; &gt;the TCC cdhash trap post&lt;/a&gt; if you want the project context.)&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>TCC pins your Accessibility grant to a cdhash. Every rebuild breaks it.</title>
      <link>https://nick-liu.com/posts/tcc-cdhash-trap/</link>
      <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/tcc-cdhash-trap/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;My daemon&#39;s preflight log said `osascript is not allowed assistive access. (-1719)`. System Settings disagreed: the entry was right there, toggled on. Spoiler: ad-hoc codesigning pins TCC&#39;s designated requirement to the binary&#39;s cdhash, and `bun build --compile` produces a different cdhash on every rebuild.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;nav class=&#34;series-progress&#34; aria-label=&#34;Series progress&#34;&gt;&#xA;    &lt;span class=&#34;series-progress-label&#34;&gt;Building ClaudeDeck · Part 8 of 10&lt;/span&gt;&#xA;    &lt;ol class=&#34;series-progress-dots&#34;&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/claude-code-hooks-reality/&#34; title=&#34;The Claude Code hooks docs are wrong. Here&amp;#39;s what&amp;#39;s actually on the wire.&#34;&gt;1&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/streamdeck-sdk-quirks/&#34; title=&#34;Two Stream Deck SDK quirks that cost me a weekend&#34;&gt;2&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/lru-session-eviction/&#34; title=&#34;Five Stream Deck keys, N Claude sessions: LRU that keeps the order I see&#34;&gt;3&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/statusline-side-channel/&#34; title=&#34;The Claude Code statusline is a per-turn telemetry side channel&#34;&gt;4&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/plan-usage-statusline-pivot/&#34; title=&#34;I polled an undocumented endpoint for 18 hours. The data was on stdin.&#34;&gt;5&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/permission-round-trip/&#34; title=&#34;Holding HTTP open for 590 seconds so a Stream Deck key can approve a tool call&#34;&gt;6&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/pty-wrap-migration/&#34; title=&#34;I split my daemon in two so a Node subprocess could own the PTY&#34;&gt;7&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-current&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tcc-cdhash-trap/&#34; title=&#34;TCC pins your Accessibility grant to a cdhash. Every rebuild breaks it.&#34; aria-current=&#34;step&#34;&gt;8&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tahoe-hotkey-dead-end/&#34; title=&#34;What replaced CGEventPost in my Stream Deck daemon&#34;&gt;9&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/launchd-bootstrap-debugging/&#34; title=&#34;launchctl unload returned 0. The daemon was still running. KeepAlive raced.&#34;&gt;10&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;/ol&gt;&#xA;  &lt;/nav&gt;&#xA;&lt;p&gt;I&amp;rsquo;m building a Stream Deck plugin called &lt;a href=&#34;https://github.com/nickboy/claudedeck&#34;  target=&#34;_blank&#34; rel=&#34;noreferrer&#34;&gt;ClaudeDeck&lt;/a&gt; (Stream Deck is Elgato&amp;rsquo;s little USB grid of programmable keys with LCD displays under each one). The plugin talks to a background daemon (a long-running process that starts at login and waits for events), and that daemon needs to call System Events via AppleScript to switch Ghostty tabs (Ghostty is my terminal emulator) whenever I press a Stream Deck key. macOS gates that capability, automating other apps, through &lt;strong&gt;System Settings → Privacy &amp;amp; Security → Accessibility&lt;/strong&gt;, the pane you&amp;rsquo;ve probably toggled for tools like Rectangle or BetterTouchTool. On first install I added the daemon, toggled it on, and got back to work.&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>Two Stream Deck SDK quirks that cost me a weekend</title>
      <link>https://nick-liu.com/posts/streamdeck-sdk-quirks/</link>
      <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/streamdeck-sdk-quirks/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;Two undocumented behaviours in the Elgato Stream Deck SDK ate most of a weekend: a per-key title-alignment cache that silently ignores manifest updates, and a `willAppear` event that doesn&#39;t always re-fire after a plugin restart. The fixes are short. Finding them was not.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;nav class=&#34;series-progress&#34; aria-label=&#34;Series progress&#34;&gt;&#xA;    &lt;span class=&#34;series-progress-label&#34;&gt;Building ClaudeDeck · Part 2 of 10&lt;/span&gt;&#xA;    &lt;ol class=&#34;series-progress-dots&#34;&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/claude-code-hooks-reality/&#34; title=&#34;The Claude Code hooks docs are wrong. Here&amp;#39;s what&amp;#39;s actually on the wire.&#34;&gt;1&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-current&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/streamdeck-sdk-quirks/&#34; title=&#34;Two Stream Deck SDK quirks that cost me a weekend&#34; aria-current=&#34;step&#34;&gt;2&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/lru-session-eviction/&#34; title=&#34;Five Stream Deck keys, N Claude sessions: LRU that keeps the order I see&#34;&gt;3&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/statusline-side-channel/&#34; title=&#34;The Claude Code statusline is a per-turn telemetry side channel&#34;&gt;4&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/plan-usage-statusline-pivot/&#34; title=&#34;I polled an undocumented endpoint for 18 hours. The data was on stdin.&#34;&gt;5&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/permission-round-trip/&#34; title=&#34;Holding HTTP open for 590 seconds so a Stream Deck key can approve a tool call&#34;&gt;6&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/pty-wrap-migration/&#34; title=&#34;I split my daemon in two so a Node subprocess could own the PTY&#34;&gt;7&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tcc-cdhash-trap/&#34; title=&#34;TCC pins your Accessibility grant to a cdhash. Every rebuild breaks it.&#34;&gt;8&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tahoe-hotkey-dead-end/&#34; title=&#34;What replaced CGEventPost in my Stream Deck daemon&#34;&gt;9&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/launchd-bootstrap-debugging/&#34; title=&#34;launchctl unload returned 0. The daemon was still running. KeepAlive raced.&#34;&gt;10&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;/ol&gt;&#xA;  &lt;/nav&gt;&#xA;&lt;p&gt;(Skip this paragraph if you&amp;rsquo;ve shipped a Stream Deck plugin before. The Stream Deck is Elgato&amp;rsquo;s USB grid of programmable LCD keys, common on streamer desks for scene switching. A &amp;ldquo;plugin&amp;rdquo; is a small program (TypeScript, in my case) that runs as a child process of Elgato&amp;rsquo;s Stream Deck app, registers one or more &lt;em&gt;actions&lt;/em&gt; the user can drag onto keys, and reacts to events like &amp;ldquo;key pressed&amp;rdquo; or &amp;ldquo;key visible.&amp;rdquo; The SDK is &lt;code&gt;@elgato/streamdeck&lt;/code&gt; from npm. A &lt;em&gt;manifest&lt;/em&gt; is a &lt;code&gt;manifest.json&lt;/code&gt; next to the plugin that declares its actions, supported devices, default icons, and per-state defaults like title alignment.)&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>What replaced CGEventPost in my Stream Deck daemon</title>
      <link>https://nick-liu.com/posts/tahoe-hotkey-dead-end/</link>
      <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/tahoe-hotkey-dead-end/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;I press the Stream Deck key. The daemon logs the press, synthesizes `Cmd+Opt+;` through CoreGraphics, and exits cleanly. Wispr Flow does nothing. Three Apple subsystems and one decompiled Electron bundle later, the working trigger turned out to be a one-line URL.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;nav class=&#34;series-progress&#34; aria-label=&#34;Series progress&#34;&gt;&#xA;    &lt;span class=&#34;series-progress-label&#34;&gt;Building ClaudeDeck · Part 9 of 10&lt;/span&gt;&#xA;    &lt;ol class=&#34;series-progress-dots&#34;&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/claude-code-hooks-reality/&#34; title=&#34;The Claude Code hooks docs are wrong. Here&amp;#39;s what&amp;#39;s actually on the wire.&#34;&gt;1&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/streamdeck-sdk-quirks/&#34; title=&#34;Two Stream Deck SDK quirks that cost me a weekend&#34;&gt;2&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/lru-session-eviction/&#34; title=&#34;Five Stream Deck keys, N Claude sessions: LRU that keeps the order I see&#34;&gt;3&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/statusline-side-channel/&#34; title=&#34;The Claude Code statusline is a per-turn telemetry side channel&#34;&gt;4&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/plan-usage-statusline-pivot/&#34; title=&#34;I polled an undocumented endpoint for 18 hours. The data was on stdin.&#34;&gt;5&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/permission-round-trip/&#34; title=&#34;Holding HTTP open for 590 seconds so a Stream Deck key can approve a tool call&#34;&gt;6&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/pty-wrap-migration/&#34; title=&#34;I split my daemon in two so a Node subprocess could own the PTY&#34;&gt;7&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tcc-cdhash-trap/&#34; title=&#34;TCC pins your Accessibility grant to a cdhash. Every rebuild breaks it.&#34;&gt;8&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-current&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tahoe-hotkey-dead-end/&#34; title=&#34;What replaced CGEventPost in my Stream Deck daemon&#34; aria-current=&#34;step&#34;&gt;9&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/launchd-bootstrap-debugging/&#34; title=&#34;launchctl unload returned 0. The daemon was still running. KeepAlive raced.&#34;&gt;10&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;/ol&gt;&#xA;  &lt;/nav&gt;&#xA;&lt;p&gt;The plan was the boring kind: Stream Deck key (the physical button on Elgato&amp;rsquo;s programmable USB grid) → WebSocket message → my daemon (long-running background process) → synthesized global hotkey → Wispr Flow&amp;rsquo;s hands-free dictation starts (Wispr Flow is the voice-to-text Mac app that types your speech into the focused window) → I talk → words show up in my editor. I&amp;rsquo;d done variants of this with &lt;code&gt;osascript&lt;/code&gt; (macOS&amp;rsquo;s command-line AppleScript runner) years ago. Should have taken an afternoon.&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>Building a Knowledge Base That AI Can Actually Use</title>
      <link>https://nick-liu.com/posts/obsidian-claude-code-knowledge-base/</link>
      <pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/obsidian-claude-code-knowledge-base/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;Obsidian + Claude Code is everywhere right now. But pointing an AI at a folder of markdown files and hoping for the best doesn&#39;t work. What matters is how you structure the knowledge base. Get that right, and Claude becomes genuinely useful. Get it wrong, and you get confident garbage.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;p&gt;There&amp;rsquo;s been a wave of posts about this combo lately: &lt;a href=&#34;https://x.com/jameesy/status/2026628809424781787&#34;  target=&#34;_blank&#34; rel=&#34;noreferrer&#34;&gt;James Bedford&amp;rsquo;s full walkthrough&lt;/a&gt;, &lt;a href=&#34;https://x.com/gregisenberg/status/2026036464287412412&#34;  target=&#34;_blank&#34; rel=&#34;noreferrer&#34;&gt;Greg Isenberg&amp;rsquo;s &amp;ldquo;personal OS&amp;rdquo; approach&lt;/a&gt;, &lt;a href=&#34;https://x.com/kepano/status/2008578873903206895&#34;  target=&#34;_blank&#34; rel=&#34;noreferrer&#34;&gt;kepano (Obsidian&amp;rsquo;s CEO) sharing Claude Skills&lt;/a&gt;. They&amp;rsquo;re all worth reading.&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>What I Learned from How Claude Code&#39;s Creator Uses Claude Code</title>
      <link>https://nick-liu.com/posts/boris-claude-code-tips/</link>
      <pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/boris-claude-code-tips/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;Boris Cherny created Claude Code. When he shared how he actually uses it day-to-day, the setup was surprisingly simple. I went through every tip, tried most of them, and have opinions about all of them.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;p&gt;The original thread is on &lt;a href=&#34;https://x.com/bcherny/status/2007179832300581177&#34;  target=&#34;_blank&#34; rel=&#34;noreferrer&#34;&gt;Boris&amp;rsquo;s X account&lt;/a&gt;. A good companion site is &lt;a href=&#34;https://howborisusesclaudecode.com&#34;  target=&#34;_blank&#34; rel=&#34;noreferrer&#34;&gt;howborisusesclaudecode.com&lt;/a&gt; which compiles everything in one place.&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>Case Study: Building AWS Billing&#39;s Unbilled Usage Auditor</title>
      <link>https://nick-liu.com/posts/case-study-aws-billing-auditor/</link>
      <pubDate>Sun, 22 Feb 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/case-study-aws-billing-auditor/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;I spent five years on the AWS Billing team. The hardest problem I tackled was detecting when customers used AWS services but weren&#39;t charged correctly. This post walks through how I designed a system that reduced charge discrepancies by **300x** and eliminated **230 million** monthly false positives.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&#xA;&lt;h2 class=&#34;relative group&#34;&gt;The Problem&#xA;    &lt;div id=&#34;the-problem&#34; class=&#34;anchor&#34;&gt;&lt;/div&gt;&#xA;    &#xA;    &lt;span&#xA;        class=&#34;absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none&#34;&gt;&#xA;        &lt;a class=&#34;text-primary-300 dark:text-neutral-700 !no-underline&#34; href=&#34;#the-problem&#34; aria-label=&#34;Anchor&#34;&gt;#&lt;/a&gt;&#xA;    &lt;/span&gt;&#xA;    &#xA;&lt;/h2&gt;&#xA;&lt;p&gt;AWS billing is trickier than it looks. When a customer launches an EC2 instance, writes to S3, or queries DynamoDB, each action generates a usage record. These records flow through a pipeline that calculates charges based on the customer&amp;rsquo;s pricing plan, region, and service tier.&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>Case Study: Fleet-Scale Kernel Automation at Twitter</title>
      <link>https://nick-liu.com/posts/case-study-twitter-fleet-automation/</link>
      <pubDate>Sun, 22 Feb 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/case-study-twitter-fleet-automation/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;At Twitter, I was responsible for kernel updates across **5,000+ production servers**. Updating a kernel is risky on one machine. Doing it across a fleet, without downtime, without data loss, and without breaking the services that millions of people depend on, is a different problem entirely.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&#xA;&lt;h2 class=&#34;relative group&#34;&gt;The Problem&#xA;    &lt;div id=&#34;the-problem&#34; class=&#34;anchor&#34;&gt;&lt;/div&gt;&#xA;    &#xA;    &lt;span&#xA;        class=&#34;absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none&#34;&gt;&#xA;        &lt;a class=&#34;text-primary-300 dark:text-neutral-700 !no-underline&#34; href=&#34;#the-problem&#34; aria-label=&#34;Anchor&#34;&gt;#&lt;/a&gt;&#xA;    &lt;/span&gt;&#xA;    &#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Twitter&amp;rsquo;s production infrastructure ran on thousands of bare-metal servers across multiple data centers. Each server ran a Linux kernel that needed regular updates for security patches, performance improvements, and hardware compatibility.&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>Catppuccin Mocha: Why I Theme Everything the Same Color</title>
      <link>https://nick-liu.com/posts/catppuccin-mocha-theming/</link>
      <pubDate>Sun, 22 Feb 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/catppuccin-mocha-theming/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;Your development environment should feel like **one cohesive tool**, not a collection of unrelated windows with clashing colors. I theme everything with the same palette: &lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    Catppuccin Mocha&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;. The result is a workspace where context-switching between tools is effortless.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&#xA;&lt;h2 class=&#34;relative group&#34;&gt;Why One Palette Everywhere?&#xA;    &lt;div id=&#34;why-one-palette-everywhere&#34; class=&#34;anchor&#34;&gt;&lt;/div&gt;&#xA;    &#xA;    &lt;span&#xA;        class=&#34;absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none&#34;&gt;&#xA;        &lt;a class=&#34;text-primary-300 dark:text-neutral-700 !no-underline&#34; href=&#34;#why-one-palette-everywhere&#34; aria-label=&#34;Anchor&#34;&gt;#&lt;/a&gt;&#xA;    &lt;/span&gt;&#xA;    &#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Most developers pick a theme for their editor and call it a day. Their terminal is one color, their editor another, their tmux status bar a third, and their Git diffs something else entirely. Every time they switch contexts, their brain spends a fraction of a second recalibrating.&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>My Terminal Setup in 2026: Ghostty, tmux, and Neovim</title>
      <link>https://nick-liu.com/posts/my-terminal-setup-2026/</link>
      <pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/my-terminal-setup-2026/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;After years of refining my terminal workflow, I&#39;ve landed on a stack I genuinely enjoy using every day: **Ghostty** as the terminal emulator, **tmux** with **sesh** for session management, and **Neovim** with **LazyVim** for editing.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;p&gt;Everything runs on macOS (Apple Silicon) with a consistent &lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    Catppuccin Mocha&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA; theme across all tools.&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>Modern CLI Tools That Replaced My Unix Classics</title>
      <link>https://nick-liu.com/posts/modern-cli-tools-replacing-unix-classics/</link>
      <pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/modern-cli-tools-replacing-unix-classics/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;I&#39;ve been gradually replacing classic Unix tools with modern alternatives, mostly written in &lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    Rust&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;. After a year of daily use, these aren&#39;t experiments anymore. They&#39;re muscle memory.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&#xA;&lt;h2 class=&#34;relative group&#34;&gt;The Replacements&#xA;    &lt;div id=&#34;the-replacements&#34; class=&#34;anchor&#34;&gt;&lt;/div&gt;&#xA;    &#xA;    &lt;span&#xA;        class=&#34;absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none&#34;&gt;&#xA;        &lt;a class=&#34;text-primary-300 dark:text-neutral-700 !no-underline&#34; href=&#34;#the-replacements&#34; aria-label=&#34;Anchor&#34;&gt;#&lt;/a&gt;&#xA;    &lt;/span&gt;&#xA;    &#xA;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;&#x9;&lt;thead&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;Classic&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;Modern&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;Why&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&lt;/thead&gt;&#xA;&#x9;&lt;tbody&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;cat&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    bat&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Syntax highlighting, line numbers, git integration&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;ls&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    eza&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Icons, git status, tree view, color-coded&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;grep&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    ripgrep&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;10x faster, respects &lt;code&gt;.gitignore&lt;/code&gt;, smart case&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;find&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    fd&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Simpler syntax, respects &lt;code&gt;.gitignore&lt;/code&gt;, colored output&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;cd&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    zoxide&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Learns your habits, fuzzy matching&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;sed&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    sd&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Intuitive regex syntax, no escaping nightmare&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;du&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    dust&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Visual directory size with a tree view&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;df&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    duf&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Colorful, filterable disk usage&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;top&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    btop&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Beautiful TUI with mouse support, per-core graphs&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;ps&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    procs&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Colorized, searchable, tree view&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;history&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;span class=&#34;flex cursor-pointer&#34;&gt;&#xA;  &#xA;  &#xA;  &#xA;  &#xA;    &lt;span&#xA;      class=&#34;rounded-md border border-primary-400 px-1 py-[1px] text-xs font-normal text-primary-700 dark:border-primary-600 dark:text-primary-400&#34;&gt;&#xA;  &#xA;    atuin&#xA;  &lt;/span&gt;&#xA;&lt;/span&gt;&#xA;&#xA;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Encrypted sync, full-text search, workspace filtering&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&#xA;&lt;h3 class=&#34;relative group&#34;&gt;Setting Up Aliases&#xA;    &lt;div id=&#34;setting-up-aliases&#34; class=&#34;anchor&#34;&gt;&lt;/div&gt;&#xA;    &#xA;    &lt;span&#xA;        class=&#34;absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none&#34;&gt;&#xA;        &lt;a class=&#34;text-primary-300 dark:text-neutral-700 !no-underline&#34; href=&#34;#setting-up-aliases&#34; aria-label=&#34;Anchor&#34;&gt;#&lt;/a&gt;&#xA;    &lt;/span&gt;&#xA;    &#xA;&lt;/h3&gt;&#xA;&lt;p&gt;In my &lt;code&gt;.zshrc&lt;/code&gt;, I alias the classics to their replacements so the transition is invisible:&lt;/p&gt;</description>
      
    </item>
    
    <item>
      <title>Managing Dotfiles Like a Pro with Yadm</title>
      <link>https://nick-liu.com/posts/managing-dotfiles-with-yadm/</link>
      <pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/managing-dotfiles-with-yadm/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;Every developer eventually reaches the point where their configs become too valuable to lose. Here&#39;s how I use **yadm** to manage my macOS dotfiles with automated testing, daily maintenance, and a pre-commit workflow that keeps everything in check.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;p&gt;For me, the turning point was spending a weekend setting up a new MacBook and realizing I couldn&amp;rsquo;t reproduce my environment reliably. That&amp;rsquo;s when I started managing my dotfiles properly.&lt;/p&gt;</description>
      
    </item>
    
  </channel>
</rss>
