<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[Anthony Panozzo's Blog]]></title>
  <link href="https://www.panozzaj.com/atom.xml" rel="self"/>
  <link href="https://www.panozzaj.com/"/>
  <id>https://www.panozzaj.com/</id>
  <author>
    <name><![CDATA[Anthony Panozzo]]></name>
    <email><![CDATA[panozzaj@gmail.com]]></email>
  </author>
  <generator uri="http://octopress.org/">Octopress</generator>

  
  <entry>
    <title type="html"><![CDATA[Quick Payment Page]]></title>
    <link href="https://www.panozzaj.com/blog/2026/03/23/dollar-sign-payment-page/"/>
    <updated>2026-03-23T00:00:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2026/03/23/dollar-sign-payment-page</id>
    <content type="html"><![CDATA[

<p>Receiving money online has never been easier, but there are a couple of small problems:</p>

<ul>
  <li>everyone has preferred sending and receiving platforms</li>
  <li>if I want to get money, I end up copy/pasting my various links everywhere</li>
</ul>

<p>I wanted a link like <code class="language-plaintext highlighter-rouge">panozzaj.com/$</code> that was easy to remember so I could type it on my phone.</p>

<p>Ended up making <a href="https://www.panozzaj.com/$">a page on this blog</a> for this. Buttons link to my preferred receiving platforms so folks can pick how they want to send money.</p>

<p><img src="https://www.panozzaj.com/images/payment-page.png" alt="Payment page showing Cash App, PayPal, and Venmo buttons" /></p>

<p>It also takes an optional <code class="language-plaintext highlighter-rouge">?amount=</code> query parameter (in cents) and pre-fills the payment links. For example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>panozzaj.com/$?amount=1500
</code></pre></div></div>

<p>shows <strong>$15.00</strong> and links directly to that platform's payment page with the amount pre-filled.</p>

<h3 id="gotchas">Gotchas</h3>

<p><strong>Shell escaping.</strong> <code class="language-plaintext highlighter-rouge">$</code> triggers variable expansion in bash, so any scripts or commands that reference the file need quoting.</p>

<p>I host my blog in S3, so there were a couple of additional production tweaks.</p>

<p><strong>Jekyll permalink needs a trailing slash.</strong> <code class="language-plaintext highlighter-rouge">permalink: /$</code> generates <code class="language-plaintext highlighter-rouge">$.html</code>, which means S3 serves it at <code class="language-plaintext highlighter-rouge">panozzaj.com/$.html</code> instead of <code class="language-plaintext highlighter-rouge">panozzaj.com/$</code>. Using <code class="language-plaintext highlighter-rouge">permalink: /$/</code> makes Jekyll generate <code class="language-plaintext highlighter-rouge">$/index.html</code>, which S3 serves correctly.</p>

<p><strong>S3 adds a trailing slash redirect.</strong> Requesting <code class="language-plaintext highlighter-rouge">panozzaj.com/$</code> returns a 302 to <code class="language-plaintext highlighter-rouge">panozzaj.com/$/</code>. To avoid this, after <code class="language-plaintext highlighter-rouge">s3 sync</code>, we copy the object to an extensionless key:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3 <span class="nb">cp</span> <span class="s2">"s3://</span><span class="k">${</span><span class="nv">bucket</span><span class="k">}</span><span class="s2">/</span><span class="nv">$/</span><span class="s2">index.html"</span> <span class="s2">"s3://</span><span class="k">${</span><span class="nv">bucket</span><span class="k">}</span><span class="s2">/$"</span> <span class="se">\</span>
  <span class="nt">--content-type</span> <span class="s2">"text/html"</span>
</code></pre></div></div>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Building a <code>/reload</code> Command for Claude Code]]></title>
    <link href="https://www.panozzaj.com/blog/2026/02/07/building-a-reload-command-for-claude-code/"/>
    <updated>2026-02-07T19:49:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2026/02/07/building-a-reload-command-for-claude-code</id>
    <content type="html"><![CDATA[

<p>I often want to reload Claude Code mid-session to pick up changes to MCP servers, hooks, or other settings. The default workflow is a little clunky: I need to manually exit, then restart, then tell Claude that I restarted.</p>

<p>As a result, I wanted a <code class="language-plaintext highlighter-rouge">/reload</code> command that I could run to restart Claude and automatically continue the conversation. Also, by making it a command, <a href="https://code.claude.com/docs/en/skills">Claude can actually invoke it itself</a> when it detects a need to reload. This allows Claude to restart itself when needed without human intervention, so it can run more autonomously.</p>

<p>I figured this out when making (yet another) <a href="https://github.com/panozzaj/gdoc-mcp">Google Drive MCP</a>. I wanted to have Claude build it and automatically reload to test it.</p>

<h3 id="initial-exploration">Initial exploration</h3>

<p>Since I was in Claude Code already, I worked on this with Claude. I figured we could find an approach that worked and then codify.</p>

<h4 id="first-attempt-exit-with-a-special-code">First attempt: Exit with a special code</h4>

<p>Claude's initial idea was to have the skill exit Claude with some special exit code. Perhaps <code class="language-plaintext highlighter-rouge">exit 42</code> to signal "please reload", and a wrapper script to listen for that code. This would work well since I already typically run Claude via short custom commands, so we could hook into that.</p>

<p>I asked about reasonable or semantic exit codes, and Claude suggested using 129 rather than an arbitrary code.</p>

<details>
  <summary>Exit code details</summary>

  Claude says: SIGHUP (signal 1) is the Unix convention for "reload configuration". Daemons like nginx and Apache reload their config on SIGHUP rather than terminating. And exit code 128+N is the shell convention for "terminated by signal N." So 129 would mean "reload requested" -- semantically correct and following established Unix patterns.
</details>

<p>However, merely exiting from a Bash tool use doesn't work. Bash commands run in subprocesses, so exiting the subprocess with a special code just kills that subshell.</p>

<h4 id="second-attempt-find-a-built-in-flag">Second attempt: Find a built-in flag</h4>

<p>We looked at Claude Code's docs and <code class="language-plaintext highlighter-rouge">--help</code> for a way to pass an exit code to <code class="language-plaintext highlighter-rouge">/exit</code>, but there's no <code class="language-plaintext highlighter-rouge">/exit &lt;code&gt;</code>, <code class="language-plaintext highlighter-rouge">/reload</code>, or <code class="language-plaintext highlighter-rouge">/restart</code> built-in.</p>

<h4 id="sending-sighup-to-the-claude-process">Sending SIGHUP to the Claude process</h4>

<p>Then I asked about the Bash command terminating the parent process, which would be Claude.</p>

<p>The key insight was that from within a bash command, <a href="https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html#index-PPID"><code class="language-plaintext highlighter-rouge">$PPID</code></a> would point to the Claude process. So then:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">kill</span> <span class="nt">-HUP</span> <span class="nv">$PPID</span>
</code></pre></div></div>

<p>sends <a href="https://en.wikipedia.org/wiki/SIGHUP">SIGHUP</a> (signal 1) to Claude, which causes it to exit with code <strong>129</strong> (128 + 1).</p>

<h3 id="the-wrapper-function">The Wrapper Function</h3>

<p>The second part of this approach is to turn my short startup command into a Claude invocation that detects that special exit code and restarts.</p>

<p>Before I had:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">CL</span><span class="o">=</span><span class="s2">"claude --dangerously-skip-permissions"</span>
</code></pre></div></div>

<p>But we need to check for the special exit code and generally be a good Unix user (preserving exit codes, etc.):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>CL <span class="o">{</span>
  <span class="nb">local </span><span class="nv">continue_flag</span><span class="o">=</span><span class="s2">""</span>               <span class="c"># Whether to continue or not</span>
  <span class="nb">local </span><span class="nv">restart_msg</span><span class="o">=</span><span class="s2">""</span>                 <span class="c"># Don't send restart message the first invocation</span>
  <span class="nb">local </span>rc
  <span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do</span>                       <span class="c"># Loop forever</span>
    claude <span class="se">\ </span>                          <span class="c"># Start Claude</span>
      <span class="nt">--dangerously-skip-permissions</span> <span class="se">\ </span><span class="c">#  Don't ask for permission    &gt;:D</span>
      <span class="nv">$continue_flag</span> <span class="se">\ </span>                <span class="c">#  Second invocation? Pass `-c` to continue last session</span>
      <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span> <span class="se">\ </span>                          <span class="c">#  Pass along any original additional parameters</span>
      <span class="nv">$restart_msg</span>                     <span class="c">#  On restarts, start Claude with a "restarted" message (see below)</span>
    <span class="nv">rc</span><span class="o">=</span><span class="nv">$?</span>                              <span class="c"># Store the Claude exit code</span>
    <span class="o">[</span> <span class="nv">$rc</span> <span class="nt">-eq</span> 129 <span class="o">]</span> <span class="o">||</span> <span class="k">return</span> <span class="nv">$rc</span>      <span class="c"># If Claude exited with code 129, restart.</span>
                                       <span class="c"># Otherwise, don't restart, and exit with the original exit code.</span>
    <span class="nb">echo</span> <span class="s2">"Reloading Claude Code..."</span>    <span class="c"># Tell the human we're restarting</span>
    <span class="nb">sleep </span>0.5                          <span class="c"># Seems to somewhat improve restart likelihood?</span>
    <span class="nv">continue_flag</span><span class="o">=</span><span class="s2">"-c"</span>                 <span class="c"># Restart the previously used session</span>
    <span class="nv">restart_msg</span><span class="o">=</span><span class="s2">"restarted"</span>            <span class="c"># Send this message to Claude when the session resumes</span>
  <span class="k">done</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Sending the <code class="language-plaintext highlighter-rouge">restart_msg</code> message is quite helpful. Without it, Claude waits for user input after restart. So this allows it to continue working on whatever it was working on without needing user intervention. If you wanted to send a different message, I usually just hit <kbd>esc</kbd> to cancel the send when Claude starts up.</p>

<p>I have some other wrappers like:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">CLC</code> for running with <code class="language-plaintext highlighter-rouge">--dangerously-skip-permissions</code> and <code class="language-plaintext highlighter-rouge">--continue</code></li>
  <li><code class="language-plaintext highlighter-rouge">CLR</code> for running with <code class="language-plaintext highlighter-rouge">--dangerously-skip-permissions</code> and <code class="language-plaintext highlighter-rouge">--resume</code></li>
</ul>

<p>Claude was easily able to modify those as well. Note you'll need to reload config or start a new session to pick up wrapper changes, especially if going from alias to function.</p>

<p>I had a little trouble getting the restart consistently working and added the short <code class="language-plaintext highlighter-rouge">sleep</code>, and while it might not be necessary, it seems like it improves the likelihood that the restart works.</p>

<h3 id="the-skill">The Skill</h3>

<p>The <code class="language-plaintext highlighter-rouge">/reload</code> skill is then just a markdown file that tells Claude to run <code class="language-plaintext highlighter-rouge">kill -HUP $PPID</code>. Simple, but it took a few iterations to arrive at.</p>

<h4 id="first-versions">First versions</h4>

<p>I had Claude write the skill based on our learnings about the <code class="language-plaintext highlighter-rouge">$PPID</code>.</p>

<p>The initial working version was something like this:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Reload Claude Code (restart Claude)</span>

Reload Claude Code to pick up configuration changes (MCP servers, hooks, settings).

<span class="gu">## Steps</span>

Send SIGHUP to the Claude process (which is $PPID from bash):

<span class="p">```</span><span class="nl">bash
</span><span class="nb">kill</span> <span class="nt">-HUP</span> <span class="nv">$PPID</span>
<span class="p">```</span>

The user will let you know when the reload is complete.
</code></pre></div></div>

<p>This had Claude read the instruction and then decide to run the Bash command. That works, but it meant that Claude had to decide to call the Bash tool. Occasionally it would add commentary, ask me to restart, write current status to file (it would be reloaded soon anyway), or ask for confirmation first. This makes it less likely the restart actually happens, and it also takes longer when it does work.</p>

<h4 id="final-iteration--prefix-in-skill--command-file">Final Iteration: <code class="language-plaintext highlighter-rouge">!</code> Prefix in Skill / Command file</h4>

<p>To try to get around the non-determinism, I wanted to see if there was a way to execute a command directly from the skill.</p>

<p>It turns out that Claude Code supports a <a href="https://code.claude.com/docs/en/skills#inject-dynamic-context"><code class="language-plaintext highlighter-rouge">!</code> prefix in skill files for immediate command execution</a>. So then the command can run directly without the LLM processing it at all.</p>

<p>The final version of <code class="language-plaintext highlighter-rouge">~/.claude/commands/reload.md</code> is simply:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Reload Claude Code (restart Claude)</span>

!<span class="sb">`kill -HUP $PPID`</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">!</code> makes it fire instantly: you type <code class="language-plaintext highlighter-rouge">/reload</code>, the signal sends, and Claude restarts. The whole thing takes about a second, and either you or Claude can initiate.</p>

<h3 id="wrap-up">Wrap up</h3>

<p>This exploration was fun and educational. It has been really useful in the last couple of days when adding skills and MCP servers.</p>

<p>Want to install it for yourself? Just point your Claude Code instance to this blog post and have it add the skill and shell wrapper!</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[External Monitor Brightness Control on Mac]]></title>
    <link href="https://www.panozzaj.com/blog/2026/01/26/external-monitor-brightness-control/"/>
    <updated>2026-01-26T04:00:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2026/01/26/external-monitor-brightness-control</id>
    <content type="html"><![CDATA[

<p>About a decade ago I wrote about my <a href="https://www.panozzaj.com/blog/2016/07/17/night-working-computer-setup">night working computer setup</a>, covering tools like Dark Reader and f.lux. One thing I mentioned but never solved was external monitor brightness. I had resigned myself to fumbling through physical buttons and monitor menus whenever I wanted to dim my screens at night…</p>

<p>It turns out there's been a solution this whole time: most external monitors support a protocol called <a href="https://en.wikipedia.org/wiki/Display_Data_Channel">DDC/CI</a> that lets software control hardware brightness over the display cable.</p>

<h3 id="monitorcontrol">MonitorControl</h3>

<p><a href="https://github.com/MonitorControl/MonitorControl">MonitorControl</a> is a free, open-source Mac app that uses DDC/CI to let you control external monitor brightness. I installed it via Homebrew (<code class="language-plaintext highlighter-rouge">brew install --cask monitorcontrol</code>) and it immediately detected both my monitors. The difference is remarkable. Now I can change the brightness of the display that has the mouse cursor using the standard brightness controls.</p>

<h3 id="tools-i-still-use">Tools I still use</h3>

<p>I still regularly <a href="https://chrome.google.com/webstore/detail/dark-reader/eimadpbcbfnmbkopoojfekhnkhdbieeh">Dark Reader</a>. It is still a nice way to shift to dark color palettes. Many sites now either seem to automatically switch colors or have color scheme support, so Dark Reader is less necessary than it was in 2016.</p>

<p>I use a slightly different new tab extension now: <a href="https://chromewebstore.google.com/detail/empty-new-tab-page/dpjamkmjmigaoobjbekmfgabipmfilij?hl=en-US">Empty New Tab Page</a>.</p>

<h3 id="tools-i-no-longer-use">Tools I no longer use</h3>

<p>In the old post I recommended f.lux for turning screen colors warmer at night. macOS now has this built in as <a href="https://support.apple.com/en-us/102191">Night Shift</a>.</p>

<p>I've also think that while the blue light / melatonin mechanism is real, the practical impact from typical screen use seems smaller than the mid-2010s hype implied. Simply dimming your screens may matter more than shifting color temperature - which makes controlling monitor brightness potentially more useful than color shifting for sleep purposes.</p>

<p>I don't really use MuPDF any more since I rarely read PDFs at night at this point.</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Making Infinite Scroll Work with Client-Side Filtering]]></title>
    <link href="https://www.panozzaj.com/blog/2026/01/23/making-infinite-scroll-work-with-client-side-filtering/"/>
    <updated>2026-01-23T20:00:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2026/01/23/making-infinite-scroll-work-with-client-side-filtering</id>
    <content type="html"><![CDATA[

<p>I've been building Chrome extensions that filter content on websites with infinite scroll, and I ran into a problem: when you hide most of the items with CSS, the page becomes too short to scroll, so no new content loads.</p>

<p>This came up in two separate extensions I built - one for filtering posts on Nextdoor, another for filtering RSS entries on Feedbin. After some trial and error, I found an approach that seems to work reasonably well.</p>

<h3 id="the-problem">The Problem</h3>

<p>Many websites use infinite scroll. As you scroll down, they automatically fetch and append more content. This typically works by detecting when you've scrolled near the bottom of the page.</p>

<p>When you add client-side filtering (hiding items based on tags, keywords, etc.), you run into a problem:</p>

<ol>
  <li>You hide items with <code class="language-plaintext highlighter-rouge">display: none</code></li>
  <li>Hidden items don't take up space</li>
  <li>The page becomes too short to scroll</li>
  <li>The infinite scroll loader never triggers</li>
  <li>No new content loads</li>
  <li>User is stuck with whatever items weren't filtered out on first load</li>
</ol>

<p>This is pronounced when there is heavy filtering.</p>

<!--more-->

<h3 id="failed-approach">Failed Approach</h3>

<p>My first attempt was to programmatically scroll to the bottom after filtering:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">scrollToLoadMore</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.entries</span><span class="dl">'</span><span class="p">)</span>
  <span class="kd">const</span> <span class="nx">lastEntry</span> <span class="o">=</span> <span class="nx">container</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.entry:last-child</span><span class="dl">'</span><span class="p">)</span>
  <span class="nx">lastEntry</span><span class="p">.</span><span class="nx">scrollIntoView</span><span class="p">({</span> <span class="na">behavior</span><span class="p">:</span> <span class="dl">'</span><span class="s1">instant</span><span class="dl">'</span><span class="p">,</span> <span class="na">block</span><span class="p">:</span> <span class="dl">'</span><span class="s1">end</span><span class="dl">'</span> <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This didn't work reliably. If most entries are hidden, there might not be enough scroll height to trigger the loader at all. Scrolling to the "bottom" just snaps back to where you were.</p>

<h3 id="the-solution">The Solution</h3>

<p>The best solution I've found is to create an invisible spacer element that maintains scroll height even when most content is hidden.</p>

<p>Here's some sample CSS:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* Invisible spacer to ensure infinite scroll works when entries are filtered */</span>
<span class="nc">.scroll-spacer</span> <span class="p">{</span>
  <span class="nl">height</span><span class="p">:</span> <span class="m">200vh</span><span class="p">;</span>
  <span class="nl">visibility</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span>
  <span class="nl">pointer-events</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>

<span class="c">/* Hide spacer when enough content is visible */</span>
<span class="nc">.scroll-spacer.has-content</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The spacer is 200% of viewport height, which ensures there's always room to scroll. It's invisible (<code class="language-plaintext highlighter-rouge">visibility: hidden</code>) so it doesn't affect the visual layout, and has no pointer events so it doesn't interfere with clicks.</p>

<p>Here's the JavaScript to manage it:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">FilteredList</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">spacer</span> <span class="o">=</span> <span class="kc">null</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">MIN_VISIBLE_ITEMS</span> <span class="o">=</span> <span class="mi">15</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">loadMoreAttempts</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">maxLoadMoreAttempts</span> <span class="o">=</span> <span class="mi">10</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">lastItemCount</span> <span class="o">=</span> <span class="mi">0</span>
  <span class="p">}</span>

  <span class="nx">createSpacer</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">spacer</span><span class="p">)</span> <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">spacer</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">spacer</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">div</span><span class="dl">'</span><span class="p">)</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">spacer</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">scroll-spacer</span><span class="dl">'</span>

    <span class="kd">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.entries</span><span class="dl">'</span><span class="p">)</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">container</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">spacer</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">spacer</span>
  <span class="p">}</span>

  <span class="nx">applyFilters</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">items</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">.entry</span><span class="dl">'</span><span class="p">)</span>
    <span class="kd">let</span> <span class="nx">visibleCount</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="nx">items</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">shouldShow</span><span class="p">(</span><span class="nx">item</span><span class="p">))</span> <span class="p">{</span>
        <span class="nx">item</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">''</span>
        <span class="nx">visibleCount</span><span class="o">++</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">item</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">none</span><span class="dl">'</span>
      <span class="p">}</span>
    <span class="p">})</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">triggerLoadMoreIfNeeded</span><span class="p">(</span><span class="nx">visibleCount</span><span class="p">)</span>
  <span class="p">}</span>

  <span class="nx">triggerLoadMoreIfNeeded</span><span class="p">(</span><span class="nx">visibleCount</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">createSpacer</span><span class="p">()</span>

    <span class="c1">// Enough visible items - hide spacer</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">visibleCount</span> <span class="o">&gt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">MIN_VISIBLE_ITEMS</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">spacer</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">has-content</span><span class="dl">'</span><span class="p">)</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">loadMoreAttempts</span> <span class="o">=</span> <span class="mi">0</span>
      <span class="k">return</span>
    <span class="p">}</span>

    <span class="c1">// Show spacer to maintain scroll height</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">spacer</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">has-content</span><span class="dl">'</span><span class="p">)</span>

    <span class="c1">// Check if new items loaded since last attempt</span>
    <span class="kd">const</span> <span class="nx">totalItems</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">.entry</span><span class="dl">'</span><span class="p">).</span><span class="nx">length</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">totalItems</span> <span class="o">===</span> <span class="k">this</span><span class="p">.</span><span class="nx">lastItemCount</span> <span class="o">&amp;&amp;</span> <span class="k">this</span><span class="p">.</span><span class="nx">loadMoreAttempts</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// No new items - source is exhausted</span>
      <span class="k">return</span>
    <span class="p">}</span>

    <span class="c1">// Limit attempts to avoid infinite loops</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">loadMoreAttempts</span> <span class="o">&gt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">maxLoadMoreAttempts</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span>
    <span class="p">}</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">lastItemCount</span> <span class="o">=</span> <span class="nx">totalItems</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">loadMoreAttempts</span><span class="o">++</span>

    <span class="c1">// Scroll to trigger loading, then scroll back</span>
    <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.entries</span><span class="dl">'</span><span class="p">).</span><span class="nx">parentElement</span>
      <span class="nx">container</span><span class="p">.</span><span class="nx">scrollTo</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">container</span><span class="p">.</span><span class="nx">scrollHeight</span><span class="p">)</span>

      <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">container</span><span class="p">.</span><span class="nx">scrollTo</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
      <span class="p">},</span> <span class="mi">300</span><span class="p">)</span>
    <span class="p">},</span> <span class="mi">200</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="key-details">Key Details</h3>

<p><strong>Why 200vh?</strong> It needs to be tall enough that the page is always scrollable, regardless of how many items are hidden. 200% of viewport height guarantees this.</p>

<p><strong>Why <code class="language-plaintext highlighter-rouge">visibility: hidden</code> instead of <code class="language-plaintext highlighter-rouge">display: none</code>?</strong> We want the spacer to take up space (maintaining scroll height) but not be visible. <code class="language-plaintext highlighter-rouge">display: none</code> would defeat the purpose.</p>

<p><strong>Why scroll back up?</strong> Without this, the user's view would jump around. Scrolling to bottom triggers the loader, then we immediately scroll back so the user doesn't notice.</p>

<h3 id="detecting-new-items">Detecting New Items</h3>

<p>You'll also want a MutationObserver to detect when the page loads new items:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">observeNewItems</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.entries</span><span class="dl">'</span><span class="p">)</span>

  <span class="kd">const</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">mutations</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">hasNewItems</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="nx">mutations</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">mutation</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">mutation</span><span class="p">.</span><span class="nx">addedNodes</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">node</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">node</span><span class="p">.</span><span class="nx">matches</span><span class="p">?.(</span><span class="dl">'</span><span class="s1">.entry</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
          <span class="nx">hasNewItems</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="p">}</span>
      <span class="p">})</span>
    <span class="p">})</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">hasNewItems</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// Debounce to avoid excessive reapplication</span>
      <span class="nx">clearTimeout</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">filterTimeout</span><span class="p">)</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">filterTimeout</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">applyFilters</span><span class="p">()</span>
      <span class="p">},</span> <span class="mi">100</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="p">})</span>

  <span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">container</span><span class="p">,</span> <span class="p">{</span> <span class="na">childList</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">subtree</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This creates a nice cycle:</p>

<ol>
  <li>Apply filters → too few visible → show spacer → scroll</li>
  <li>Site loads more items → MutationObserver fires</li>
  <li>Apply filters again → check if enough visible now</li>
  <li>Repeat until enough items or source exhausted</li>
</ol>

<h3 id="conclusion">Conclusion</h3>

<p>The spacer pattern has worked well across two different extensions with different sites. This pattern should work for any site with infinite scroll where you want to do client-side filtering. The main things you need to customize are the selectors and possibly the scroll container detection.</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Vibe Coding 24/7 On A Screen For Ants]]></title>
    <link href="https://www.panozzaj.com/blog/2026/01/07/vibe-coding-24-7-on-a-screen-for-ants/"/>
    <updated>2026-01-07T09:00:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2026/01/07/vibe-coding-24-7-on-a-screen-for-ants</id>
    <content type="html"><![CDATA[

<p>Given the increasing effectiveness and autonomy of coding agents, I wanted a good way of working with them when not at my computer. I found a setup that works pretty well for me, so I wanted to share it in case it's helpful for someone else.</p>

<p>I've been using Claude Code as my primary agentic coding tool for about 6 months now, so all examples here will use that, although I'm guessing it could be used with others. Claude Code has been solid and continually improving, especially with Opus 4.5. (But I guess ask me in a few months… :D)</p>

<h3 id="tailscale">Tailscale</h3>

<p>The key part of a setup like this is having some computer that has Claude Code set up, and then being able to securely connect to it.</p>

<p>My current overall approach is to set up a secure network through <a href="https://tailscale.com/">Tailscale</a> so I can SSH to it.</p>

<div class="mermaid-diagram"><img src="data:image/png;base64,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" alt="Mermaid diagram: tailscale-ssh-flow" style="max-width: 100%; height: auto;"></div>

<p>I set up my main computer (currently M3 Max Macbook Pro) to run Tailscale. First, run the <a href="https://tailscale.com/kb/1278/tailscaled">Tailscale daemon</a> with <code class="language-plaintext highlighter-rouge">$ sudo tailscaled</code>. (This changed my computer's hostname for some reason, which was not super desirable.)</p>

<p>Then configure your computer to accept SSH connections from anyone in your <a href="https://tailscale.com/kb/1136/tailnet">tailnet</a> (basically your authenticated devices):</p>

<p><code class="language-plaintext highlighter-rouge">$ tailscale set --ssh</code></p>

<p>It seems like Tailscale has <del>two</del> (<a href="https://tailscale.com/kb/1065/macos-variants#what-are-the-differences">three</a>!) Mac interfaces:</p>
<ul>
  <li>a native Mac app (used for connecting to the Tailscale network)</li>
  <li>the same, but installable through the app store</li>
  <li>and one that's a CLI interface.</li>
</ul>

<p>You need the CLI version to be able to run the <code class="language-plaintext highlighter-rouge">tailscale ssh</code> command, and the versions seem to need to agree with one another or you get warnings and possibly issues:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Warning: client version "1.90.8-tccf4f3c7c" != tailscaled server version "1.86.4-t3149aad97-g60158502b"
The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.
</code></pre></div></div>

<p>For overall stability, it would probably be a good idea to have the main computer be some server instead of a laptop to be able to connect when the laptop is closed or sleeping. Until I get one set up, I have been using <a href="https://apps.apple.com/us/app/amphetamine/id937984704?mt=12">Amphetamine</a> to keep my Macbook Pro awake. One thing I like about Amphetamine is that it has the ability to keep the Mac awake sometimes when in clamshell mode. This is similar to the <a href="https://www.intelliscapesolutions.com/apps/caffeine/">Caffeine</a> app I previously used. I think I ran into some issue with using that on Apple silicon mac, but maybe I was looking at the wrong site.</p>

<p>I'm guessing that there are other similar services, but for my needs, Tailscale seems to work consistently well and has a generous free tier. This is a pretty simple use case, so might play around with it for having some file servers or media servers or other things in the future.</p>

<h3 id="sshing-from-your-phone">SSHing from your phone</h3>

<p>First, you need to install Tailscale on your phone and connect to the tailnet.</p>

<p>I have an Android phone, so I'm running the <a href="https://termius.com/">Termius</a> app on my phone for SSH. You can enter your computer's tailnet address into the client configuration so it is a one-button startup the next time.</p>

<p>I don't totally love the Termius app, but it's pretty decent. Small shortcomings in this context:</p>

<ul>
  <li>keyboard shortcuts are a bit finicky and some aren't supported (<kbd>shift</kbd>+<kbd>enter</kbd> for newline, for example)</li>
  <li>font doesn't support certain characters like emojis, and Claude likes to use emojis</li>
  <li>the (beta) voice dictation integration stops after like a second of inactivity, which makes it difficult to use</li>
  <li>autocorrect is off by default, and turning it on generally gives better results but sometimes it garbles the text</li>
</ul>

<p>Usually I'll issue a prompt and then want to be notified when Claude has finished/responded. To hear beep noises from your SSH session, you can type <code class="language-plaintext highlighter-rouge">/config</code>, then set <code class="language-plaintext highlighter-rouge">Notifications</code> to <code class="language-plaintext highlighter-rouge">Terminal Bell (\a)</code> and be sure your phone volume is up.</p>

<h3 id="session-persistence-with-tmux">Session persistence with <code class="language-plaintext highlighter-rouge">tmux</code></h3>

<p>One challenge with this setup is that if you get disconnected, you lose your current session and have to get back to it. To fix this, I like to use <a href="https://github.com/tmux/tmux/wiki"><code class="language-plaintext highlighter-rouge">tmux</code></a> for session persistence. (<a href="https://www.blle.co/blog/claude-code-tmux-beautiful-terminal">This random blog post has a decent overview of how to use tmux for this purpose</a>) I haven't really dialed in the tmux config, but with so little screen real estate, I'm mostly only running one window at a time anyway.</p>

<p>Sometimes the terminal will reprint your entire buffer, which is not ideal, as it is slow and you can't read the current output while it's happening. I think <a href="https://github.com/anthropics/claude-code/issues/3648">this is a scroll bug in Claude Code</a>. For me it seems to happen more when in a tmux session, but maybe that's because it's a longer session?</p>

<p>It will disconnect when you switch from home wi-fi to cell network (or vice versa), but you can just reconnect your SSH session.</p>

<h3 id="viewing-websites">Viewing websites</h3>

<p>When developing websites, it's useful to view the website. To do this, you can run <a href="https://tailscale.com/kb/1312/serve"><code class="language-plaintext highlighter-rouge">$ tailscale serve &lt;port&gt;</code></a> on the host machine. This will expose the service running on that port to your tailnet. Then you can connect by typing in the URL to your mobile browser. (Example: https:​//my-computer.tail123455.ts.net)</p>

<p>One nice thing is that this is effectively a mobile view of your site, so you can test how it works on a real device pretty easily. It's slightly slower than a normal connection, which is actually a benefit in my opinion to optimize your site. (I believe you can do this on your computer with Chrome devtools and on your phone in the same manner, but it's nice to have this option.)</p>

<h3 id="claude-code-niceties">Claude Code niceties</h3>

<p>I added the following to my global <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> file, which are good for less typing on the computer or especially on the phone:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>If I type just:
  "c", I mean "continue"
  "ss", I mean "restate current state as short summary"
  "dnmac", I mean "do not make any changes"
  "UR" (sometimes "ur") -&gt; "update requirements files accordingly"
</code></pre></div></div>

<p>I also made the following <a href="https://code.claude.com/docs/en/slash-commands#custom-slash-commands">custom command</a> (<code class="language-plaintext highlighter-rouge">/phone-mode</code>). This is a bit newer and less vetted than the other commands but it seems directionally useful.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Phone Mode Instructions

This is a shortcut for when I'm on my phone (possibly biking) or have a bad
connection and need quick, concise information.

Attempt to make solid progress with minimal back-and-forth. You are agentic and
can make progress autonomously.

If you need more details, ask specific, targeted questions rather than
open-ended ones. Ideally I could respond with a number or minimal words.

Prefer short answers, bullet points, or numbered lists.

Ideally keep responses under ten lines.

At the beginning of each response, write `[PHONE MODE]` to indicate and
remember you're in phone mode. If I want to exit phone mode, I'll explicitly
say so.
</code></pre></div></div>

<h3 id="best-kinds-of-tasks">Best kinds of tasks</h3>

<p>I have found that something that the agent can make meaningful progress on mostly unassisted is best. So something where there is heavy lifting but there are good tests or automated ways of determining goodness. I have had some good success with:</p>

<ul>
  <li>adding tests, especially when paired with code coverage metrics</li>
  <li>fixing flaky tests</li>
  <li>adding features, with tests</li>
</ul>

<p>For some specific small projects:</p>

<ul>
  <li>improving recipes in <a href="https://github.com/panozzaj/recipes">my recipes repo</a></li>
  <li>getting organized with set of Markdown files for life tasks</li>
  <li>fixing things on my blog</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Avoid Losing Work with Jujutsu (jj) for AI Coding Agents]]></title>
    <link href="https://www.panozzaj.com/blog/2025/11/22/avoid-losing-work-with-jujutsu-jj-for-ai-coding-agents/"/>
    <updated>2025-11-22T22:10:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2025/11/22/avoid-losing-work-with-jujutsu-jj-for-ai-coding-agents</id>
    <content type="html"><![CDATA[

<p>Someone in the <a href="https://slack.indyhackers.org/">Indy Hackers Slack</a> shared the following post:</p>

<blockquote class="mastodon-embed mastodon-embed-fallback" data-embed-url="https://mastodon.social/@rands/115577562153654788/embed" style="background: #FCF8FF; border-radius: 8px; border: 1px solid #C9C4DA; margin: 0; max-width: 540px; min-width: 270px; overflow: hidden; padding: 0;">
<div style="padding: 20px;">
<p lang="en" dir="ltr">My first Gemini CLI experience: "I messed up. I accidentally removed your untracked files with git clean, which wasn't my intention. I'm truly sorry. Since they weren't in git, I can't restore them easily."</p>
<p>&mdash; rands (@rands@mastodon.social) <a href="https://mastodon.social/@rands/115577562153654788">View on Mastodon</a></p>
</div>
</blockquote>
<script data-allowed-prefixes="https://mastodon.social/" async="" src="https://mastodon.social/embed.js"></script>

<p>I had also run into this problem while coding with AI agents. This seemed like an opportune time to write up some thoughts to hopefully save others some time and agony.</p>

<p>Sometimes this issue happens to me because I get too confident in the current work direction and have not committed in a while. And then one or two prompts later, I have a broken or confusing set of changes.</p>

<p>Or, as Rands's post mentions, sometimes the AI agent purposely or accidentally reverts some changes and can't recover them. Also, when Claude Code compacts context, it clears the terminal, so it often can't remember changes, and then we would be unlikely to recover.</p>

<h3 id="enter-jj">Enter <code class="language-plaintext highlighter-rouge">jj</code></h3>

<p>To try to combat this problem, I've been setting up and using <a href="https://github.com/jj-vcs/jj">Jujutsu</a> (henceforth <code class="language-plaintext highlighter-rouge">jj</code>) instances for the repos that I have. <code class="language-plaintext highlighter-rouge">jj</code> snapshots the working copy whenever you run a <code class="language-plaintext highlighter-rouge">jj</code> command, so you can use its history to see and restore changes that might have otherwise gotten lost. It also works fairly seamlessly with <code class="language-plaintext highlighter-rouge">git</code>, which basically all of my projects use at this point. It also doesn't affect other users of your projects. So in my mind there are basically no downsides to setting it up.</p>

<!-- more -->

<p>It's easy to set up <code class="language-plaintext highlighter-rouge">jj</code> alongside an existing <code class="language-plaintext highlighter-rouge">git</code> repo with <code class="language-plaintext highlighter-rouge">jj git init --colocate</code>.</p>

<p>Then there are a few commands that I've been using. I just set it up on my blog repo, and have the following info:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ jj
@  lrklqzxy panozzaj@gmail.com 2025-11-22 16:06:25 b76e8471
│  (no description set)
◆  qppwxvzp panozzaj@gmail.com 2025-11-06 10:57:53 master master@origin git_head() e9476b33
│  Add site perf audit document
...
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git log --oneline --graph --decorate | head 1
* e9476b3 (HEAD -&gt; master, origin/master, origin/HEAD) Add site perf audit document
</code></pre></div></div>

<p>So you can see from the <code class="language-plaintext highlighter-rouge">jj</code> command output that it knows about <code class="language-plaintext highlighter-rouge">git</code> commit <code class="language-plaintext highlighter-rouge">e9476b3</code> (<code class="language-plaintext highlighter-rouge">jj</code>'s identifier for that commit is <code class="language-plaintext highlighter-rouge">qppwxvzp</code>).</p>

<p>One thing I almost immediately picked up on is that <code class="language-plaintext highlighter-rouge">jj</code> commit identifiers use characters in the range of <code class="language-plaintext highlighter-rouge">[g-z]</code>, which is nice since <code class="language-plaintext highlighter-rouge">git</code> uses the hex characters (<code class="language-plaintext highlighter-rouge">[0-9,a-f]</code>) for its commit hashes. So this makes it easier to not mix up the two systems' unique identifiers. <code class="language-plaintext highlighter-rouge">jj</code> also highlights the unique starting characters in a different color, so it's easy to type out a couple of characters:</p>

<p><img src="https://www.panozzaj.com/images/jj_screenshot.png" alt="jj screenshot showing color-coded commit identifiers" /></p>

<p>From above, <code class="language-plaintext highlighter-rouge">jj</code> has a working set of changes that it currently describes with <code class="language-plaintext highlighter-rouge">lrklqzxy</code>/<code class="language-plaintext highlighter-rouge">b76e8471</code>. Or, at least it did, until I just saved this draft. Now the <code class="language-plaintext highlighter-rouge">git</code> identifier is something else, since the hash of the underlying filesystem changed.</p>

<p>The uncommitted set of changes is aliased to <code class="language-plaintext highlighter-rouge">@</code>. It's similar to the <code class="language-plaintext highlighter-rouge">git</code> working directory, but each filesystem change is actually "committed" under the hood. To save the current set of changes in <code class="language-plaintext highlighter-rouge">jj</code> and change <code class="language-plaintext highlighter-rouge">@</code>s identifier, we'd use <a href="https://steveklabnik.github.io/jujutsu-tutorial/hello-world/describing-commits.html"><code class="language-plaintext highlighter-rouge">jj describe</code></a> to write a commit message.</p>

<p>Here's where the work-saving ability comes in. With <code class="language-plaintext highlighter-rouge">jj</code>, we can view the last couple of repo changes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ jj obslog --revision @ --patch --limit 2
@  lrklqzxy panozzaj@gmail.com 2025-11-22 16:16:46 2583d144
│  (no description set)
│  -- operation 213466f024c9 snapshot working copy
│  M _drafts/using-jujutsu-jj-with-ai-coding-agents.markdown
│  Modified regular file _drafts/using-jujutsu-jj-with-ai-coding-agents.markdown:
│      ...
│    37   37: * e9476b3 (HEAD -&gt; master, origin/master, origin/HEAD) Add site perf audit document
│    38   38: ```
│    39   39:
│    40     : So you can see from the `jj` command output that it knows about git commit `e9476b3` (`jj`'s version of this is `qppwxvzp`), and has a working set of changes that it currently describes with `b76e8471`. Or, at least it did, until I just saved this draft. Now it's something else. So that working set is nicknamed `@`, and it's a pretty useful concept.
│         40: So you can see from the `jj` command output that it knows about git commit `e9476b3` (`jj`'s version of this is `qppwxvzp`).
│         41:
│         42: One thing I almost immediately picked up on is that the `jj` commit descriptors uses characters in the range of `[g-z]`, which is nice since `git` uses the hex characters (`[0-9][a-f]`) for its commit hashes. So this means that you'll never get the two systems' identifiers mixed up. It's not visible here, but `jj` also highlights the unique starting characters in a different color, so it's easy to type out a couple of characters.
│         43:
│         44: From above, `jj` has a working set of changes that it currently describes with `lrklqzxy`/`b76e8471`. Or, at least it did, until I just saved this draft. Now the git portion of it is something else, since the hash of the underlying filesystem changed. The uncommitted set of changes is nicknamed `@`, and it's a pretty useful concept. Similar to the `git` working directory, but each filesystem change is actually "committed" under the hood.
│         45:
│         46: To see this, we can see the last few changes:
│         47:
│    41   48:
│    42   49:
│    43   50:
│      ...
○  lrklqzxy hidden panozzaj@gmail.com 2025-11-22 16:10:37 1915095f
│  (no description set)
│  -- operation 217810a349cf snapshot working copy
│  M _drafts/using-jujutsu-jj-with-ai-coding-agents.markdown
│  Modified regular file _drafts/using-jujutsu-jj-with-ai-coding-agents.markdown:
│      ...
│    37   37: * e9476b3 (HEAD -&gt; master, origin/master, origin/HEAD) Add site perf audit document
│    38   38: ```
│    39   39:
│    40   40: So you can see from the `jj` command output that it knows about git commit `e9476b3` (`jj`'s version of this is `qppwxvzp`), and has a working set of changes that it currently describes with `b76e8471`. Or, at least it did, until I just saved this draft. Now it's something else. So that working set is nicknamed `@`, and it's a pretty useful concept.
│    41   41:
│    42   42:
│    43   43:
│      ...
</code></pre></div></div>

<p>So this command (<code class="language-plaintext highlighter-rouge">jj obslog --revision @ --patch --limit 2</code>) basically says:</p>

<ul>
  <li>show me the operations         (<code class="language-plaintext highlighter-rouge">obslog</code>)</li>
  <li>starting with revision <code class="language-plaintext highlighter-rouge">@</code>     (<code class="language-plaintext highlighter-rouge">--revision @</code>)</li>
  <li>show diffs                     (<code class="language-plaintext highlighter-rouge">--patch</code>)</li>
  <li>limit to the last two changes  (<code class="language-plaintext highlighter-rouge">--limit 2</code>)</li>
</ul>

<p>Finally, when you make changes to the <code class="language-plaintext highlighter-rouge">git</code> repo, <code class="language-plaintext highlighter-rouge">jj</code> is kept in sync with it.</p>

<p>Long-time users of <code class="language-plaintext highlighter-rouge">git</code> might see this as being similar to the <code class="language-plaintext highlighter-rouge">git svn</code> bridge for Subversion. You get the advantages of working with <code class="language-plaintext highlighter-rouge">git</code>, but Subversion stays the source of truth for collaboration.</p>

<h3 id="advantages-of-using">Advantages of using</h3>

<p>I started using <code class="language-plaintext highlighter-rouge">jj</code> before Claude Code had its <a href="https://code.claude.com/docs/en/checkpointing">checkpointing</a> or rewind feature. I still use this regularly since:</p>

<ol>
  <li>If you get out of the context window, you can still see past changes (which I am not confident rewind handles correctly, and as previously mentioned, at one point my console history would clear when context was compacted)</li>
  <li>It works across editors / agents, so you don't need to rely on them implementing checkpoints. Also, if I accidentally lose a file using <code class="language-plaintext highlighter-rouge">sed</code> or <code class="language-plaintext highlighter-rouge">mv</code> or something, I could likely get it back correctly.</li>
  <li>You don't need to be an expert with <code class="language-plaintext highlighter-rouge">jj</code> to use it. If you get in trouble, your agent should know enough to use <code class="language-plaintext highlighter-rouge">jj</code> to get those changes back if asked, and otherwise seems to not be trained to use it, so it won't be committing there.</li>
</ol>

<p>I don't yet fully grasp all of the underlying concepts to be able to use advanced <code class="language-plaintext highlighter-rouge">jj</code> correctly, but for this one case (recovering uncommitted changes that I actually wanted) it's saved me a couple of times. And it's just kind of a cool tool.</p>

<p>I suspect that there are things that I could do with using this to tracking incremental changes in <code class="language-plaintext highlighter-rouge">jj</code> and then batching those up for <code class="language-plaintext highlighter-rouge">git</code> commits. Almost like <code class="language-plaintext highlighter-rouge">git add --patch</code> and committing, or by saving semantic changes along the way. (Maybe this would be a useful place for the agent to track detailed change history?)</p>

<h3 id="making-snapshots-automatic">Making snapshots automatic</h3>

<p><strong>Update (2026-01-23):</strong> One thing I got wrong in my original post: <code class="language-plaintext highlighter-rouge">jj</code> doesn't have a background daemon watching for file changes. It snapshots the working copy when you run a <code class="language-plaintext highlighter-rouge">jj</code> command (like <code class="language-plaintext highlighter-rouge">jj status</code>, <code class="language-plaintext highlighter-rouge">jj log</code>, or even just <code class="language-plaintext highlighter-rouge">jj</code>). So if an agent makes changes and then crashes before any <code class="language-plaintext highlighter-rouge">jj</code> command runs, those changes won't be in the history.</p>

<p>To make snapshotting more automatic, I use two approaches:</p>

<p><strong>1. Shell hook</strong> - Add <code class="language-plaintext highlighter-rouge">jj status</code> to your shell's <code class="language-plaintext highlighter-rouge">preexec</code> so it snapshots before each command runs:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>preexec <span class="o">()</span> <span class="o">{</span>
  <span class="c"># Snapshot jj repo before command runs</span>
  <span class="o">[[</span> <span class="nt">-d</span> .jj <span class="o">]]</span> <span class="o">&amp;&amp;</span> jj status <span class="o">&gt;</span>/dev/null 2&gt;&amp;1
<span class="o">}</span>
</code></pre></div></div>

<p>Using <code class="language-plaintext highlighter-rouge">preexec</code> instead of <code class="language-plaintext highlighter-rouge">precmd</code> means it only runs when you actually execute a command, not on prompt redraws or empty enters. This captures the state right before a command you type might change things.</p>

<p><strong>2. Claude Code hooks</strong> - Snapshot at high-risk moments (session start and before context compaction).</p>

<p>In <code class="language-plaintext highlighter-rouge">~/.claude/settings.json</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"hooks"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"SessionStart"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="nl">"matcher"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"hooks"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"command"</span><span class="p">,</span><span class="w"> </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[[ -d .jj ]] &amp;&amp; jj status"</span><span class="w"> </span><span class="p">}]</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"PreCompact"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="nl">"matcher"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"hooks"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"command"</span><span class="p">,</span><span class="w"> </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[[ -d .jj ]] &amp;&amp; jj status"</span><span class="w"> </span><span class="p">}]</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The shell hook captures changes after every command you or the agent runs. The Claude Code hooks specifically target the moments when context loss is most likely:</p>
<ul>
  <li>at session start: before the agent might delete something without having read the contents (<code class="language-plaintext highlighter-rouge">git reset</code>, for example)</li>
  <li>before compaction (when the agent might forget what it had been reading).</li>
</ul>

<p>Adding a <code class="language-plaintext highlighter-rouge">PreToolUse</code> hook that runs <code class="language-plaintext highlighter-rouge">jj status</code> before each tool call works, but it adds a bit of latency. It seems like running <code class="language-plaintext highlighter-rouge">time jj status</code> only takes about 0.01s, but there might be some overhead in invoking the hook itself. Maybe not an issue if you have other <code class="language-plaintext highlighter-rouge">PreToolUse</code> hooks, since I believe they run in parallel.</p>

<h3 id="references">References</h3>

<p>I think I originally got the idea of using <code class="language-plaintext highlighter-rouge">jj</code> for this purpose on some Hacker News comment, possibly <a href="https://news.ycombinator.com/item?id=44645239">this one</a>:</p>

<blockquote>
  <p>Eh, I can see how, if you use GitButler, the porcelain is fairly irrelevant to you, but a few days ago I decided to try Jujutsu, asked Claude how I could do a few things that came up (commit, move branches, push/pull to Github). It took me ten minutes to become proficiend in Jujutsu, and now it's my VCS of choice.</p>

  <p>I still use Lazygit for the improved diffing, but, as long as you don't mind being in detached HEAD all the time, there's really no issue with doing that. JJ interoperates fine with git, but why would I use the arcane git commands when JJ will do the same thing much more straightforwardly?</p>

  <p>Also, the ability to jump from branch to branch with all my uncommitted files traveling with me is a godsend. Now I can breeze between feature development, bug fixing, copy changing, etc just by editing the commit I want. If I want multiple AI agents working on that stuff, I just make a worktree and get on with it.</p>

  <p>Not to mention that I am really liking the fact that I can describe changes (basically add commit messages) before I'm done with them, so I can see them in the tree.</p>

  <p>JJ is just all around great.</p>
</blockquote>

<p>Although <a href="https://news.ycombinator.com/item?id=45055550">a newer comment</a> also captures this idea, and is perhaps better documented (though see my update above—the "automatically captured" claim is slightly misleading):</p>

<blockquote>
  <p>With jj, every file change is automatically captured (no manual commits needed), and you can create lightweight "sandbox" revisions for each Claude Code task. When things go wrong, <code class="language-plaintext highlighter-rouge">jj undo</code> instantly reverts to any previous state. The operation log tracks everything, making it virtually impossible to lose work.</p>

  <p>The workflow becomes: let Claude Code generate messy experimental code → use <code class="language-plaintext highlighter-rouge">jj squash</code>/<code class="language-plaintext highlighter-rouge">jj split</code> to shape clean commits afterward. You get automatic checkpointing plus powerful history manipulation in one tool.</p>

  <p>I've been using jj with Claude Code for months and it's transformed how I work with coding agents - no fear of breaking things because everything is instantly reversible. The MCP integration seems like added complexity when jj's native capabilities already handle the core problem.</p>

  <p>For anyone interested in the jj + agent workflow, read my post: <a href="https://slavakurilyak.com/posts/use-jujutsu-not-git" rel="noopener">https://slavakurilyak.com/posts/use-jujutsu-not-git</a></p>
</blockquote>

<p>Also, I <a href="https://www.panozzaj.com/blog/2025/11/22/avoid-losing-work-with-jujutsu-jj-for-ai-coding-agents">wrote about making commits while writing</a> a very long time ago, and it would now seem fairly obviated with <code class="language-plaintext highlighter-rouge">jj</code>.</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[How I Generally Reduced Sugar Consumption]]></title>
    <link href="https://www.panozzaj.com/blog/2025/10/31/how-i-generally-reduced-sugar-consumption/"/>
    <updated>2025-10-31T13:00:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2025/10/31/how-i-generally-reduced-sugar-consumption</id>
    <content type="html"><![CDATA[

<p>About a decade ago after reading on the topic, I significantly cut down my added sugar consumption, and stopped drinking soda. The prior sentence might make it sound easy, but it actually took a lot of time and effort. Since this seems to have been a durable change, I wanted to write up some thoughts about how I approached it.</p>

<p>Benefits:</p>

<ul>
  <li>weight loss (about 10 pounds in my case, as an already pretty lean person)</li>
  <li>better metabolic health</li>
  <li>more stable moods and energy due to less blood sugar fluctuation</li>
</ul>

<p>The general thought that motivated me was that having basically any added refined sugar is not a positive thing, and so I wanted to limit this as much as possible. There is basically no nutritional benefit to added sugars, and it has many negative effects like metabolic dysfunctions, obesity, diabetes, inflammation, and cardiovascular disease. Since my grandfather had type 2 diabetes, this was something that I wanted to avoid.</p>

<p>For a while, I was pretty hardcore about this, and would try to avoid fruits and generally go low/no carb. I have softened to basically eat fruit as it's available or desired, and have been eating rice and bread fairly liberally.</p>

<p>My general approach now is to eliminate the obviously bad, moderate the questionable, and still enjoy treats from time to time.</p>

<!-- more -->

<h3 id="soda">Soda</h3>

<p>First, I went for a "cut back on obviously high sugar things, then eventually eliminate more if you want". Also going for reducing daily consumption before focusing too hard on rare things like parties / holidays. We're trying to reduce the area under the curve for sugar.</p>

<p>So soda intake, especially daily soda intake, would be a solid first step.</p>

<h4 id="soda-deconstruction">Soda deconstruction</h4>

<p>One important strategy for me was deconstructing / decoupling aspects of soda. What makes drinking Coca-Cola (for example) so desirable / addictive? This drink has the following qualities:</p>

<ul>
  <li>hydration</li>
  <li>carbonation</li>
  <li>caffeine</li>
  <li>flavor</li>
  <li>routine / familiarity</li>
  <li>sweetness</li>
</ul>

<p>So if I had a taste for a soda, I might try to figure out why I had that taste. Was I just thirsty? Start by drinking some water. Was I bored? Maybe drink a soda water. Was I low energy? Maybe drink coffee or tea or go for a walk. Like the taste: sometimes there are flavored sparkling waters or try Coke Zero.</p>

<p>This sometimes resulted in me literally deconstructing a drink to try reducing sugar. I'd "triple-fist" at home by having soda water, coffee, and water all next to each other. And then I could reduce how much I was consuming of the soda while getting the other desirable effects.</p>

<h4 id="not-ordering-it-not-buying-it-having-substitutes">Not ordering it, not buying it, having substitutes</h4>

<p>If you don't have it in your house or available, it's going to be impossible to consume it. So the easiest way to cut back is to not buy it.</p>

<p>You can try drinking things like soda water.</p>

<h4 id="free-poison">"Free poison"</h4>

<p>One thing that tripped me up was restaurants offering a free beverage with the meal. Often this ended up being for a fountain soda machine. I thought: "well, I don't want to waste a chance to get free soda." But then I started thinking: "if they offered me free poison, would I take it?" And my answer to that was "NO!" So this was a helpful framing to avoid consuming something that wasn't in my long term health interests.</p>

<p>Additional tips:</p>

<ul>
  <li>Sometimes there's a carbonated water button next to the Sprite, great choice</li>
  <li>Freestyle type machines typically have soda water hidden under one of the menus (look for caffeine-free section)</li>
  <li>Sometimes you can do something like an unsweetened tea</li>
  <li>Can just drink water</li>
</ul>

<h4 id="sugar-in-coffee--tea">Sugar in coffee / tea</h4>

<p>Consider reducing sugar in your daily coffee or tea.</p>

<p>You can actually <a href="https://www.bonappetit.com/story/add-salt-to-coffee">add a couple of grains of <em>salt</em></a> to reduce coffee bitterness (as well as brewing at a slightly lower temperature or higher quality bean). So that's why those salted caramel drinks are popular… (but also high in sugar).</p>

<h3 id="added-sugar-in-food">Added sugar in food</h3>

<p>After you eliminate the big or daily sources of sugar, if you want to continue, the next thing to go after would be the smaller aspects.</p>

<p>This is challenging but tractable. It just takes time, diligence, and the willingness to change habits / preferences.</p>

<p>Almost anything in the central part of the store is going to contain added sugar. Why? Sugar is a fairly cheap preservative that also increases the palatability of food. Adding sugar to food makes it last longer on the shelves and be more likely to sell. So the food companies are incentivized to add it to make more money.</p>

<p>Generally avoiding grilled meat marinates with sugar since these <a href="https://www.aicr.org/resources/blog/grilling-and-cancer-risk-what-you-need-to-know-for-a-healthier-barbecue/">may produce more free radicals / other things that are carcinogenic</a>.</p>

<p>I'm kind of OK with eating salad dressing with marginal amounts of sugar in a tough food environment if it makes it easier to eat a lot of vegetables. But most of the time at home you'll be able to get or make a vinegar and oil mixture that contains minimal sugar.</p>

<h4 id="what-to-watch-for">What to watch for</h4>

<p>Most products marketed at you contain added sugar. No one is advertising broccoli!</p>

<p>I look for products that have no sugar added or artificial sweeteners. I call these "no BS".</p>

<p>This can be quite challenging, since there are certain classes of foods that almost always contain sugar. For example, granola, cereal, bread, condiments (ketchup, BBQ sauce), and salad dressings almost always have some kind of sugar added to it.</p>

<p>Food label warning words:</p>

<ul>
  <li>"low fat": typically they replace the fat with sugar</li>
  <li>"sugar-free": they might be using a sweetener or artificial sweetener other than sugar</li>
  <li>"no added sugar(s)": might have artificial sweeteners</li>
</ul>

<p>Even (or especially) the ones marketed as being healthy, often contain a lot of added sugar. Don't be fooled by what I call "health washed" foods:</p>

<ul>
  <li>"organic": there are organic sugars. Think of this as food source certification, not healthiness</li>
  <li>"all-natural": arsenic is also all natural</li>
  <li>"non-GMO": could very well still contain a lot of sugar</li>
</ul>

<p>It's easier than it's ever been with online research and shopping online so you're not "that person" in the aisle reading every ingredient label. Then when you find versions you like, can stick with those. It's also easier since the FDA changed labeling requirements to have "added sugars" listed in the ingredient list. Sometimes if you</p>

<p>Get plain versions of things, and add your own fruit / honey to them. This works well for yogurt. Instead of 9 grams of sugar in a fruit-on-the-bottom Greek Yogurt, you can have plain full fat yogurt and add some fruit to it.</p>

<p>Sometimes I found "saving" my sugar intake for a higher quality dessert was a helpful frame. Why eat something junky when I could eat something that was really good?</p>

<h4 id="artificial-sweeteners">Artificial sweeteners</h4>

<p>I'm not sure whether artificial sweeteners are good or bad. Like – it's not sugar, but it resembles it enough that our brains (and gut bacteria?) are somewhat tricked by it. So I'm not sure what the metabolic effects of it are.</p>

<p>I generally don't like the taste, which makes it easier to spot these when I'm tasting them.</p>

<p>Don't be tricked by things that are alternate sweetener names, or the various names for artificial sweeteners.
See: https://www.nytimes.com/2016/05/22/upshot/it-isnt-easy-to-figure-out-which-foods-contain-sugar.html and https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4733620/</p>

<h3 id="desserts">Desserts</h3>

<p>This was probably one that impacted me less than the average person. I typically didn't eat much dessert. But here are some thoughts.</p>

<p>Eventually others know you to be someone who avoids sugar and will start reducing requests for desserts.</p>

<p>Fruit as a dessert. It's also sweet, and is a nice substitute. I like keeping some frozen fruit, which makes for a refreshing evening treat. You can also pair it with yogurt, etc.</p>

<h3 id="conclusion">Conclusion</h3>

<p>Hope this helps someone trying to reduce their sugar consumption. It's definitely a journey, and takes time and effort, but I find the health benefits are worth it.</p>

<p>Nowadays I can typically feel it in my finger joints the next day if I've had a lot of sugar.</p>

<p>Let me know if anything was helpful for you!</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[PlainErrors: Streamlined Rails Error Pages for LLM Agents]]></title>
    <link href="https://www.panozzaj.com/blog/2025/10/23/plainerrors-streamlined-rails-error-pages-for-llm-agents/"/>
    <updated>2025-10-23T23:00:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2025/10/23/plainerrors-streamlined-rails-error-pages-for-llm-agents</id>
    <content type="html"><![CDATA[

<p>I work a lot with Rails applications and I've been having Claude Code do some local testing and other poking around using <a href="https://github.com/microsoft/playwright-mcp">Playwright MCP</a>. However, when there are backend errors, there are be a lot of tokens returned to display the <a href="https://github.com/BetterErrors/better_errors">BetterErrors</a> page or the standard Rails development error page, which would unnecessarily fill up the context window.</p>

<p>So… I worked with Claude Code to build a new gem (<a href="https://github.com/panozzaj/plain_errors">PlainErrors</a>) that is a Rack Middleware that gives streamlined error reports for LLMs.</p>

<p>In practice, this should let the agent do more debugging and iterating without filling up the context.</p>

<!--more-->

<p>In my test with a real Rails application, PlainErrors achieves significant token reductions over both BetterErrors and the standard Rails development error page (as calculated by OpenAI's tiktoken library):</p>

<p>In my test with a real Rails application, PlainErrors achieves significant
token reductions over both BetterErrors and the standard Rails development
error page (as calculated by OpenAI's
<a href="https://github.com/openai/tiktoken"><code class="language-plaintext highlighter-rouge">tiktoken</code></a> library):</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>PlainErrors</th>
      <th>Rails Default</th>
      <th>BetterErrors</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bytes</td>
      <td>755</td>
      <td>8,854</td>
      <td>113,544</td>
    </tr>
    <tr>
      <td>Tokens</td>
      <td>217</td>
      <td>2,975 (13.7x more)</td>
      <td>25,055 (115.5x more)</td>
    </tr>
  </tbody>
</table>

<p>There's a little (mostly optional) configuration, and potentially some local tweaks to get your agent sending over the right headers or other info. (And you can also direct the agent to the gem README to install or to direct it using the query params if that's easier than digging through Claude Code's config files.)</p>

<p>As seems to be typical recently, I basically wrote no code for this, was just guiding the LLM and having it test it in an application that I was using. And cleaning up some documentation that was a bit overly verbose.</p>

<p>Open to any feedback if you use this on your application, think it's cool, or have issues or missing features!</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Set A Minimum Daily Step Goal]]></title>
    <link href="https://www.panozzaj.com/blog/2025/02/28/set-a-minimum-daily-step-goal/"/>
    <updated>2025-02-28T17:00:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2025/02/28/set-a-minimum-daily-step-goal</id>
    <content type="html"><![CDATA[

<p>I had some major lower back and leg issues in 2015 that lasted at least a year. After doing some physical therapy, I was much better off, enough so that I could play Ultimate competitively again.</p>

<p>On Father's Day weekend 2020, I tweaked my back again. This was a true Father's Day weekend. My wife was working so I watched the kids the whole weekend. I was fairly deconditioned due to skipping the gym to try to avoid getting the then-novel Covid-19. Biking was sketchy for my back since the initial injury, but that weekend I took the kids around town in a bike trailer. The weight of the kids and their stuff, combined with the fact that I was minimally exercising at the time, was too much for my back to handle. I felt it tighten up, and then kept pushing. By the time we made it home, I knew I was in trouble.</p>

<p>Stress may play a role in pain (see <a href="https://www.edbatista.com/2020/12/on-pain-and-hope.html">On Pain and Hope</a>), and 2020 was a stressful time for me personally (two young children, helping run a startup that just had its sales pipeline dry up and a cofounder leave), and the world (U.S. election, pandemic, etc.)</p>

<p>My back wasn't quite as bad off as the first time, but with the pain that I had, most days I was hardly getting out of the house. By December of that year, after starting to see the physical therapist again, I was feeling marginally better, and I decided to get a step tracker to try to walk more consistently to avoid future injury.</p>

<p>Now that I've kept it up for several years and feeling better than ever, I wanted to write up some thoughts and tips. If you're looking to do something like this, I hope that this article gets you off to a good start.</p>

<!--more-->

<h3 id="first-watch">First watch</h3>

<p>My first fitness watch was the <a href="https://www.amazon.com/Garmin-v%C3%ADvofit-activity-display-010-01847-00/dp/B077X1SQY9">Garmin Vivofit 4</a>. Here's <a href="https://www.amazon.com/gp/customer-reviews/RT8J765AZVFFQ/ref=cm_cr_arp_d_rvw_ttl?ie=UTF8&amp;ASIN=B077X1SQY9">my Amazon review of it</a></p>

<p>The reason I got this watch was the best-in-class battery life (1 year or more) since I disliked taking past watches off to charge them. This one could stay on my wrist for basically a year.</p>

<p>Surprisingly, the best feature for me was the watch's step goal streak counter. Streaks greatly motivate me. Every day I hit the step goal, it would cheerily chirp about it and show the streak counter advancing by one. Every five days in a row would result in a special animation. Small things, but they kept me going.</p>

<p>At first the watch set an automatically calculated step goal. This was motivating since I started at a low baseline. However, it was calibrated to increase when I hit the goal, and decrease when I missed it. This ended up being hard to reason about and resulted in needing to do more steps every day that I hit the goal, which was not very motivating. ("My reward for hitting my goal today is to make tomorrow even harder?") So I took a look at my recent steps achieved and set an achievable goal of (I think) 5,000 steps.</p>

<p>For the last few years my step goal has been 6000 steps. I have hit a few streaks of 250 consecutive days, and likely hit the step goal 360 days out of the year for a <em>minimum</em> of 2 million steps per year.</p>

<h3 id="set-a-floor-not-a-ceiling">Set a floor, not a ceiling</h3>

<p>I thought about increasing the step goal further, but instead decided to try to consistently hit the goal instead. <strong>I treated the step goal as a step <em>minimum</em>, not a stretch goal.</strong> Or, to put it another way, I set a floor, not a ceiling.</p>

<p>If you're interested in doing something like this, my key piece of advice is to start and stay lower than you think you need to.</p>

<p>Going for a high daily goal commits you to a lot each day. It makes it a lot harder to stick with the habit. Sure, I could do the vaunted 10,000 daily step day on occasion, but the effort (and time!) needed to achieve this day after day is very high. Time constraint was one of the things I learned while doing the <a href="https://www.panozzaj.com/blog/2015/09/12/how-i-did-5580-pushups-in-23-weeks">fitness challenge</a>.</p>

<p>When people pick exercise goals, they often imagine working out under ideal conditions. I would recommend imagining how many steps you'll want to get when:</p>

<ul>
  <li>it's going to rain all day</li>
  <li>it's 100 degrees outside and 90% humidity</li>
  <li>there are a couple of inches of snow on the ground and your phone is turning off since it's so cold</li>
  <li>you have a slight injury or pain</li>
  <li>work or family obligations are higher than normal</li>
  <li>you're traveling</li>
</ul>

<p>Also, this is the <strong>step</strong> minimum. You may be doing other exercise like swimming or cycling or strength work that won't show up.</p>

<p>Setting a step floor is probably most helpful for folks who work from home or work desk jobs. I feel that it keeps me honest. "Hmm, yeah I only got like 2000 steps today, need to move around a little!"</p>

<p>Typically I pick up a few thousand steps just by walking around the house, doing errands, etc. After doing the step goal for a while, I found out that I walk around 100 steps a minute. I know a few routes around my house that take 5, 10, 15, 20, 30, 45, 60 minutes, so it makes it easier to get close to the goal based on how many more steps I need.</p>

<h3 id="setting-yourself-up-to-succeed">Setting yourself up to succeed</h3>

<p>To maintain a long streak, you've got to minimize your chances to fail. Identify the most common reasons for failure and try to mitigate.</p>

<p>Forgetfulness is probably my biggest enemy. One thing leads to another and it just sort of slips away. Setting a few alarms/reminders at night and snoozing until I hit the goal has been the most helpful for me. After a while, it becomes part of a nightly mental check.</p>

<p>Since I can't log steps if the watch is out of battery, I monitor the watch battery and bring the charger on trips or charge it before going if it's getting close to empty.</p>

<p>Based on the walking volume, I try to get good shoes and purchase new ones every few months. If you get random aches (knees, back, etc.) then it might be a good time to switch.</p>

<p>After being cold a few times and reluctant to walk, I got this gear for the elements like warm gloves and this <a href="https://www.amazon.com/dp/B0188V5T58?ref_=ppx_hzsearch_conn_dt_b_fed_asin_title_1&amp;th=1&amp;psc=1">balaclava</a> after reading <a href="https://www.amazon.com/gp/customer-reviews/R2F0WYO48ST2ZU/ref=cm_cr_getr_d_rvw_ttl?ie=UTF8&amp;ASIN=B0188V5T58">this review</a>:</p>

<blockquote>
  <p>I work at an airport in Indiana. As you can I imagine it is super cold in winter and the winds oh my. It is an airport so nothing blocking those winds. We all survive my dressing in layers and not just a few. On average we are wearing 5 layers minimum. This hat is the best combo the built in neck gator protects my nose and mouth. So that it does not hurt to breath. The hoodie in it keeps the wind out my face. Nothing worse then those super cold nights like we just had with the artic polar. I was working in -25 and the winds make you tear up the cold made those watery eyes freeze yes my eyelashes and all froze like ice cycles….</p>
</blockquote>

<p>After a few years and a few Vivofit watches, I wanting something more durable. I ended up upgrading to the <a href="https://www.amazon.com/Garmin-Instinct-Multi-GNSS-Tracback-Graphite/dp/B09NMMN9W8">Garmin Instinct 2</a> and that has been a positive change due to the heart rate tracking while offering roughly month-long battery life on a single charge.</p>

<p>If I accidentally forgot or missed the goal by a few steps, I try not to take failure too seriously. I got way more steps than I would have otherwise, I'm helping keep my body healthy, and look at it as a chance to start a new (potentially even longer) streak.</p>

<p>I could see a treadmill being in my future since it would enable me to get steps in inclement weather more easily.</p>

<h3 id="conclusion">Conclusion</h3>

<p>Having a daily step floor has worked for me pretty well. I've missed a few days here and there, but feel like it's been a positive change and hope to continue it for the rest of my life.</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[How I Fix Issues On Open Source Projects]]></title>
    <link href="https://www.panozzaj.com/blog/2024/10/13/how-i-fix-issues-on-open-source-projects/"/>
    <updated>2024-10-13T22:30:00+00:00</updated>
    <id>https://www.panozzaj.com/blog/2024/10/13/how-i-fix-issues-on-open-source-projects</id>
    <content type="html"><![CDATA[

<p>Here's a post detailing how I typically think about fixing issues for open source projects.</p>

<h3 id="identify-an-issue">Identify an issue</h3>

<p>There are two main ways that I identify issues on code repos.</p>

<p>When I evaluate a new (to me) project, I need to figure out whether it will meet my needs. Often this will be functionality, but I also want to determine how well supported the project is. I will usually look at:</p>

<ul>
  <li>the README / wiki</li>
  <li>when the repo was created and when it was last updated</li>
  <li>a few of the most recent commits</li>
  <li>the repo's issues / pull requests tab (including recently closed ones)</li>
</ul>

<!--more-->

<p>These give me a good sense of the level of activity on the project. For example:</p>

<ul>
  <li>Is it maintained by one person, a team, or has it been mostly abandoned? (To be fair, some older projects will have lower activity since they are more stable.)</li>
  <li>Are there are a lot of issues or pull requests that have lingered without resolution?</li>
  <li>Does it seem to have enough performance, security, or other -ilities?</li>
  <li>Also, the repo itself may be deprecated or point to other alternatives to consider.</li>
</ul>

<p>As part of this investigation, I typically skim through some of the open issues to check if there are any critical things that I need to be aware of or that might impact my application. <a href="https://www.panozzaj.com/blog/2023/04/22/using-a-redlock-mutex-to-avoid-duplicate-requests">This post</a> has a good example of where this habit was useful for identifying a known issue that impacted my project.</p>

<p>The second way to identify issues is to actually use the code. There are a handful of items that I commonly find:</p>

<ul>
  <li>unclear documentation or typos</li>
  <li>broken documentation links</li>
  <li>setup instruction improvements</li>
  <li>unclear error messages</li>
  <li>incompatibility with other packages or newer language versions</li>
</ul>

<p>These are all helpful to fix. It helps out other developers for a small time commitment. Observing the amount of small frictions also provides useful information about the repo.</p>

<h3 id="find-or-create-an-issue">Find or create an issue</h3>

<p>My goal at this point is to start or advance a conversation about the issue.</p>

<p>If there's an existing open issue, I prefer using that to keep things centralized.</p>

<p>If the issue is quick, obvious, or low risk fix, then I would usually create a pull request with that change. It can be quick: fork, make the change (even using the GitHub editor), and then submit a PR.</p>

<p>Otherwise, I typically create a new issue. This gives me a chance to start a conversation around whether this is something that needs to be fixed, whether a pull request would be welcome, etc. Other people might have have run into this issue and know a fix, or will run into it in the future.</p>

<p>If I'm creating the issue, I write up as clear of a description of the issue as I can. General notes:</p>

<ul>
  <li>Start by thanking the maintainers for their work and say how the project is useful to you or how it appears promising</li>
  <li>Describe the issue at a high level</li>
  <li>Minimal reproduction of the issue</li>
  <li>Include any system details (other libraries, versions, platform, etc.)</li>
  <li>If I've looked into the code, I may provide some ideas of where the problem lies</li>
</ul>

<p>If the path to fixing is not clear, I like to ask if they also think this is an issue and whether they'd be open to a fix. This is useful since sometimes I won't hear back, and sometimes I might learn something that would save time (it's not a good fit for the project, it's actually really hard, it is planned for the next version or already released, etc.)</p>

<p>Getting buy-in for a potential fix also increases the speed / likelihood of it eventually getting merged. I think it's more likely that someone will review your changes if they've already agreed they would be useful.</p>

<p>General tone notes:</p>

<ul>
  <li>Never be demanding / condescending. Open source is typically unpaid work for someone, and may not be their current focus. The status quo is that most issues are not commented on, or even resolved, so any communication is appreciated.</li>
  <li>I assume I am fallible and my issue could be something that's due to my specific setup, ignorance, or other issues.</li>
</ul>

<p>If you're responding to an existing issue, you can follow some of these steps as well, especially if they were missing from the original post. I usually try to either add some details or move the conversation along. Things like "+1!" aren't usually helpful (can use emoji reactions for that kind of feedback.)</p>

<h3 id="consider-fixing-the-problem">Consider fixing the problem</h3>

<p>If the problem is a blocker or important and I have an angle to fix, I often try to fix the problem. Sometimes I don't have enough knowledge. It's possible that there's another repo or a fork that will get around the issue, so I might not have to fix it. Even so, opening the issue is useful since someone else might be able to fix it.</p>

<p>Typically the reference to this library from our code will be in a package file like <code class="language-plaintext highlighter-rouge">package.json</code>, <code class="language-plaintext highlighter-rouge">Gemfile</code>, etc. The eventual goal is to be able to update to the latest version of the library. The more special cases we have in our package file (pointing to a fork, pointing to a specific branch/revision), the harder it will be to upgrade the library and the more likely we are to fall behind on security updates or new features.</p>

<p>In practice, the iteration usually looks like:</p>

<ul>
  <li>Point to a fork of the project with the fix</li>
  <li>Create a PR of the project</li>
  <li>Point to the master branch of the project once it's merged</li>
  <li>Point to the latest revision once it is released</li>
</ul>

<p>So the first step is to create a fork of the project. You can usually point your main project development environment to that fork for testing. Worst case, we can test the change locally and potentially get a fix weeks or months before it's officially merged. Best case, our code will get merged and make the world a better place.</p>

<h3 id="fixing-the-problem">Fixing the problem</h3>

<p>Usually I first run the tests for the project. This helps make sure that we don't break the code when fixing our change. If there are broken tests or docs, can fix those and make a separate PR. Here is a good example of a <a href="https://github.com/phusion/frontapp/pull/22">PR that fixes tests</a>.</p>

<p>When making the fix, I try to consider how to write tests or change tests to cover the behavior in question. It's typically a faster feedback loop than testing from my main application directly. This also adds to confidence, which helps get a more timely merge.</p>

<p>I like to consider whether there are any potentially breaking changes like API changes. This saves the maintainer a step and develops a bit of empathy for the review process.</p>

<p>When creating a pull request, reference the original issue. This will usually save time since the issue should be documented and you can discuss the solution and any tradeoffs.</p>

<h3 id="building-confidence">Building confidence</h3>

<p>After you've made a good change, the outcome of the rest of the project is more about communication and trust-building. This includes the communication before the fix is ready.</p>

<p>My general approach involves imagining I am the maintainer of a system that hundreds or thousands of projects depend on. The projects want the next release of the project to work! And I am on the hook if something goes wrong. What message would you rather receive?</p>

<ol>
  <li>
    <p>Here are some changes to fix X. It works on my machine!</p>
  </li>
  <li>
    <p>Here are some changes to fix X. Here's what I did and here's why. I believe this should work since I added some tests and have been running this on my production project for the last couple of days with no issues. One thing I'm not sure about is Y, but I think this should not be a blocker given that it is also an issue in Z. I believe this does not contain any breaking API changes.</p>
  </li>
</ol>

<p>I far prefer the second message, as it demonstrates more thoughtfulness and knowledge of the system. (This reminds me a bit of some of the principles of "<a href="https://davidmarquet.com/turn-the-ship-around-book/">Turn the Ship Around!</a>")</p>

<h3 id="shepherd-the-pr">Shepherd the PR</h3>

<p>Sometimes your PR will be accepted quickly. But the reality is that most open source maintainers are busy or this project may not be a high priority for them.</p>

<p>So your goal at this point is to shepherd the PR through the merge process. Typically this is going to look like:</p>

<ul>
  <li>responding to review comments / questions / requested revisions</li>
  <li>always having a next step or responsible person identified</li>
</ul>

<p>If someone says they'll look at it tonight, tomorrow, this weekend, etc., I like to follow up with them a day or two after the last date of the range passes. This gives them a bit of leeway so it doesn't feel like you're hounding them, while still being clear that they own the next step so it doesn't fall through the cracks.</p>

<p>Also, you may need to follow up on getting a release. For example, going from v1.3 -&gt; v1.4 in the library. Just merging to the main branch is often insufficient, since often people will point to the latest released version of the code.</p>

]]></content>
  </entry>
  
</feed>
