<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="http://nickgnd.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://nickgnd.com/" rel="alternate" type="text/html" /><updated>2025-12-23T16:03:40+00:00</updated><id>http://nickgnd.com/feed.xml</id><title type="html">/home/nickgnd</title><subtitle>nickgnd - software engineer passionate about technology. sharing whatever comes to my mind, read it at your own risk.</subtitle><author><name>nickgnd</name></author><entry><title type="html">My journey on building nvim:// URL handler for clickable stacktraces</title><link href="http://nickgnd.com/posts/2025/09/15/nvim-url-handler.html" rel="alternate" type="text/html" title="My journey on building nvim:// URL handler for clickable stacktraces" /><published>2025-09-15T00:00:00+00:00</published><updated>2025-09-15T00:00:00+00:00</updated><id>http://nickgnd.com/posts/2025/09/15/nvim-url-handler</id><content type="html" xml:base="http://nickgnd.com/posts/2025/09/15/nvim-url-handler.html"><![CDATA[<p>I was scrolling through the <a href="https://www.phoenixframework.org/" target="_blank">Phoenix</a> documentation and stumbled upon one of its hidden gems: the <a href="https://hexdocs.pm/plug/Plug.Debugger.html#module-links-to-the-text-editor" target="_blank"><code class="language-plaintext highlighter-rouge">PLUG_EDITOR</code> environment variable</a>. <strong>What?</strong> When configured, it allows you to open files directly from <code class="language-plaintext highlighter-rouge">Plug.Debugger</code>’s error pages in your favorite text editor.</p>

<p><br /></p>

<p>It works beautifully out of the box with <strong>VSCode</strong> and <strong>TextMate</strong>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PLUG_EDITOR</span><span class="o">=</span><span class="s2">"vscode://file/__FILE__:__LINE__"</span>

<span class="c"># or</span>

<span class="nb">export </span><span class="nv">PLUG_EDITOR</span><span class="o">=</span><span class="s2">"txmt://open?url=file://__FILE__&amp;line=__LINE__"</span>
</code></pre></div></div>

<p>But I <em>live</em> in the terminal, my editor of choice is <strong>Neovim</strong> nowadays and I felt left out. There’s no built-in <code class="language-plaintext highlighter-rouge">nvim://</code> URL scheme, yet I really wanted the same smooth experience in Neovim, to enjoy this <strong>one-click debugging experience</strong>: no more copying and pasting filenames… Just let me click! 🐭</p>

<p><br /></p>

<p>Guess what, in this post, I’ll show you how to build a custom <code class="language-plaintext highlighter-rouge">nvim://</code> URL handler that brings <em>clickable stacktraces</em> to Neovim. We’ll start simple and evolve it into an <strong>over-engineered masterpiece</strong> based on my workflow.</p>

<p><br /></p>

<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 12px; margin: 16px 0; border-radius: 4px;">
  <strong>⚠️ WARNING:</strong> This is only for macOS, sorry :/
</div>

<p><br /></p>

<h2 id="a-simple-url-handler-for-neovim">A simple URL handler for Neovim</h2>

<p>Before doing anything crazy, I decided to start with the basics: how to create a URL handler that reads the URL, extracts the filename and line number, starts my terminal and opens the file in a new instance of <strong>Neovim</strong>. That’s already something, right?</p>

<p><br /></p>

<p>First, I turned to ChatGPT and Google searches, and I immediately realized that wasn’t as straightforward as I’d hoped. My first attempt was a complete failure—I tried creating a macOS app using <a href="https://support.apple.com/guide/automator/welcome/mac" target="_blank">Automator</a>, spent a couple of hours on it, but nothing worked. Eventually, I gave up.</p>

<p>The day after, I asked Claude and the LLM suggested me to write an <a href="https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html" target="_blank">AppleScript</a> for registering the custom URL schema and parsing the URL, then shelling out to a Bash script for doing the rest. <strong>Yay!</strong></p>

<p><br /></p>

<p>This approach had its own quirks too—sometimes the custom URL schema wasn’t properly registered and I didn’t know how to observe/debug it, apart from seeing my <code class="language-plaintext highlighter-rouge">nvim://...</code> URL being ignored by the handler and opened as a Google search 😅. But eventually I made it work!</p>

<p><br /></p>

<h3 id="custom-url-format">Custom URL format</h3>

<p>Easy. I kept it consistent with the format used by <strong>VSCode</strong>, but with the nvim prefix: <code class="language-plaintext highlighter-rouge">nvim://file/__FILE__:__LINE__</code>.</p>

<p><br /></p>

<h3 id="next-applescript">Next, AppleScript</h3>

<p>Now that we’ve defined the URL format, we need to write an AppleScript that:</p>

<ul>
  <li><strong>registers</strong> the custom URL handler</li>
  <li><strong>parses</strong> the URL and calls a bash script with the result</li>
</ul>

<p>I was not familiar with AppleScript, I therefore resorted to Claude again to help me write the script.</p>

<p><br /></p>

<p>You can create a new one by opening the <strong>Script Editor</strong> application on your Mac, then copy and paste the following code with the right adjustments based on your setup:</p>

<div class="language-applescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">on</span> <span class="nb">open location</span><span class="w"> </span><span class="nv">schemeUrl</span><span class="w">
   </span><span class="c1">-- Save original delimiters</span><span class="w">
  </span><span class="k">set</span><span class="w"> </span><span class="nv">oldDelims</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w">

  </span><span class="k">try</span><span class="w">
    </span><span class="c1">-- Validate and extract the file path from the URL</span><span class="w">
    </span><span class="c1">-- Split the URL on "nvim://file/" to separate protocol from path</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">{</span><span class="s2">"nvim://file/"</span><span class="p">}</span><span class="w">

     </span><span class="c1">-- Check if we have at least 2 parts (before and after the delimiter)</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">count</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">schemeUrl</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">then</span><span class="w"> </span><span class="k">error</span><span class="w"> </span><span class="s2">"Invalid URL format"</span><span class="w">

    </span><span class="c1">-- Extract everything after "nvim://file/" (this is our file path with line number)</span><span class="w">
    </span><span class="c1">-- For "nvim://file/path/to/file.txt:42", this gives us "path/to/file.txt:42"</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">filePath</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">item</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">the</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">schemeUrl</span><span class="w">

    </span><span class="c1">-- Pass file path to the bash script</span><span class="w">
    </span><span class="nb">do shell script</span><span class="w"> </span><span class="s2">"/bin/bash "</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nb">quoted form</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="s2">"/PATH/TO/YOUR/nvim_url_handler.sh"</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="s2">" "</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nb">quoted form</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">filePath</span><span class="w">

  </span><span class="nb">on</span><span class="w"> </span><span class="k">error</span><span class="w"> </span><span class="nv">errMsg</span><span class="w">
    </span><span class="nb">display alert</span><span class="w"> </span><span class="s2">"Error processing URL: "</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">errMsg</span><span class="w">
  </span><span class="k">end</span><span class="w"> </span><span class="k">try</span><span class="w">

  </span><span class="c1">-- Restore original delimiters</span><span class="w">
  </span><span class="k">set</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nv">oldDelims</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="nb">open location</span><span class="w">
</span></code></pre></div></div>

<p>Then, save it as <code class="language-plaintext highlighter-rouge">application</code> in the Applications folder, for example as <code class="language-plaintext highlighter-rouge">Nvim URL Handler.app</code>:</p>

<p><img src="/assets/images/nvim_url_handler_apple_script_app.png" alt="Save AppleScript as Application" /></p>

<p>And for registering the URL Schema application you need to update the <code class="language-plaintext highlighter-rouge">Info.plist</code> file inside the newly created app:</p>

<ul>
  <li>Right click on the application you just created</li>
  <li>Select “Show Package Contents”</li>
  <li>Open the <code class="language-plaintext highlighter-rouge">Info.plist</code> file</li>
  <li>Find the lines</li>
</ul>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;key&gt;</span>CFBundleName<span class="nt">&lt;/key&gt;</span>
<span class="nt">&lt;string&gt;</span>...<span class="nt">&lt;/string&gt;</span>
</code></pre></div></div>

<p>And append after them:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;key&gt;</span>CFBundleURLTypes<span class="nt">&lt;/key&gt;</span>
<span class="nt">&lt;array&gt;</span>
  <span class="nt">&lt;dict&gt;</span>
    <span class="nt">&lt;key&gt;</span>CFBundleURLName<span class="nt">&lt;/key&gt;</span>
    <span class="nt">&lt;string&gt;</span>Nvim Scheme<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;key&gt;</span>CFBundleURLSchemes<span class="nt">&lt;/key&gt;</span>
    <span class="nt">&lt;array&gt;</span>
      <span class="nt">&lt;string&gt;</span>nvim<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;/array&gt;</span>
  <span class="nt">&lt;/dict&gt;</span>
<span class="nt">&lt;/array&gt;</span>
</code></pre></div></div>

<p>👆This registers the <code class="language-plaintext highlighter-rouge">nvim://</code> URL schema to your application… Don’t forget to save the file.</p>

<p><br /></p>

<h3 id="bash-script-to-open-the-file-in-nvim">Bash script to open the file in Nvim</h3>

<p>You might have noticed that the AppleScript calls a bash script with the extracted file path. This script is responsible for opening the file in Neovim.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="nv">FILE_PATH</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>

<span class="k">if</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$FILE_PATH</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="c"># Open a new instance of alacritty and run nvim with the provided file path</span>
    /opt/homebrew/bin/alacritty <span class="nt">-e</span> /opt/homebrew/bin/nvim <span class="s2">"</span><span class="nv">$FILE_PATH</span><span class="s2">"</span>
<span class="k">fi</span>
</code></pre></div></div>

<p>I’m using the wonderful <a href="https://github.com/alacritty/alacritty" target="_blank">Alacritty</a>, but you can replace it with your favorite terminal emulator. Then, ensure the <code class="language-plaintext highlighter-rouge">nvim</code> path is correct too (I installed it with <code class="language-plaintext highlighter-rouge">brew</code>).</p>

<p><strong>Last touch</strong>, make it executable <code class="language-plaintext highlighter-rouge">chmod +x /path/to/your/script.sh</code> and double check that the script path is the same as the one specified in the Apple Script above.</p>

<p><br /></p>

<p>If all the stars are aligned ✨, you should be able to open a file in Neovim by clicking on a link like <code class="language-plaintext highlighter-rouge">nvim://file/path/to/file.txt:42</code> in your browser 🤞.</p>

<p><br /></p>

<h2 id="the-over-engineered-masterpiece">The over-engineered masterpiece</h2>

<p><strong>Nice</strong>, we are now able to open a file in nvim by clicking on a link 🤩</p>

<p>But I found spawning a new shell not so convenient, I wanted the file to appear in my <strong>current terminal</strong> and instance of Neovim, so I started to think about how to achieve that based on my workflow and setup, by taking advantage of Neovim <em>special</em> features, like <a href="https://neovim.io/doc/user/api.html#RPC" target="_blank">RPC</a>.</p>

<p><br /></p>

<p>🥁🥁🥁 And I ended up with the following <em>solution</em>:</p>

<ol>
  <li>Start Neovim with <strong>socket listening</strong> on a specific path based on the current tmux session name</li>
  <li>Update the Apple Script to handle <code class="language-plaintext highlighter-rouge">nvim:// ...</code> URL with <strong>tmux session name</strong> as additional query param</li>
  <li>Update the bash script to open the file on the <strong>running Nvim instance</strong> in the specific tmux session</li>
</ol>

<blockquote>
  <p>The over-engineering is actually thoughtful engineering - you’ve created a robust tool that handles the complexity of multiple tmux sessions and nvim instances gracefully. (Claude)</p>
</blockquote>

<h3 id="1-start-neovim-server-with-socket-address-based-on-tmux-session">1. Start Neovim server with socket address based on tmux session</h3>

<p>I believe I once watched this <a href="https://www.youtube.com/watch?v=xO5yMutC-rM" target="_blank">video</a> by <a href="https://x.com/teej_dv" target="_blank">@teej</a> on <code class="language-plaintext highlighter-rouge">nvim --remote</code> and how to use it, it stuck with me and I only needed a reason to use it.</p>

<p>Neovim starts a <a href="https://neovim.io/doc/user/api.html#RPC" target="_blank">MessagePack-RPC</a> server at <a href="https://neovim.io/doc/user/starting.html#startup" target="_blank">startup</a>, creating a socket with a random address by default. The <code class="language-plaintext highlighter-rouge">--listen</code> option lets you specify a custom socket path that other processes can connect to.</p>

<p><br /></p>

<p>I’m a <strong>tmux user</strong>, and I have an <a href="https://github.com/nickgnd/dotfiles/blob/8bc6d07f1104e7741b57a7895cf7f3c072b0249a/fish/config.fish.symlink#L57-L65" target="_blank">alias</a> to start a tmux session named as the folder I’m in, which most of the time is the name of the Git repository. I therefore opted to re-use this convention and name the socket path after the folder name.</p>

<p>I created a pair of bash function and alias to accomplish that, an excerpt:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nvim_listen<span class="o">()</span> <span class="o">{</span>
  <span class="c"># Get current tmux session name</span>
  <span class="nv">TMUX_SESSION_NAME</span><span class="o">=</span><span class="si">$(</span>tmux display-message <span class="nt">-p</span> <span class="s1">'#S'</span><span class="si">)</span>

  <span class="c"># Set socket path based on session name</span>
  <span class="nv">SOCKET_PATH</span><span class="o">=</span><span class="s2">"/tmp/nvim-</span><span class="nv">$TMUX_SESSION_NAME</span><span class="s2">"</span>

  <span class="nb">echo</span> <span class="s2">"Nvim will listen on socket: </span><span class="nv">$SOCKET_PATH</span><span class="s2">"</span>

  <span class="c"># Start Neovim with the socket</span>
  <span class="nb">command </span>nvim <span class="nt">--listen</span> <span class="s2">"</span><span class="nv">$SOCKET_PATH</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
<span class="o">}</span>

<span class="c"># Start nvim listening to a socket if in tmux session</span>
nvim_with_socket<span class="o">()</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$TMUX</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"Using tmux session-aware Neovim"</span>
        nvim_listen <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
    <span class="k">else
        </span><span class="nb">command </span>nvim <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
    <span class="k">fi</span>
<span class="o">}</span>

<span class="c"># Open nvim in the current folder with socket listening</span>
<span class="nb">alias </span><span class="nv">nn</span><span class="o">=</span><span class="s2">"nvim_with_socket ."</span>
</code></pre></div></div>

<p><strong>To recap</strong>, typing <code class="language-plaintext highlighter-rouge">nn</code> in a tmux session will open the current directory in Neovim and configure it to listen on a socket named after the current tmux session.</p>

<p><br /></p>

<h3 id="2-update-the-apple-script-to-parse-the-tmux-session-param">2. Update the Apple Script to parse the tmux-session param</h3>

<p>Next, I needed to update the AppleScript to handle the new <code class="language-plaintext highlighter-rouge">tmux-session</code> query param in the URL. The updated script looks like this:</p>

<div class="language-applescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">on</span> <span class="nb">open location</span><span class="w"> </span><span class="nv">schemeUrl</span><span class="w">
  </span><span class="c1">-- Save original delimiters</span><span class="w">
  </span><span class="k">set</span><span class="w"> </span><span class="nv">oldDelims</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w">

  </span><span class="k">try</span><span class="w">
    </span><span class="c1">-- Extract everything after nvim://</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">{</span><span class="s2">"nvim://"</span><span class="p">}</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">count</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">schemeUrl</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">then</span><span class="w"> </span><span class="k">error</span><span class="w"> </span><span class="s2">"Invalid URL format"</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">fullContent</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">item</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">the</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">schemeUrl</span><span class="w">

    </span><span class="c1">-- Extract the file path (everything between file/ and ?)</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">{</span><span class="s2">"file/"</span><span class="p">}</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">count</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">fullContent</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">then</span><span class="w"> </span><span class="k">error</span><span class="w"> </span><span class="s2">"Invalid URL format, missing file path"</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">pathWithQuery</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">item</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">the</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">fullContent</span><span class="w">

    </span><span class="c1">-- Split path and query</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">{</span><span class="s2">"?"</span><span class="p">}</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">filePath</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">item</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">the</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">pathWithQuery</span><span class="w">

    </span><span class="c1">-- Extract tmux session from query if present</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">tmuxSession</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">""</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">count</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">pathWithQuery</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">then</span><span class="w">
      </span><span class="k">set</span><span class="w"> </span><span class="nv">queryPart</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">item</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">the</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">pathWithQuery</span><span class="w">

      </span><span class="c1">-- Look for tmux-session parameter</span><span class="w">
      </span><span class="k">set</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">{</span><span class="s2">"tmux-session="</span><span class="p">}</span><span class="w">
      </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">count</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">queryPart</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">then</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">tmuxSession</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">item</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">the</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">queryPart</span><span class="w">

        </span><span class="c1">-- Handle additional query parameters (if any)</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">{</span><span class="s2">"&amp;"</span><span class="p">}</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">tmuxSession</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">item</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">the</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="nb">items</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">tmuxSession</span><span class="w">
      </span><span class="k">end</span><span class="w"> </span><span class="k">if</span><span class="w">
    </span><span class="k">end</span><span class="w"> </span><span class="k">if</span><span class="w">

    </span><span class="c1">-- Pass tmux session name and file path to the bash script</span><span class="w">
    </span><span class="nb">do shell script</span><span class="w"> </span><span class="s2">"/bin/bash "</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nb">quoted form</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="s2">"/PATH/TO/SCRIPT.sh"</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="s2">" "</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nb">quoted form</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">tmuxSession</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="s2">" "</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nb">quoted form</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">filePath</span><span class="w">

  </span><span class="nb">on</span><span class="w"> </span><span class="k">error</span><span class="w"> </span><span class="nv">errMsg</span><span class="w">
    </span><span class="nb">display alert</span><span class="w"> </span><span class="s2">"Error processing URL: "</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">errMsg</span><span class="w">
  </span><span class="k">end</span><span class="w"> </span><span class="k">try</span><span class="w">

  </span><span class="c1">-- Restore original delimiters</span><span class="w">
  </span><span class="k">set</span><span class="w"> </span><span class="nb">AppleScript</span>'s <span class="nb">text item delimiters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nv">oldDelims</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="nb">open location</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<h3 id="3-update-the-bash-script-to-open-the-file-on-the-running-neovim-instance">3. Update the bash script to open the file on the running Neovim instance</h3>

<p>And finally, I updated the bash script to handle the tmux session name and open the file in the running Nvim instance:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># URL Example: nvim://file//Users/nico/dotfiles/README.md:10?tmux-session=dotfiles</span>

<span class="c"># Extract socket and path from arguments</span>
<span class="nv">TMUX_SESSION_NAME</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="nv">FILE_PATH</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>

<span class="c"># Check if both arguments were provided</span>
<span class="k">if</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$TMUX_SESSION_NAME</span><span class="s2">"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$FILE_PATH</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="c"># Build the socket path (see /bin/nvim_listen script)</span>
    <span class="nv">SOCKET_PATH</span><span class="o">=</span><span class="s2">"/tmp/nvim-</span><span class="nv">$TMUX_SESSION_NAME</span><span class="s2">"</span>

    <span class="c"># Check if the socket exists</span>
    <span class="k">if</span> <span class="o">[</span> <span class="nt">-e</span> <span class="s2">"</span><span class="nv">$SOCKET_PATH</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
        <span class="c"># Try connecting to existing nvim instance</span>
        /opt/homebrew/bin/nvim <span class="nt">--server</span> <span class="s2">"</span><span class="nv">$SOCKET_PATH</span><span class="s2">"</span> <span class="nt">--remote</span> <span class="s2">"</span><span class="nv">$FILE_PATH</span><span class="s2">"</span>

        <span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-ne</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
            <span class="c"># Fall back to opening in a new instance if remote connection fails</span>
            /opt/homebrew/bin/alacritty <span class="nt">-e</span> /opt/homebrew/bin/nvim <span class="s2">"</span><span class="nv">$FILE_PATH</span><span class="s2">"</span>
        <span class="k">fi
    else</span>
        <span class="c"># Socket doesn't exist, fall back to opening in a new window</span>
        /opt/homebrew/bin/alacritty <span class="nt">-e</span> /opt/homebrew/bin/nvim <span class="s2">"</span><span class="nv">$FILE_PATH</span><span class="s2">"</span>
    <span class="k">fi
elif</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$FILE_PATH</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="c"># Only file path provided (no socket), open in new Alacritty window</span>
    /opt/homebrew/bin/alacritty <span class="nt">-e</span> /opt/homebrew/bin/nvim <span class="s2">"</span><span class="nv">$FILE_PATH</span><span class="s2">"</span>
<span class="k">fi</span>
</code></pre></div></div>

<p>And <strong>voilà!</strong> Now I can click on a link like <code class="language-plaintext highlighter-rouge">nvim://file/path/to/file.txt:42?tmux-session=dotfiles</code> in my browser and it will open the file in the current Neovim instance running in the tmux session named <code class="language-plaintext highlighter-rouge">dotfiles</code>. <strong>Over-engineered?</strong> Maybe, but it works like a charm and I love it!</p>

<p><br /></p>

<p>Ah I almost forgot why we are here! Now I can use the <code class="language-plaintext highlighter-rouge">PLUG_EDITOR</code> environment variable to use the <code class="language-plaintext highlighter-rouge">nvim://</code> custom URL schema in my Phoenix application:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PLUG_EDITOR <span class="o">=</span> <span class="s2">"nvim://file/__FILE__:__LINE__?tmux-session=name-of-the-folder"</span>
</code></pre></div></div>

<p>And here’s the <strong>final result</strong> in action:</p>

<p><img src="/assets/images/plug_editor_with_nvim_url_handler.gif" alt="Clickable stacktraces opening in Neovim" /></p>

<p><br /></p>

<h2 id="one-final-unnecessary-optimization-dynamic-session-names-with-mise">One Final Unnecessary Optimization: Dynamic Session Names with Mise</h2>

<p>If you scroll up, you can see that the tmux session name in our <code class="language-plaintext highlighter-rouge">PLUG_EDITOR</code> environment variable is hardcoded. What a shame, we can do better!</p>

<p>I’m using <a href="https://mise.jdx.dev/" target="_blank">Mise</a> as tool version manager and more, and with that you can also handle environment variables dynamically scoped to a specific folder 🤩.</p>

<p><br /></p>

<p>With Mise, we can automatically set the <code class="language-plaintext highlighter-rouge">PLUG_EDITOR</code> variable with the <strong>current tmux session name</strong>. Here’s how I set it up in my Elixir projects:</p>

<p><br /></p>

<p>First, create a <code class="language-plaintext highlighter-rouge">.mise.toml</code> file in your project root:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[env]</span>
<span class="c"># Source a script to set dynamic environment variables</span>
<span class="py">_.source</span> <span class="p">=</span> <span class="s">"~/dotfiles/bin/mise-env.sh"</span>

<span class="py">PLUG_EDITOR</span> <span class="p">=</span> <span class="py">"nvim://file/__FILE__:__LINE__?tmux-session</span><span class="o">=</span><span class="p">{</span><span class="err">{env.TMUX_SESSION_NAME</span><span class="p">}</span><span class="err">}</span><span class="s">"</span><span class="err">
</span></code></pre></div></div>

<p><br /></p>

<p>Then, create the <code class="language-plaintext highlighter-rouge">mise-env.sh</code> script that dynamically extracts the tmux session name and sets the corresponding <code class="language-plaintext highlighter-rouge">TMUX_SESSION_NAME</code> env variable.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>

<span class="k">if</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$TMUX</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">export </span><span class="nv">TMUX_SESSION_NAME</span><span class="o">=</span><span class="si">$(</span>tmux display-message <span class="nt">-p</span> <span class="s1">'#S'</span><span class="si">)</span>
<span class="k">else
    </span><span class="nb">export </span><span class="nv">TMUX_SESSION_NAME</span><span class="o">=</span><span class="s2">""</span>
<span class="k">fi</span>
</code></pre></div></div>

<p><br /></p>

<p>Now, whenever you <code class="language-plaintext highlighter-rouge">cd</code> into your project directory (assuming you have Mise installed and configured), it will:</p>

<ol>
  <li>Detect you’re in a tmux session</li>
  <li>Extract the current tmux session name</li>
  <li>Set <code class="language-plaintext highlighter-rouge">PLUG_EDITOR</code> with the correct tmux session name automatically as query param</li>
</ol>

<p>No more hardcoding session names! The URL handler will always open files in the correct Neovim instance for your current tmux session. :chef-kiss:</p>

<p><br /></p>

<h2 id="complete-implementation">Complete Implementation</h2>

<p>If you want to use this setup, you can find my full implementation in my dotfiles:</p>

<ul>
  <li><strong><a href="https://github.com/nickgnd/dotfiles/tree/main/neovim_url_handler" target="_blank">Complete URL Handler Implementation</a></strong> - Both the AppleScript and bash script with all the error handling and edge cases</li>
  <li><strong><a href="https://github.com/nickgnd/dotfiles/blob/main/bin/nvim_listen" target="_blank">nvim_listen Script</a></strong> - The bash function that starts Neovim with session-specific sockets</li>
  <li><strong><a href="https://github.com/nickgnd/dotfiles/blob/main/fish/config.fish.symlink#L168" target="_blank">Fish Shell Configuration</a></strong> - The <code class="language-plaintext highlighter-rouge">nn</code> alias and <code class="language-plaintext highlighter-rouge">nvim_with_socket</code> function (for Fish shell 🐟)</li>
  <li><strong><a href="https://github.com/nickgnd/dotfiles/blob/main/bin/mise-env.sh" target="_blank">mise-env script</a></strong> - The script that sets the <code class="language-plaintext highlighter-rouge">TMUX_SESSION_NAME</code> env variable dynamically</li>
</ul>

<p>The code includes additional features like <strong>logging</strong>, <strong>error handling</strong>, and has been <strong>battle-tested</strong> in my daily workflow for a few months now. Feel free to adapt it to your own setup!</p>

<p><br /></p>

<p>Hope my journey will spare you some headaches and help you enjoy <strong>clickable stacktraces</strong> in Neovim as much as I do! If you have any questions or suggestions, feel free to reach out.</p>

<p><br /></p>

<p>Happy hacking! 🚀
:wq</p>]]></content><author><name>nickgnd</name></author><summary type="html"><![CDATA[I was scrolling through the Phoenix documentation and stumbled upon one of its hidden gems: the PLUG_EDITOR environment variable. What? When configured, it allows you to open files directly from Plug.Debugger’s error pages in your favorite text editor.]]></summary></entry><entry><title type="html">Multiplexed Vibes with tmux-mcp</title><link href="http://nickgnd.com/posts/2025/03/24/tmux-mcp.html" rel="alternate" type="text/html" title="Multiplexed Vibes with tmux-mcp" /><published>2025-03-24T00:00:00+00:00</published><updated>2025-03-24T00:00:00+00:00</updated><id>http://nickgnd.com/posts/2025/03/24/tmux-mcp</id><content type="html" xml:base="http://nickgnd.com/posts/2025/03/24/tmux-mcp.html"><![CDATA[<p>I started using tmux in 2018 (?) and I never looked back. I typically have 5-6 different <code class="language-plaintext highlighter-rouge">tmux</code> sessions at any given time. Each with a different context: a web server, logs I’m monitoring, or simply neovim.</p>

<p><br /></p>

<p><em>(If you don’t know what’s tmux, check <a href="https://www.youtube.com/watch?v=-B5VDp50daI" target="_blank">this</a> out)</em></p>

<p><br /></p>

<p>When I started using Claude as my personal “tuttofare” (italian expression for handyman that can do everything), I quickly found myself bored by copying and pasting terminal content or taking screenshots just to give Claude the context it needed. I thought: “There must be a better way to connect these worlds.”</p>

<p><br /></p>

<p>And here we are! <code class="language-plaintext highlighter-rouge">tmux-mcp</code> grew out of this discomfort. A simple but powerful bridge that connects Claude AI directly to your tmux sessions, eliminating that tedious back-and-forth.</p>

<p><br /></p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%;">
  <iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/3W0pqRF1RS0?si=7PB-XjAiC4fWXZkW&amp;vq=hd720" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
</div>

<p><br /></p>

<p>Show me the code! Here’s the <a href="https://github.com/nickgnd/tmux-mcp" target="_blank">repo</a>.</p>

<h2 id="what-is-tmux-mcp">What Is tmux-mcp?</h2>

<p><code class="language-plaintext highlighter-rouge">tmux-mcp</code> is an implementation of <a href="https://www.anthropic.com/news/model-context-protocol" target="_blank">Model Context Protocol</a> (MCP) that enables Claude (and possibly any other AI assistant) to:</p>

<ul>
  <li>View and navigate your tmux sessions, windows, and panes</li>
  <li>Capture and analyze terminal output in real-time</li>
  <li>Execute commands on your behalf (when you permit it) and get the result back</li>
  <li>Create new tmux sessions and windows programmatically</li>
</ul>

<p>But there is more!</p>

<ul>
  <li>Panes’ content are treated as <em>resources</em>: every tmux pane’s content can be attached to your chat context, making it instantly visible to Claude.</li>
  <li>Same for command results: when Claude executes a command in a tmux pane, its results also become a resource that can be attached to the conversation (it’s available for 10 minutes).</li>
</ul>

<p><img src="/assets/images/tmux-mcp-panes-as-resource.png" alt="tmux panes as MCP resource" /></p>

<p>This means Claude can now see what you’re seeing in your terminal, understand the context of your work, and provide more targeted assistance without you having to copy and paste terminal output.</p>

<h3 id="how-it-works">How It Works</h3>

<p>The secret sauce behind <code class="language-plaintext highlighter-rouge">tmux-mcp</code> is Anthropic’s protocol for connecting AI assistants to external systems. Explaining how it works is outside the scope of this blog post, you can find a good introduction <a href="https://www.anthropic.com/news/model-context-protocol" target="_blank">here</a>.</p>

<p><br /></p>

<p>At high level, the MCP server acts as a “translator” between tmux and the Claude, exposing tmux’s functionality through standardized MCP <a href="https://modelcontextprotocol.io/docs/concepts/resources" target="_blank">resources</a> and <a href="https://modelcontextprotocol.io/docs/concepts/tools" target="_blank">tools</a>. When you ask Claude about something happening in your terminal, it can query the appropriate tmux pane, read the content, and analyze it - all seamlessly (you can read more on how to build a custom MCP <a href="https://modelcontextprotocol.io/tutorials/building-mcp-with-llms" target="_blank">here</a>).</p>

<h2 id="use-cases">Use Cases</h2>

<h3 id="debugging">Debugging</h3>

<p>Imagine you’re debugging a crash in your web application. Instead of copying and pasting error logs to Claude, you can simply ask: <em>“Claude, can you help me understand what’s causing the crash in my web app? It’s in session ‘webapp’“</em>. Claude can now look at your terminal output, understand the context, and provide a more informed analysis.</p>

<h3 id="guided-command-execution">Guided Command Execution</h3>

<p>Working with unfamiliar tools or commands? Let Claude help: <em>“I need to parse this JSON log file and extract all events with status ‘error’. Can you help me craft the command?”</em></p>

<p>Claude can suggest a command and, with your permission, execute it in the appropriate pane and analyze the results.</p>

<h3 id="pair-programming">Pair Programming</h3>

<p>Uncle Claude can access your terminal output by attaching the pane to the chat and provide contextual suggestions:</p>

<p><em>“I can see from your terminal that the command failed. You typed <code class="language-plaintext highlighter-rouge">git comit</code> instead of <code class="language-plaintext highlighter-rouge">git commit</code>. Try running it again with the correct spelling.”</em></p>

<h2 id="security-considerations">Security Considerations</h2>

<p>The ability to execute commands is powerful but comes with responsibilities. With <code class="language-plaintext highlighter-rouge">tmux-mcp</code>, Claude can suggest commands to run in your terminal, and if you approve, the MCP server will execute them directly in the specified pane. It’s important to understand that:</p>

<ul>
  <li>You should carefully review any command Claude suggests before approving its execution</li>
  <li>You maintain control by choosing whether to approve command execution</li>
  <li>Commands are sent directly to the specified tmux pane after the approval</li>
  <li>All executed commands and their outputs remain visible in your terminal history</li>
</ul>

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

<p>I’m still tweaking and testing <code class="language-plaintext highlighter-rouge">tmux-mcp</code> to see if it fits my workflow, but this MCP has some potential: no more copy/pasta or screenshots - just seamless collaboration.</p>

<p><br /></p>

<p>I hope other tmux fans will find this <a href="https://github.com/nickgnd/tmux-mcp" target="_blank">small project</a> useful in their daily work. Give it a try, and let me know what you think, feedback are always welcome. Happy hacking 🤖!</p>]]></content><author><name>nickgnd</name></author><summary type="html"><![CDATA[I started using tmux in 2018 (?) and I never looked back. I typically have 5-6 different tmux sessions at any given time. Each with a different context: a web server, logs I’m monitoring, or simply neovim.]]></summary></entry><entry><title type="html">But wait, there’s more! Hacking LLMs thinking time</title><link href="http://nickgnd.com/posts/2025/02/10/hacking-LLMs-thinking-time.html" rel="alternate" type="text/html" title="But wait, there’s more! Hacking LLMs thinking time" /><published>2025-02-10T00:00:00+00:00</published><updated>2025-02-10T00:00:00+00:00</updated><id>http://nickgnd.com/posts/2025/02/10/hacking-LLMs-thinking-time</id><content type="html" xml:base="http://nickgnd.com/posts/2025/02/10/hacking-LLMs-thinking-time.html"><![CDATA[<p>Can we make Large Language Models (LLMs) think longer and better? A recent <a href="https://arxiv.org/abs/2501.19393" target="_blank">paper</a> explores “test-time scaling” - a fascinating technique that allows us to control an LLM’s reasoning process by manipulating its “thinking time”. This capability was first demonstrated by <a href="https://openai.com/o1/" target="_blank">OpenAI’s o1 model</a>, though without revealing their methodology.</p>

<p><br /></p>

<p>I discovered this paper through a fascinating <a href="https://www.youtube.com/watch?v=JoLimVExFdE" target="_blank">video</a> by <a href="https://github.com/antirez/" target="_blank">Antirez</a> (Salvatore Sanfilippo - hacker, creator of <a href="https://github.com/redis/redis" target="_blank">Redis</a> and much more), where he explores the researchers’ findings and demonstrates a practical implementation of this theory.</p>

<p><br /></p>

<p>As the paper explains:</p>

<blockquote>
  <p>Test-time scaling is a promising new approach to language modeling that uses extra test-time compute to improve performance. Recently, OpenAI’s o1 model showed this capability but did not publicly share its methodology, leading to many replication efforts.</p>
</blockquote>

<h2 id="understanding-test-time-scaling">Understanding Test-time Scaling</h2>

<p>Test-time scaling is an innovative approach to improve LLM performance without modifying the model itself.</p>

<ul>
  <li>
    <p><strong>Traditional Scaling</strong>: Models are improved by using larger architectures, more training data, and longer training times.</p>
  </li>
  <li>
    <p><strong>Test-Time Scaling</strong>: Performance is enhanced during inference by manipulating how the model generates responses, without changing its parameters.</p>
  </li>
</ul>

<h2 id="reasoning-process-manipulation">Reasoning process manipulation</h2>

<p>The paper illustrate a clever, or as Antirez called it <em>diabolical</em>, methodology called “budget forcing”. This technique manipulates the model’s reasoning process by either forcefully terminating the thinking process or lengthening it by appending “Wait” tokens.
This forces the model to think longer and potentially improve its answers.</p>

<p><br /></p>

<p><em>That’s smart, isn’t it?</em></p>

<h2 id="antirez-poc-demonstration">Antirez PoC demonstration</h2>

<p>I was impressed by Antirez’s demonstration of testing the paper’s concepts using a small model on his local MacBook. He leveraged <a href="https://github.com/ml-explore/mlx" target="_blank">MLX</a>, an array framework optimized for machine learning on Apple Silicon, to implement the experiments.</p>

<p><br /></p>

<p>With the combination of a specific prompt and 5 lines of code, he could force the model to continue reasoning, endlessly 🤯.</p>

<ul>
  <li>the prompt instructs the LLM to use a chain of thoughts style for the response with the reasoning wrapped in the <code class="language-plaintext highlighter-rouge">&lt;think&gt; ... &lt;/think&gt;</code> tags. Example:</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>What<span class="s1">'s the capital of Italy?
Before replying, do a chain of thoughts style thinking between &lt;think&gt; &lt;/think&gt; tags.
Open and close the tags ONLY once. At the end: reply.
</span></code></pre></div></div>

<ul>
  <li>then, Antirez modified the inference step by replacing the end token <code class="language-plaintext highlighter-rouge">&lt;/think&gt;</code> with <code class="language-plaintext highlighter-rouge">Wait</code> to extended reasoning, as described in the paper.</li>
</ul>

<h2 id="hands-on-">Hands on 👨‍💻</h2>

<p>This idea of directly controlling the chain of thought was very intriguing and I wanted to explore it myself.</p>

<p>To get started, I installed the <a href="https://pypi.org/project/mlx-lm/" target="_blank">mlx-lm</a> package to get familiar:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>mlx-lm
</code></pre></div></div>

<p>After installing it, you can use it directly from the command line to generate text (the first time it will download the <code class="language-plaintext highlighter-rouge">mlx-community/Llama-3.2-3B-Instruct-4bit</code> model, which is the one used by default). Side note, there are already a bunch of models (Llama, Mistral, DeepSeek, Qwen, … ) that have been converted to run on Apple Silicon with MLX, check out the <a href="https://github.com/ml-explore/mlx-examples/tree/main/llms#supported-models" target="_blank">official docs in GH</a> and in <a href="https://huggingface.co/docs/hub/en/mlx" target="_blank">Hugging Face</a>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ mlx_lm.generate <span class="nt">--prompt</span> <span class="s2">"what's the capital of Italy?"</span>
Fetching 6 files: 100%|█████████████████████████████████████████████████████████| 6/6 <span class="o">[</span>00:00&lt;00:00, 75800.67it/s]
<span class="o">==========</span>
The capital of Italy is Rome <span class="o">(</span>Italian: Roma<span class="o">)</span><span class="nb">.</span>
<span class="o">==========</span>
Prompt: 42 tokens, 317.802 tokens-per-sec
Generation: 12 tokens, 69.677 tokens-per-sec
Peak memory: 1.859 GB
</code></pre></div></div>

<p>Great, now that I got familiar with the tool, it was time for me to follow Antirez steps and start hacking.</p>

<ul>
  <li>First, I wanted to download the <code class="language-plaintext highlighter-rouge">mlx-lm</code> repo locally in order to change the <code class="language-plaintext highlighter-rouge">mlx_ml.generate</code> function.</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git@github.com:ml-explore/mlx-examples.git
</code></pre></div></div>

<p><em>(yes, the <code class="language-plaintext highlighter-rouge">mlx_lm</code> CLI is implemented in the <code class="language-plaintext highlighter-rouge">ml-explore/mlx-examples</code> repo)</em></p>

<ul>
  <li>Time to look for the <code class="language-plaintext highlighter-rouge">generate</code> utility</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ fd <span class="nt">--type</span> f generate
flux/static/generated-mlx.png
llava/generate.py
llms/gguf_llm/generate.py
llms/mlx_lm/examples/generate_response.py
llms/mlx_lm/examples/pipeline_generate.py
llms/mlx_lm/generate.py
llms/tests/test_generate.py
musicgen/generate.py
stable_diffusion/generated-mlx.png
</code></pre></div></div>

<p>Bingo <code class="language-plaintext highlighter-rouge">llms/mlx_lm/generate.py</code> !</p>

<p><br /></p>

<p>To modify the inference process, we need to understand how the code works at a high-level. The main chain of function calls starts from <code class="language-plaintext highlighter-rouge">generate.py</code> and flows through several functions in <code class="language-plaintext highlighter-rouge">utils.py</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>generate.py (main)
  └── utils.py
      ├── generate()
      │   └── stream_generate()
      │       └── generate_step()
      │           └── _step()  # This is where inference happens
</code></pre></div></div>

<p>The key function we need to modify is <a href="https://github.com/ml-explore/mlx-examples/blob/6120a5f3763788f2444e082875b917925b80afa5/llms/mlx_lm/utils.py#L277-L293" target="_blank"><code class="language-plaintext highlighter-rouge">_step()</code></a> within <code class="language-plaintext highlighter-rouge">generate_step()</code>. This is where the actual token inference takes place and where we’ll implement our token replacement strategy.</p>

<p><br /></p>

<p>Let’s examine how the <code class="language-plaintext highlighter-rouge">_step</code> function works. At its core, it processes input tokens and produces probabilities for the next token in the sequence. Here’s a simplified look at its implementation:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_step</span><span class="p">(</span><span class="n">y</span><span class="p">):</span>
    <span class="k">with</span> <span class="n">mx</span><span class="p">.</span><span class="nf">stream</span><span class="p">(</span><span class="n">generation_stream</span><span class="p">):</span>
        <span class="c1"># Generates logits from the model using the prompt as input token
</span>        <span class="n">logits</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="bp">None</span><span class="p">],</span> <span class="n">cache</span><span class="o">=</span><span class="n">prompt_cache</span><span class="p">)</span>
        <span class="c1"># Get logits for last token only
</span>        <span class="n">logits</span> <span class="o">=</span> <span class="n">logits</span><span class="p">[:,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="p">:]</span>

        <span class="c1"># Apply any logits processors if specified, I believe in this case no one is performed
</span>        <span class="c1"># (default value is `None` in the `generate_step` function)
</span>        <span class="k">if</span> <span class="n">logits_processors</span><span class="p">:</span>
            <span class="k">nonlocal</span> <span class="n">tokens</span>
            <span class="n">tokens</span> <span class="o">=</span> <span class="n">mx</span><span class="p">.</span><span class="nf">concat</span><span class="p">([</span><span class="n">tokens</span><span class="p">,</span> <span class="n">y</span><span class="p">])</span> <span class="k">if</span> <span class="n">tokens</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">y</span>

            <span class="k">for</span> <span class="n">processor</span> <span class="ow">in</span> <span class="n">logits_processors</span><span class="p">:</span>
                <span class="n">logits</span> <span class="o">=</span> <span class="nf">processor</span><span class="p">(</span><span class="n">tokens</span><span class="p">,</span> <span class="n">logits</span><span class="p">)</span>

        <span class="c1"># Optimization, let's ignore them
</span>        <span class="nf">quantize_cache_fn</span><span class="p">(</span><span class="n">prompt_cache</span><span class="p">)</span>

        <span class="c1"># Convert logits to log probabilities
</span>        <span class="n">logprobs</span> <span class="o">=</span> <span class="n">logits</span> <span class="o">-</span> <span class="n">mx</span><span class="p">.</span><span class="nf">logsumexp</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="c1"># Sample next token using the provided sampler, which i believe is `argmax` by default
</span>        <span class="n">y</span> <span class="o">=</span> <span class="nf">sampler</span><span class="p">(</span><span class="n">logprobs</span><span class="p">)</span>

        <span class="c1"># Return the next token `y` and the log probabilities
</span>        <span class="c1"># `logprobs` for each token in the vocabularity
</span>        <span class="k">return</span> <span class="n">y</span><span class="p">,</span> <span class="n">logprobs</span><span class="p">.</span><span class="nf">squeeze</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</code></pre></div></div>

<p>With this understanding of the <code class="language-plaintext highlighter-rouge">_step</code> function, we can now implement our test-time scaling modification.</p>

<ul>
  <li>Copy/paste what Antirez did and voila, Les jeux sont faits 🎉</li>
</ul>

<p>I modified the <code class="language-plaintext highlighter-rouge">_step</code> function to intercept the generation of the “end of thinking” token <code class="language-plaintext highlighter-rouge">&lt;/think&gt;</code> and replace it with the <code class="language-plaintext highlighter-rouge">Wait</code> token, which would prompt the model to continue reasoning.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">_step</span><span class="p">(</span><span class="n">y</span><span class="p">):</span>
        <span class="k">with</span> <span class="n">mx</span><span class="p">.</span><span class="nf">stream</span><span class="p">(</span><span class="n">generation_stream</span><span class="p">):</span>
            <span class="n">logits</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="bp">None</span><span class="p">],</span> <span class="n">cache</span><span class="o">=</span><span class="n">prompt_cache</span><span class="p">)</span>
            <span class="n">logits</span> <span class="o">=</span> <span class="n">logits</span><span class="p">[:,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="p">:]</span>

            <span class="k">if</span> <span class="n">logits_processors</span><span class="p">:</span>
                <span class="k">nonlocal</span> <span class="n">tokens</span>
                <span class="n">tokens</span> <span class="o">=</span> <span class="n">mx</span><span class="p">.</span><span class="nf">concat</span><span class="p">([</span><span class="n">tokens</span><span class="p">,</span> <span class="n">y</span><span class="p">])</span> <span class="k">if</span> <span class="n">tokens</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">y</span>

                <span class="k">for</span> <span class="n">processor</span> <span class="ow">in</span> <span class="n">logits_processors</span><span class="p">:</span>
                    <span class="n">logits</span> <span class="o">=</span> <span class="nf">processor</span><span class="p">(</span><span class="n">tokens</span><span class="p">,</span> <span class="n">logits</span><span class="p">)</span>

            <span class="nf">quantize_cache_fn</span><span class="p">(</span><span class="n">prompt_cache</span><span class="p">)</span>

            <span class="n">logprobs</span> <span class="o">=</span> <span class="n">logits</span> <span class="o">-</span> <span class="n">mx</span><span class="p">.</span><span class="nf">logsumexp</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
            <span class="n">y</span> <span class="o">=</span> <span class="nf">sampler</span><span class="p">(</span><span class="n">logprobs</span><span class="p">)</span>

            <span class="c1">### HACK START
</span>            <span class="c1">#
</span>            <span class="k">if</span> <span class="bp">True</span><span class="p">:</span>
                <span class="c1"># Converts a token ID to its text representation
</span>                <span class="n">token_text</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">.</span><span class="n">_tokenizer</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="nf">int</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span>
                <span class="k">if</span> <span class="sh">"</span><span class="s">&lt;/</span><span class="sh">"</span> <span class="ow">in</span> <span class="n">token_text</span><span class="p">:</span>
                    <span class="c1"># Converts text back to token ID
</span>                    <span class="n">alt_token</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">.</span><span class="n">_tokenizer</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="sh">"</span><span class="s"> Wait, </span><span class="sh">"</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
                    <span class="n">y</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">alt_token</span>
            <span class="c1">#
</span>            <span class="c1">### HACK END
</span>
            <span class="k">return</span> <span class="n">y</span><span class="p">,</span> <span class="n">logprobs</span><span class="p">.</span><span class="nf">squeeze</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">if True</code> condition is just a convenient way to enable/disable the <code class="language-plaintext highlighter-rouge">&lt;/think&gt;</code> token replacement, useful for running some tests.</p>

<p><br /></p>

<p>Now, that’s time to play 🥁.</p>

<h2 id="results">Results</h2>

<p>I experimented by running the modified code, and indeed this trick force the LLM to generate longer chains of thought, and sometimes with better results.</p>

<h3 id="methodology">Methodology</h3>

<ul>
  <li>Run the prompt with no chain-of-thought</li>
  <li>Run the prompt with the chain-of-thought</li>
  <li>Run the prompt with the chain-of-thought and hack to extend the reasoning</li>
</ul>

<p>To simplify the testing I created a small script to run the custom implementation, with the <em>hack</em> disabled/enabled depending on the tested case. The script simply executes the <code class="language-plaintext highlighter-rouge">main</code> function from the <code class="language-plaintext highlighter-rouge">generate.py</code> module loaded from the local repository 🤓.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ <span class="nb">cat </span>llms/run_mlx_lm_generate.sh
<span class="c">#!/Users/nickgnd/.local/share/mise/installs/python/3.12.4/bin/python3.12</span>
<span class="c"># -*- coding: utf-8 -*-</span>
import re
import sys
from mlx_lm.generate import main
<span class="k">if </span>__name__ <span class="o">==</span> <span class="s1">'__main__'</span>:
    sys.exit<span class="o">(</span>main<span class="o">())</span>
</code></pre></div></div>

<p>Let’s see some examples.</p>

<p><br /></p>

<h3 id="a-classic---how-many-r-letters-are-there-in-the-word-strawberry">A classic - how many R letters are there in the word strawberry?</h3>

<p>I couldn’t resist, I tested Llama (<code class="language-plaintext highlighter-rouge">mlx-community/Llama-3.2-3B-Instruct-4bit</code>) with this classic question.</p>

<blockquote>
  <p>Hi. how many r letters are there in the word strawberry?</p>
</blockquote>

<h4 id="regular-version-">Regular version ❌</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ mlx_lm.generate <span class="nt">--prompt</span> <span class="s2">"Hi. how many r letters are there in the word strawberry?"</span>
<span class="o">==========</span>
Let<span class="s1">'s count the R'</span>s <span class="k">in </span>the word <span class="s2">"strawberry"</span><span class="nb">.</span>

Here<span class="s1">'s the correct count:

1. S
2. T
3. R
4. A
5. W
6. B
7. E
8. R
9. R
10. Y

There are 2 R'</span>s <span class="k">in </span>the word <span class="s2">"strawberry"</span><span class="nb">.</span>
<span class="o">==========</span>
Prompt: 48 tokens, 346.599 tokens-per-sec
Generation: 75 tokens, 63.538 tokens-per-sec
Peak memory: 1.862 GB
</code></pre></div></div>

<h4 id="chain-of-thought---no-wait-replacement-">Chain of thought - NO “Wait” replacement ❌</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ ./run_mlx_lm_generate.sh <span class="nt">--prompt</span> <span class="s2">"Hi. how many r letters are there in the word strawberry? Before replying, do a chain of thoughts style thinking between &lt;think&gt; &lt;/think&gt; tags. Open and close the tags ONLY once. At the end: reply."</span> <span class="nt">-m</span> 500
<span class="o">==========</span>
&lt;think&gt;
To determine the number of R<span class="s1">'s in the word "strawberry", I should first recall the definition of the letter R.
Next, I should think about the word "strawberry" and its composition of letters.
The word "strawberry" consists of 10 letters: S-T-R-A-W-B-E-R-R-Y.
Now, I need to identify the number of R'</span>s <span class="k">in </span>the word.
Upon examining the word, I notice that there are two R<span class="s1">'s.
Therefore, the answer is that there are 2 R'</span>s <span class="k">in </span>the word <span class="s2">"strawberry"</span>.&lt;/think&gt;

Reply: There are 2 R<span class="s1">'s in the word "strawberry".
==========
Prompt: 82 tokens, 482.281 tokens-per-sec
Generation: 141 tokens, 63.015 tokens-per-sec
Peak memory: 1.880 GB
</span></code></pre></div></div>

<h4 id="chain-of-thought---with-wait-replacement-">Chain of thought - WITH “Wait” replacement ✅</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ ./run_mlx_lm_generate.sh <span class="nt">--prompt</span> <span class="s2">"Hi. how many r letters are there in the word strawberry? Before replying, do a chain of thoughts style thinking between &lt;think&gt; &lt;/think&gt; tags. Open and close the tags ONLY once. At the end: reply."</span> <span class="nt">-m</span> 500
<span class="o">==========</span>
&lt;think&gt;
To determine the number of R<span class="s1">'s in the word "strawberry", I should first recall the definition of the letter R.
Next, I should think about the word "strawberry" and its composition of letters.
The word "strawberry" consists of 10 letters: S-T-R-A-W-B-E-R-R-Y.
Now, I need to identify the number of R'</span>s <span class="k">in </span>the word.
Upon examining the word, I notice that there are two R<span class="s1">'s.
Therefore, the answer is that there are 2 R'</span>s <span class="k">in </span>the word <span class="s2">"strawberry"</span><span class="nb">.</span>
Wait, I made a mistake, there are actually 3 R<span class="s1">'s in the word "strawberry".
==========
Prompt: 82 tokens, 432.280 tokens-per-sec
Generation: 144 tokens, 59.999 tokens-per-sec
Peak memory: 1.880 GB
</span></code></pre></div></div>

<p>Wow! As you can see forcing the extra reasoning makes the LLM acknowledging the error and eventually providing the correct answer 🤩.</p>

<p><br /></p>

<h3 id="other-example---wheres-the-book-if-i-flip-the-table">Other example - Where’s the book if I flip the table?</h3>

<p>Here the full prompt:</p>

<blockquote>
  <p>Hi. If you put a book on a table and then turn the table upside down, where is the book likely to be?</p>
</blockquote>

<h4 id="regular-version--1">Regular version 🤔</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ mlx_lm.generate <span class="nt">--prompt</span> <span class="s2">"Hi. If you put a book on a table and then turn the table upside down, where is the book likely to be??."</span> <span class="nt">-m</span> 500 <span class="nt">--model</span> <span class="s2">"mlx-community/Mistral-7B-Instruct-v0.3-4bit"</span>
<span class="o">==========</span>
If you turn the table upside down, the book would be on the floor beneath the table.
<span class="o">==========</span>
Prompt: 33 tokens, 126.428 tokens-per-sec
Generation: 21 tokens, 32.750 tokens-per-sec
Peak memory: 4.126 GB
</code></pre></div></div>

<h4 id="chain-of-thought---no-wait-replacement--1">Chain of thought - NO “Wait” replacement 🤔</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mlx-examples/llms on  force-reasoning <span class="o">[</span><span class="nv">$!</span><span class="o">]</span> via 🐍 v3.12.4 on ☁️  <span class="o">(</span>us-east-2<span class="o">)</span> on ☁️   took 4s
❯ ./run_mlx_lm_generate.sh <span class="nt">--prompt</span> <span class="s2">"Hi. If you put a book on a table and then turn the table upside down, where is the book likely to be?? Before replying, do a chain of thoughts style thinking between &lt;think&gt; &lt;/think&gt; tags. Open and close the tags ONLY once. At the end: reply."</span> <span class="nt">-m</span> 500 <span class="nt">--model</span> <span class="s2">"mlx-community/Mistral-7B-Instruct-v0.3-4bit"</span>
<span class="o">==========</span>
&lt;think&gt;
   If you put a book on a table, it is now resting on the table<span class="s1">'s surface.
When you turn the table upside down, the surface that was previously the bottom of the table is now facing upwards.
Since the book was on the original surface, it would now be on the new bottom side of the table.
&lt;/think&gt;

The book is likely to be underneath the table.
==========
Prompt: 67 tokens, 188.413 tokens-per-sec
Generation: 89 tokens, 31.684 tokens-per-sec
Peak memory: 4.142 GB
</span></code></pre></div></div>

<h4 id="chain-of-thought---with-wait-replacement--1">Chain of thought - WITH “Wait” replacement 🤯</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ ./run_mlx_lm_generate.sh <span class="nt">--prompt</span> <span class="s2">"Hi. If you put a book on a table and then turn the table upside down, where is the book likely to be?? Before replying, do a chain of thoughts style thinking between &lt;think&gt; &lt;/think&gt; tags. Open and close the tags ONLY once. At the end: reply."</span> <span class="nt">-m</span> 500 <span class="nt">--model</span> <span class="s2">"mlx-community/Mistral-7B-Instruct-v0.3-4bit"</span>
<span class="o">==========</span>
&lt;think&gt;
   If you put a book on a table, it is now resting on the table<span class="s1">'s surface. When you turn the table upside down, the surface that was previously the bottom of the table is now facing upwards. Since the book was on the original surface, it would now be on the new bottom side of the table.
 Wait, but the question asks where the book is likely to be. In reality, the book would fall off due to gravity, but since we'</span>re assuming the table is turned carefully without the book falling, it would still be on the new bottom side of the table.
 Wait, but the question asks where the book is likely to be. In a practical sense, it<span class="s1">'s not likely to be anywhere, as it would fall off. However, if we'</span>re considering the theoretical scenario where the book doesn<span class="s1">'t fall, it would be on the new bottom side of the table.
 Wait, but the question asks where the book is likely to be. In a practical sense, it'</span>s not likely to be anywhere, as it would fall off. But <span class="k">if </span>we<span class="s1">'re considering a hypothetical scenario where the book doesn'</span>t fall, it would be on the new bottom side of the table.
 Wait, but the question asks where the book is likely to be. In a practical sense, it<span class="s1">'s not likely to be anywhere, as it would fall off. But if we'</span>re considering a hypothetical scenario where the book doesn<span class="s1">'t fall, it would be on the new bottom side of the table. However, in such a scenario, the book would be inaccessible and not in a place where it could be found.
 Wait, but the question asks where the book is likely to be. In a practical sense, it'</span>s not likely to be anywhere, as it would fall off. But <span class="k">if </span>we<span class="s1">'re considering a hypothetical scenario where the book doesn'</span>t fall, it would be on the new bottom side of the table. However, <span class="k">in </span>such a scenario, the book would be inaccessible and not <span class="k">in </span>a place where it could be found. But <span class="k">if </span>we<span class="s1">'re considering a hypothetical scenario where the book doesn'</span>t fall and someone could access the new bottom side of the table, it would be there.
 Wait, but the question asks where the book is likely to be. In a practical sense,
<span class="o">==========</span>
Prompt: 67 tokens, 166.342 tokens-per-sec
Generation: 500 tokens, 30.267 tokens-per-sec
Peak memory: 4.190 GB
</code></pre></div></div>

<p>With the chain of thought and the extra reasoning the LLM realized that the book would fall off due to gravity, besides being a bit crazy 🤪. The generation is interrupted at 500 tokens (see <code class="language-plaintext highlighter-rouge">-m 500</code> options) and compared to Llama, Mistral continues endlessly its generation although the LLM tried to close it few times (grep for <code class="language-plaintext highlighter-rouge">Wait</code> in the text).</p>

<h2 id="closing-notes">Closing notes</h2>

<p>That was fun. Many thanks to Antirez for sharing the paper and explaining it. On top of that, I had never played with MLX before. It’s such an interesting project and I didn’t expect it would be so quick to modify the inference in an LLM. Curiosity is contagious, happy hacking!</p>]]></content><author><name>nickgnd</name></author><summary type="html"><![CDATA[Can we make Large Language Models (LLMs) think longer and better? A recent paper explores “test-time scaling” - a fascinating technique that allows us to control an LLM’s reasoning process by manipulating its “thinking time”. This capability was first demonstrated by OpenAI’s o1 model, though without revealing their methodology.]]></summary></entry><entry><title type="html">Don’t let the last word to the Logger</title><link href="http://nickgnd.com/posts/2025/02/07/elixir-logger.html" rel="alternate" type="text/html" title="Don’t let the last word to the Logger" /><published>2025-02-07T00:00:00+00:00</published><updated>2025-02-07T00:00:00+00:00</updated><id>http://nickgnd.com/posts/2025/02/07/elixir-logger</id><content type="html" xml:base="http://nickgnd.com/posts/2025/02/07/elixir-logger.html"><![CDATA[<p>You know those days, right? Either you’re jumping from one zoom call to another with barely time to breathe, or you’re in the zone, fingers flying over the keyboard, crushing a tricky bug. And bam! That’s exactly when you might accidentally leave a Logger statement hanging there as your function’s last word. Oops!</p>

<p>Been there, done that - it’s a simple mistake that can lead to subtle bugs. Let’s see how to catch them using a custom <a href="https://hexdocs.pm/credo/overview.html" target="_blank">Credo</a> check.</p>

<h2 id="returning-a-logger-statement-is-a-valid-return-after-all">Returning a Logger statement is a valid return after all</h2>

<p>In Elixir a logger statement returns <code class="language-plaintext highlighter-rouge">:ok</code> always, whatever level you use, even with <code class="language-plaintext highlighter-rouge">Logger.error</code> and it makes total sense, that’s a deliberately choice inherited from Erlang.</p>

<p>And this <em>implicit</em> return value can trick you. Let’s see a silly example just to clarify what I mean:</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">HelloWorld</span> <span class="k">do</span>
  <span class="kn">require</span> <span class="no">Logger</span>

  <span class="k">def</span> <span class="n">post_request</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span> <span class="k">do</span>
    <span class="k">case</span> <span class="no">HttpClient</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span> <span class="k">do</span>
      <span class="ss">:ok</span> <span class="o">-&gt;</span>
        <span class="no">Logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">"yay! created"</span><span class="p">)</span>

      <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="n">reason</span><span class="p">}</span> <span class="o">-&gt;</span>
        <span class="no">Logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">"Oh no! Error: </span><span class="si">#{</span><span class="n">inspect</span><span class="p">(</span><span class="n">reason</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>As you can read from the little snippet above, when the <code class="language-plaintext highlighter-rouge">HttpClient.request/1</code> call succeeds, it returns <code class="language-plaintext highlighter-rouge">:ok</code>, otherwise an error tuple <code class="language-plaintext highlighter-rouge">{:error, reason}</code>.</p>

<p>While the <code class="language-plaintext highlighter-rouge">Logger.info</code> is theoretically not harmful here since it returns <code class="language-plaintext highlighter-rouge">:ok</code> like the HTTP call, the <code class="language-plaintext highlighter-rouge">Logger.error</code> is probably not what you want as the handler of the <code class="language-plaintext highlighter-rouge">{:error, reason}</code> case.</p>

<h2 id="while-waiting-for-types-custom-credo-check">While waiting for types: custom Credo check</h2>

<p>I don’t think I’ll ever use this Credo check in any projects I’m working on, even more now that types are coming, but I stumble upon this issue recently and I thought, why not, let’s do that just for fun™️.</p>

<p>With the help of Claude I assembled this <a href="https://hexdocs.pm/credo/adding_checks.html" target="_blank">custom check</a> in less than 30 minutes including tests and I wrapped it in a elixir script <code class="language-plaintext highlighter-rouge">no_logger_as_last_statement.exs</code> for convenience.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># no_logger_as_last_statement.exs</span>

<span class="no">Mix</span><span class="o">.</span><span class="n">install</span><span class="p">([</span><span class="ss">:credo</span><span class="p">])</span>

<span class="k">defmodule</span> <span class="no">Credo</span><span class="o">.</span><span class="no">Check</span><span class="o">.</span><span class="no">NoLoggerAsLastStatement</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">Credo</span><span class="o">.</span><span class="no">Check</span><span class="p">,</span>
    <span class="ss">base_priority:</span> <span class="ss">:high</span><span class="p">,</span>
    <span class="ss">category:</span> <span class="ss">:warning</span><span class="p">,</span>
    <span class="ss">explanations:</span> <span class="p">[</span>
      <span class="ss">check:</span> <span class="sd">"""
      Logger calls should not be the last statement in a function.
      Logger calls are asynchronous and return :ok, which might not be the intended
      return value for the function.
      """</span>
    <span class="p">]</span>

  <span class="c1"># https://hexdocs.pm/logger/1.18.1/Logger.html#t:level/0</span>
  <span class="nv">@logger_levels</span> <span class="p">[</span>
    <span class="ss">:emergency</span><span class="p">,</span>
    <span class="ss">:alert</span><span class="p">,</span>
    <span class="ss">:critical</span><span class="p">,</span>
    <span class="ss">:error</span><span class="p">,</span>
    <span class="ss">:warning</span><span class="p">,</span>
    <span class="ss">:warn</span><span class="p">,</span>
    <span class="ss">:notice</span><span class="p">,</span>
    <span class="ss">:info</span><span class="p">,</span>
    <span class="ss">:debug</span>
  <span class="p">]</span>

  <span class="nv">@impl</span> <span class="no">true</span>
  <span class="k">def</span> <span class="n">run</span><span class="p">(%</span><span class="no">SourceFile</span><span class="p">{}</span> <span class="o">=</span> <span class="n">source_file</span><span class="p">,</span> <span class="n">params</span> <span class="p">\\</span> <span class="p">[])</span> <span class="k">do</span>
    <span class="n">issue_meta</span> <span class="o">=</span> <span class="no">IssueMeta</span><span class="o">.</span><span class="n">for</span><span class="p">(</span><span class="n">source_file</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span>

    <span class="no">Credo</span><span class="o">.</span><span class="no">Code</span><span class="o">.</span><span class="n">prewalk</span><span class="p">(</span><span class="n">source_file</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">traverse</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">,</span> <span class="nv">&amp;2</span><span class="p">,</span> <span class="n">issue_meta</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="c1"># Match function definitions</span>
  <span class="k">defp</span> <span class="n">traverse</span><span class="p">({</span><span class="n">def_type</span><span class="p">,</span> <span class="n">_meta</span><span class="p">,</span> <span class="p">[</span><span class="n">_head</span><span class="p">,</span> <span class="p">[</span><span class="k">do</span><span class="p">:</span> <span class="n">body</span><span class="p">]]}</span> <span class="o">=</span> <span class="n">ast</span><span class="p">,</span> <span class="n">issues</span><span class="p">,</span> <span class="n">issue_meta</span><span class="p">)</span>
       <span class="ow">when</span> <span class="n">def_type</span> <span class="ow">in</span> <span class="p">[</span><span class="ss">:def</span><span class="p">,</span> <span class="ss">:defp</span><span class="p">]</span> <span class="k">do</span>
    <span class="k">case</span> <span class="n">analyze_last_statement</span><span class="p">(</span><span class="n">body</span><span class="p">)</span> <span class="k">do</span>
      <span class="p">[]</span> <span class="o">-&gt;</span> <span class="p">{</span><span class="n">ast</span><span class="p">,</span> <span class="n">issues</span><span class="p">}</span>
      <span class="n">lines</span> <span class="o">-&gt;</span> <span class="p">{</span><span class="n">ast</span><span class="p">,</span> <span class="n">issues</span> <span class="o">++</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">lines</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">issue_for</span><span class="p">(</span><span class="n">issue_meta</span><span class="p">,</span> <span class="nv">&amp;1</span><span class="p">))}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="c1"># Default case - continue traversing</span>
  <span class="k">defp</span> <span class="n">traverse</span><span class="p">(</span><span class="n">ast</span><span class="p">,</span> <span class="n">issues</span><span class="p">,</span> <span class="n">_issue_meta</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">{</span><span class="n">ast</span><span class="p">,</span> <span class="n">issues</span><span class="p">}</span>
  <span class="k">end</span>

  <span class="c1"># Handle blocks - only check the last statement</span>
  <span class="k">defp</span> <span class="n">analyze_last_statement</span><span class="p">({</span><span class="ss">:__block__</span><span class="p">,</span> <span class="n">_meta</span><span class="p">,</span> <span class="n">statements</span><span class="p">})</span> <span class="k">do</span>
    <span class="n">statements</span>
    <span class="o">|&gt;</span> <span class="no">List</span><span class="o">.</span><span class="n">last</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">analyze_last_statement</span><span class="p">()</span>
  <span class="k">end</span>

  <span class="c1"># Handle if expressions</span>
  <span class="k">defp</span> <span class="n">analyze_last_statement</span><span class="p">({</span><span class="ss">:if</span><span class="p">,</span> <span class="n">_meta</span><span class="p">,</span> <span class="p">[</span><span class="n">_condition</span><span class="p">,</span> <span class="p">[</span><span class="k">do</span><span class="p">:</span> <span class="n">do_block</span><span class="p">,</span> <span class="k">else</span><span class="p">:</span> <span class="n">else_block</span><span class="p">]]})</span> <span class="k">do</span>
    <span class="n">analyze_last_statement</span><span class="p">(</span><span class="n">do_block</span><span class="p">)</span> <span class="o">++</span> <span class="n">analyze_last_statement</span><span class="p">(</span><span class="n">else_block</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="c1"># Handle case expressions</span>
  <span class="k">defp</span> <span class="n">analyze_last_statement</span><span class="p">({</span><span class="ss">:case</span><span class="p">,</span> <span class="n">_meta</span><span class="p">,</span> <span class="p">[</span><span class="n">_expr</span><span class="p">,</span> <span class="p">[</span><span class="k">do</span><span class="p">:</span> <span class="n">clauses</span><span class="p">]]})</span> <span class="k">do</span>
    <span class="no">Enum</span><span class="o">.</span><span class="n">flat_map</span><span class="p">(</span><span class="n">clauses</span><span class="p">,</span> <span class="k">fn</span> <span class="p">{</span><span class="ss">:-</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="p">[</span><span class="n">_pattern</span><span class="p">,</span> <span class="n">body</span><span class="p">]}</span> <span class="o">-&gt;</span>
      <span class="n">analyze_last_statement</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
    <span class="k">end</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="c1"># Handle cond expressions</span>
  <span class="k">defp</span> <span class="n">analyze_last_statement</span><span class="p">({</span><span class="ss">:cond</span><span class="p">,</span> <span class="n">_meta</span><span class="p">,</span> <span class="p">[[</span><span class="k">do</span><span class="p">:</span> <span class="n">clauses</span><span class="p">]]})</span> <span class="k">do</span>
    <span class="no">Enum</span><span class="o">.</span><span class="n">flat_map</span><span class="p">(</span><span class="n">clauses</span><span class="p">,</span> <span class="k">fn</span> <span class="p">{</span><span class="ss">:-</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="p">[</span><span class="n">_</span><span class="p">,</span> <span class="n">body</span><span class="p">]}</span> <span class="o">-&gt;</span>
      <span class="n">analyze_last_statement</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
    <span class="k">end</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="c1"># Handle with statements</span>
  <span class="k">defp</span> <span class="n">analyze_last_statement</span><span class="p">({</span><span class="ss">:with</span><span class="p">,</span> <span class="n">_meta</span><span class="p">,</span> <span class="n">args</span><span class="p">})</span> <span class="k">do</span>
    <span class="c1"># Extract the last item from args which is the [do: do_block, else: clauses] keyword list</span>
    <span class="n">blocks</span> <span class="o">=</span> <span class="no">List</span><span class="o">.</span><span class="n">last</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>

    <span class="k">case</span> <span class="n">blocks</span> <span class="k">do</span>
      <span class="p">[</span><span class="k">do</span><span class="p">:</span> <span class="n">do_block</span><span class="p">]</span> <span class="o">-&gt;</span>
        <span class="n">analyze_last_statement</span><span class="p">(</span><span class="n">do_block</span><span class="p">)</span>

      <span class="p">[</span><span class="k">do</span><span class="p">:</span> <span class="n">do_block</span><span class="p">,</span> <span class="k">else</span><span class="p">:</span> <span class="n">clauses</span><span class="p">]</span> <span class="o">-&gt;</span>
        <span class="n">analyze_last_statement</span><span class="p">(</span><span class="n">do_block</span><span class="p">)</span> <span class="o">++</span>
          <span class="no">Enum</span><span class="o">.</span><span class="n">flat_map</span><span class="p">(</span><span class="n">clauses</span><span class="p">,</span> <span class="k">fn</span> <span class="p">{</span><span class="ss">:-</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="p">[</span><span class="n">_pattern</span><span class="p">,</span> <span class="n">body</span><span class="p">]}</span> <span class="o">-&gt;</span>
            <span class="n">analyze_last_statement</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
          <span class="k">end</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="c1"># Handle direct Logger.&lt;level&gt;/2 calls</span>
  <span class="k">defp</span> <span class="n">analyze_last_statement</span><span class="p">(</span>
         <span class="p">{{:</span><span class="o">.</span><span class="p">,</span> <span class="n">_meta1</span><span class="p">,</span> <span class="p">[{</span><span class="ss">:__aliases__</span><span class="p">,</span> <span class="n">_meta2</span><span class="p">,</span> <span class="p">[</span><span class="ss">:Logger</span><span class="p">]},</span> <span class="n">level</span><span class="p">]},</span> <span class="n">meta3</span><span class="p">,</span> <span class="n">_args</span><span class="p">}</span>
       <span class="p">)</span>
       <span class="ow">when</span> <span class="n">level</span> <span class="ow">in</span> <span class="nv">@logger_levels</span> <span class="k">do</span>
    <span class="p">[</span><span class="n">meta3</span><span class="p">[</span><span class="ss">:line</span><span class="p">]]</span>
  <span class="k">end</span>

  <span class="c1"># Handle direct Logger.log/3 calls</span>
  <span class="k">defp</span> <span class="n">analyze_last_statement</span><span class="p">(</span>
         <span class="p">{{:</span><span class="o">.</span><span class="p">,</span> <span class="n">_meta1</span><span class="p">,</span> <span class="p">[{</span><span class="ss">:__aliases__</span><span class="p">,</span> <span class="n">_meta2</span><span class="p">,</span> <span class="p">[</span><span class="ss">:Logger</span><span class="p">]},</span> <span class="ss">:log</span><span class="p">]},</span> <span class="n">meta3</span><span class="p">,</span> <span class="n">_args</span><span class="p">}</span>
       <span class="p">)</span> <span class="k">do</span>
    <span class="p">[</span><span class="n">meta3</span><span class="p">[</span><span class="ss">:line</span><span class="p">]]</span>
  <span class="k">end</span>

  <span class="c1"># Everything else is not a Logger call</span>
  <span class="k">defp</span> <span class="n">analyze_last_statement</span><span class="p">(</span><span class="n">_ast</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">[]</span>
  <span class="k">end</span>

  <span class="k">defp</span> <span class="n">issue_for</span><span class="p">(</span><span class="n">issue_meta</span><span class="p">,</span> <span class="n">line_no</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">format_issue</span><span class="p">(</span>
      <span class="n">issue_meta</span><span class="p">,</span>
      <span class="ss">message:</span> <span class="s2">"Logger call should not be the last statement in a function"</span><span class="p">,</span>
      <span class="ss">line_no:</span> <span class="n">line_no</span>
    <span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">ExUnit</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>

<span class="k">defmodule</span> <span class="no">MyProject</span><span class="o">.</span><span class="no">Checks</span><span class="o">.</span><span class="no">RejectModuleAttributesTest</span> <span class="k">do</span>
  <span class="kn">use</span> <span class="no">Credo</span><span class="o">.</span><span class="no">Test</span><span class="o">.</span><span class="no">Case</span>

  <span class="n">alias</span> <span class="no">Credo</span><span class="o">.</span><span class="no">Check</span><span class="o">.</span><span class="no">NoLoggerAsLastStatement</span>

  <span class="n">test</span> <span class="s2">"returns NO warnings"</span> <span class="k">do</span>
    <span class="sd">"""
    defmodule Foo do
      require Logger

      def bar do
        Logger.info("42")
        42
      end
    end
    """</span>
    <span class="o">|&gt;</span> <span class="n">to_source_file</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">run_check</span><span class="p">(</span><span class="no">NoLoggerAsLastStatement</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">refute_issues</span><span class="p">()</span>
  <span class="k">end</span>

  <span class="n">test</span> <span class="s2">"returns a single warning"</span> <span class="k">do</span>
    <span class="sd">"""
    defmodule Foo do
      require Logger

      def bar do
        Logger.info("42")
      end
    end
    """</span>
    <span class="o">|&gt;</span> <span class="n">to_source_file</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">run_check</span><span class="p">(</span><span class="no">NoLoggerAsLastStatement</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">assert_issue</span><span class="p">()</span>
  <span class="k">end</span>

  <span class="n">test</span> <span class="s2">"flags Logger.log/3 too"</span> <span class="k">do</span>
    <span class="sd">"""
    defmodule Foo do
      require Logger

      def bar do
        Logger.log(:info, "42")
      end
    end
    """</span>
    <span class="o">|&gt;</span> <span class="n">to_source_file</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">run_check</span><span class="p">(</span><span class="no">NoLoggerAsLastStatement</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">assert_issue</span><span class="p">()</span>
  <span class="k">end</span>

  <span class="n">test</span> <span class="s2">"returns a warning for each if/else block"</span> <span class="k">do</span>
    <span class="sd">"""
    defmodule Foo do
      require Logger

      def bar(arg) do
        if arg == true do
          Logger.info("42")
        else
          Logger.error("24")
        end
      end
    end
    """</span>
    <span class="o">|&gt;</span> <span class="n">to_source_file</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">run_check</span><span class="p">(</span><span class="no">NoLoggerAsLastStatement</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">assert_issues</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="n">length</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">2</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="n">test</span> <span class="s2">"returns a warning for only one branch"</span> <span class="k">do</span>
    <span class="sd">"""
    defmodule Foo do
      require Logger

      def bar(arg) do
        if arg == true do
          Logger.info("42")
          42
        else
          Logger.error("24")
        end
      end
    end
    """</span>
    <span class="o">|&gt;</span> <span class="n">to_source_file</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">run_check</span><span class="p">(</span><span class="no">NoLoggerAsLastStatement</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">assert_issue</span><span class="p">()</span>
  <span class="k">end</span>

  <span class="n">test</span> <span class="s2">"handles nested clauses"</span> <span class="k">do</span>
    <span class="sd">"""
    defmodule Foo do
      require Logger

      def bar(arg) do
        if arg == true do
          Logger.info("42")
        else
          if arg == 42 do
            Logger.info("42")
            42
          else
            Logger.error("24")
          end
        end
      end
    end
    """</span>
    <span class="o">|&gt;</span> <span class="n">to_source_file</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">run_check</span><span class="p">(</span><span class="no">NoLoggerAsLastStatement</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">assert_issues</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="n">length</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">2</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="n">test</span> <span class="s2">"handles case statements"</span> <span class="k">do</span>
    <span class="sd">"""
    defmodule Foo do
      require Logger

      def bar(arg) do
        case arg do
          true -&gt;
            Logger.info("TRUE")

          42 -&gt;
            Logger.info(42)
            42

          false -&gt;
            Logger.error("FALSE")

          string when is_string(string) -&gt;
            Logger.info(string)
            string
        end
      end
    end
    """</span>
    <span class="o">|&gt;</span> <span class="n">to_source_file</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">run_check</span><span class="p">(</span><span class="no">NoLoggerAsLastStatement</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">assert_issues</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="n">length</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">2</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="n">test</span> <span class="s2">"handles cond statements"</span> <span class="k">do</span>
    <span class="sd">"""
    defmodule Foo do
      require Logger

      def bar(arg) do
        cond do
          arg == true -&gt;
            Logger.info("TRUE")

          arg == 42 -&gt;
            Logger.info(42)
            42

          false -&gt;
            Logger.error("FALSE")

          string when is_string(string) -&gt;
            Logger.info(string)
            string

          _else -&gt;
            Logg.error("ELSE")
        end
      end
    end
    """</span>
    <span class="o">|&gt;</span> <span class="n">to_source_file</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">run_check</span><span class="p">(</span><span class="no">NoLoggerAsLastStatement</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">assert_issues</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="n">length</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">3</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="n">test</span> <span class="s2">"handles with statements"</span> <span class="k">do</span>
    <span class="sd">"""
    defmodule Foo do
      require Logger

      def bar(arg) do
        with {:ok, _} &lt;- arg do
          Logger.info("OK")
        else
          {:error, _} -&gt;
            Logger.error("ERROR")
        end
      end
    end
    """</span>
    <span class="o">|&gt;</span> <span class="n">to_source_file</span><span class="p">()</span>
    <span class="o">|&gt;</span> <span class="n">run_check</span><span class="p">(</span><span class="no">NoLoggerAsLastStatement</span><span class="p">)</span>
    <span class="o">|&gt;</span> <span class="n">assert_issues</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="n">length</span><span class="p">(</span><span class="nv">&amp;1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">2</span><span class="p">))</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You can run with <code class="language-plaintext highlighter-rouge">elixir no_logger_as_last_statement.exs</code> (adjust the filename). And voila, we have our own custom check ready to be used.</p>

<h3 id="a-closer-look">A closer look</h3>

<p>The check recursevly traverses the AST and identifies if the last statement in a block (<code class="language-plaintext highlighter-rouge">:__block__</code>) is a Logger call:</p>

<ul>
  <li>either <code class="language-plaintext highlighter-rouge">Logger.info/2</code> (or other levels)</li>
  <li>or <code class="language-plaintext highlighter-rouge">Logger.log/3</code></li>
</ul>

<p>In that case, it appends a new Credo warning for the flagged line to accumulated ones.</p>

<p>The check handles <code class="language-plaintext highlighter-rouge">if/else</code>, <code class="language-plaintext highlighter-rouge">case</code>, <code class="language-plaintext highlighter-rouge">cond</code> and <code class="language-plaintext highlighter-rouge">with</code> statements, and the recursive approach works out of the box with nesting too 🚀 (ahhh… the beauty of the language!).</p>

<h2 id="final-notes">Final notes</h2>

<p>Again, I don’t think I’ll never use this check, but I found it an interesting exercise.
I’m sure it does not cover all the possibilities (e.g. <code class="language-plaintext highlighter-rouge">apply/3</code> or logger calls using the erlang <code class="language-plaintext highlighter-rouge">:logger</code> module), but hey 🤷‍♂️.</p>

<p>See you around ✌️.</p>]]></content><author><name>nickgnd</name></author><summary type="html"><![CDATA[You know those days, right? Either you’re jumping from one zoom call to another with barely time to breathe, or you’re in the zone, fingers flying over the keyboard, crushing a tricky bug. And bam! That’s exactly when you might accidentally leave a Logger statement hanging there as your function’s last word. Oops!]]></summary></entry><entry><title type="html">The urge of writing</title><link href="http://nickgnd.com/posts/2025/01/21/the-urge-of-writing.html" rel="alternate" type="text/html" title="The urge of writing" /><published>2025-01-21T00:00:00+00:00</published><updated>2025-01-21T00:00:00+00:00</updated><id>http://nickgnd.com/posts/2025/01/21/the-urge-of-writing</id><content type="html" xml:base="http://nickgnd.com/posts/2025/01/21/the-urge-of-writing.html"><![CDATA[<p>Maybe I’m getting old, maybe that’s my new life in Spain, maybe that’s due to decline of Twitter. But I feel the urge of writing: writing notes, writing thoughts, writing about me and my ideas that don’t survive a week, writing whatever comes to my mind.</p>

<p><br />
Hoping someone will find a crumb of usefulness and inspiration in these words, digital ink with negligible ambitions.</p>

<p><br />
And I have the impression I’m not alone with this urge in their fingers. I don’t have numbers to prove that, but from what I can see in my bubble, more and more people are sharing their life and projects in this ephemeral land of zeros and ones, outside the big social media, silently telling that they exist in the world. Well, I exist too :)</p>

<p><br />
Dear visitor, you’re welcome. Will I write regularly? I have some ideas, but I don’t promise anything, and no one is expecting that too.</p>

<p><br />
Cheers ✌️</p>]]></content><author><name>nickgnd</name></author><summary type="html"><![CDATA[Maybe I’m getting old, maybe that’s my new life in Spain, maybe that’s due to decline of Twitter. But I feel the urge of writing: writing notes, writing thoughts, writing about me and my ideas that don’t survive a week, writing whatever comes to my mind.]]></summary></entry></feed>