<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Quan Nguyen</title>
        <link>https://qu8n.com</link>
        <description>Quan Nguyen's RSS Feed</description>
        <lastBuildDate>Fri, 16 Oct 2026 13:53:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2026, Quan Nguyen</copyright>
        <atom:link href="https://qu8n.com/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[A better Herdr configuration for managing multiple agents]]></title>
            <link>https://qu8n.com/posts/better-herdr-config-for-managing-multiple-agents</link>
            <guid isPermaLink="false">https://qu8n.com/posts/better-herdr-config-for-managing-multiple-agents</guid>
            <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[This is an opinionated outline of how I set up my Herdr config for the best user experience.]]></description>
            <content:encoded><![CDATA[<p><a href="https://herdr.dev/">Herdr</a> is one of the best tools right now for managing multiple coding agents, but it has some terrible defaults. This is an opinionated guide to how I set up and use Herdr more efficiently and comfortably.</p>
<h2>If you&#39;re just getting started</h2>
<h3>Make it easy to use</h3>
<p>Too busy to read their docs? No problem. You can either:</p>
<ol>
<li><p>Pass this <a href="https://herdr.dev/agent-guide.md">agent-guide.md</a> to your coding agent, and it will teach you how to set up, use, and troubleshoot Herdr. Or ask it to set up Herdr for you.</p>
</li>
<li><p>Install the <a href="https://herdr.dev/docs/agent-skill/"><code>/herdr</code></a> agent skill so your agents can drive Herdr themselves: split panes, run commands, and check on neighboring agents.</p>
</li>
</ol>
<h3>View all your agents by default</h3>
<p>Herdr&#39;s main features revolve around helping you command your team of agents. This is easier when you can see them all at once. How I do it:</p>
<ul>
<li>Use a single Herdr server, the default one that starts when you run <code>herdr</code> in the terminal.</li>
<li>Keep the sidebar expanded by default. (Bonus: it pushes your panes toward the center of the screen, which is much more comfortable for your neck. On a wide monitor, this saves you from constantly turning your head to the left.)</li>
</ul>
<h3>Install agent integrations</h3>
<p>I feel like this should have been a required step when installing Herdr. Having the Herdr agent integrations unlocks Herdr&#39;s real power:</p>
<ol>
<li>It automatically reopens and resumes active agent conversations across reboots, instead of dropping you into an empty shell.</li>
<li>For some agents (like Pi and OMP), it reports their states (<code>working</code>, <code>blocked</code>, or <code>idle</code>) directly, instead of Herdr guessing from the screen.</li>
</ol>
<p>Follow <a href="https://herdr.dev/docs/integrations/">their Integrations guide</a> to set yours up.</p>
<h2>Better keyboard shortcuts</h2>
<p>A Herdr keyboard shortcut typically involves two motions. For example, to create a new Herdr tab, you&#39;d press the prefix of <code>ctrl+b</code> in one stroke, then press <code>c</code>:</p>
<Replayable>
  <KeySequence combo={[['Ctrl', 'b'], ['c']]} />
</Replayable>

<p>This <code>ctrl+b</code>+<code>&lt;key&gt;</code> pattern was inspired directly by <a href="https://github.com/tmux/tmux">tmux</a>, the OG terminal multiplexer. But this is a bad default. Among tmux users (myself included), <code>ctrl+b</code> was widely considered awkward to press, and it was very common to change this prefix to <code>ctrl+a</code>.</p>
<p>I think we should go one step further: use no prefix at all. Most of us are used to keyboard shortcuts that involve a single motion, like <code>cmd+t</code> (I&#39;m on macOS) to create a new tab in Google Chrome. And we can do that here!</p>
<Replayable>
  <KeySequence combo={[['Cmd', 't']]} />
</Replayable>

<p>Isn&#39;t that so much better?</p>
<p>To bind <code>cmd+t</code> to creating a new Herdr tab:</p>
<pre><code class="language-toml"># ~/.config/herdr/config.toml

[keys]
new_tab = &quot;cmd+t&quot; # previously prefix+c
</code></pre>
<p>For any custom keys you set, make sure to disable the conflicting keys set by your terminal emulator. I use <a href="https://ghostty.org/">Ghostty</a>, so I disabled its built-in <code>cmd+t</code>:</p>
<pre><code class="language-zsh"># ~/.config/ghostty/config

keybind = cmd+t=unbind
</code></pre>
<p>Here are my full custom Herdr keys as of this writing. Take what you like, and don&#39;t forget to unbind conflicting keys. Notice how I don&#39;t completely replace the <code>prefix</code> shortcuts, as they&#39;re great for less common actions and avoiding conflicts with other apps. I also set my prefix to <code>ctrl+space</code>, which is even easier to press than <code>ctrl+a</code>.</p>
<pre><code class="language-toml"># ~/.config/herdr/config.toml

[keys]

# General
prefix = &quot;ctrl+space&quot;
settings = &quot;cmd+,&quot;
detach = &quot;prefix+d&quot;
reload_config = &quot;prefix+r&quot;
goto = &quot;cmd+g&quot;
workspace_picker = [&quot;prefix+w&quot;, &quot;alt+w&quot;]
split_vertical = [&quot;prefix+|&quot;, &quot;cmd+d&quot;]
split_horizontal = [&quot;prefix+-&quot;, &quot;cmd+shift+d&quot;]
zoom = [&quot;prefix+z&quot;, &quot;alt+z&quot;]
resize_mode = &quot;prefix+,&quot;
toggle_sidebar = &quot;cmd+shift+b&quot;
copy_mode = &quot;alt+enter&quot;

# Navigation
focus_agent = &quot;cmd+alt+1..9&quot;
switch_tab = &quot;cmd+1..9&quot;
switch_workspace = &quot;alt+1..9&quot;
last_pane = &quot;alt+tab&quot;
previous_workspace = &quot;cmd+alt+left&quot;
next_workspace = &quot;cmd+alt+right&quot;

# Workspace and tab management
rename_workspace = &quot;alt+r&quot;
new_tab = &quot;cmd+t&quot;
rename_tab = &quot;cmd+r&quot;
close_pane = &quot;cmd+w&quot;
</code></pre>
<h2>Better tab names</h2>
<p>By default, Herdr asks you to name each new tab, which gets old fast. Turn the prompt off with <code>prompt_new_tab_name = false</code>, and you get plain numbers instead. It&#39;s difficult to know what&#39;s going on in a tab, especially when you have multiple tabs/agents running across different workspaces:</p>
<p><code>‎ 1 </code>‎‎‎‎ ‎ <code>‎ 2 </code> ‎ ‎ ‎‎‎<code>‎ 3 </code>‎‎‎ ‎ ‎ +</p>
<p>You can install a Herdr plugin like <a href="/herdr-automatic-rename">herdr-automatic-rename</a>, a plugin that I created, to automatically name them either after their foreground process:</p>
<p><code>‎ [1] zsh </code>‎‎‎ ‎ <code>‎ [2] nvim  </code> ‎ ‎ ‎‎‎<code>‎ [3] ssh </code>‎‎‎ ‎ ‎ +</p>
<p>Or, even more useful, after the agent session context:</p>
<p><code>‎ [1] Debug transient issue </code>‎ ‎ ‎ ‎‎<code>‎ [2] Improve test suite </code>‎‎‎ ‎ ‎ +</p>
<p>This makes it much easier to quickly know what each agent is working on and prioritize the more important ones.</p>
<h2>Make it yours by extending it</h2>
<h3>Community plugins</h3>
<p>Herdr is highly configurable and extensible. Any aspects you don&#39;t like can likely be reconfigured or changed.</p>
<p>Check out their <a href="https://herdr.dev/plugins/">community plugins</a> if you haven&#39;t already. There are more than 1,400 of them as I&#39;m writing this:</p>
<ul>
<li>Want a code review and file viewer sidebar? <a href="https://github.com/persiyanov/herdr-reviewr">Check</a>.</li>
<li>Want a kanban board in your Herdr? <a href="https://github.com/nelsonPires5/herdr-board">Check</a>.</li>
<li>Want a more integrated nvim experience? <a href="https://github.com/ChmaraX/herdr-nvim">Check</a>.</li>
</ul>
<h3>Your own plugins</h3>
<p>It&#39;s easy to build your own plugins to solve any friction you have in Herdr.</p>
<p>The plugins you make can just be private. Most of mine are. (Although if you make one and think others will find it helpful, I encourage you to open source it!)</p>
<p>Prototyping personal software is an area where AI shines, so you can also point your agent to the docs&#39; <a href="https://herdr.dev/docs/plugins/">plugins page</a> and let it work.</p>
<p>A preview of mine, for your inspo:</p>
<h4>herdr-prompt-picker</h4>
<img alt="herdr-prompt-picker preview" src={__img0} placeholder="blur" />

<p>This popup appears whenever I type &quot;@&quot; or &quot;/&quot; in Claude Code. When I type &quot;@&quot;, this picker shows a list of files and a preview of their content. When I type &quot;/&quot;, this picker shows a list of my available skills and a preview of their <code>SKILL.md</code>.</p>
<h4>herdr-screen-picker</h4>
<img alt="herdr-screen-picker preview" src={__img1} placeholder="blur" />

<p>This popup appears via a manual keyboard shortcut. The picker shows a list of file paths or URLs that appear on the screen. Selecting a file opens it with <code>nvim</code> in a new Herdr tab. Selecting a URL opens it in my browser.</p>
<h4>herdr-resource-manager</h4>
<img alt="herdr-resource-manager preview" src={__img2} placeholder="blur" />

<p>This one was inspired by <a href="https://www.onorca.dev/">Orca</a>&#39;s Resource Manager feature. This popup shows the CPU and memory consumed by my Herdr workspaces, agents, and tabs. Useful when my laptop&#39;s fan gets loud and I want to quickly spot the culprit.</p>
<p>You can see the pattern. I love the Herdr popup&#39;s form factor. :)</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Configuring Neovim tree-sitter from scratch from first principles]]></title>
            <link>https://qu8n.com/posts/treesitter-without-nvim-treesitter</link>
            <guid isPermaLink="false">https://qu8n.com/posts/treesitter-without-nvim-treesitter</guid>
            <pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Configuring Neovim 0.12's built-in treesitter from first principles, no plugin required.]]></description>
            <content:encoded><![CDATA[<p>In April I wrote a <a href="/posts/treesitter-migration-guide-for-nvim-0-12">migration guide</a> for updating the <code>nvim-treesitter</code> plugin to Neovim 0.12. Ironically enough, a day after that was posted, <code>nvim-treesitter</code>&#39;s maintainer <a href="https://github.com/nvim-treesitter/nvim-treesitter/discussions/8627#discussioncomment-16440673">archived it</a>.</p>
<p>While this plugin still works, seeing it break my setup after the 0.12 update made me curious about what&#39;s going on under the hood. Luckily, Neovim 0.12 ships almost everything built-in, so you and I can run treesitter with no plugin at all.</p>
<Callout title="New to treesitter?" type="info">
  It parses your code into a syntax tree so Neovim understands structure instead of guessing with regex. That tree powers highlighting, folding, and navigation. My [migration post](/posts/treesitter-migration-guide-for-nvim-0-12) has a longer explainer. There is also this neat [playground](https://tree-sitter.github.io/tree-sitter/7-playground.html) that lets you watch the tree build as you type.
</Callout>

<p><code>nvim-treesitter</code> is a manager sitting on top of three layers: parsers, queries, and a bit of Lua to glue it all together. Let&#39;s dive in.</p>
<h2>Parsers</h2>
<p>A parser is a small compiled library, one per language, that turns source text into a syntax tree: <code>lua.so</code>, <code>python.so</code>, and so on. (A syntax tree, or formally a <a href="https://en.wikipedia.org/wiki/Parse_tree">parse tree</a>, is a data structure that represents how a program or snippet is organized.)</p>
<p>Neovim 0.12 only bundles support for seven languages out of the box: <code>c</code>, <code>lua</code>, <code>markdown</code>, <code>markdown_inline</code>, <code>query</code>, <code>vim</code>, and <code>vimdoc</code>. For any other language, you need to compile the parser yourself.</p>
<p>First, find the grammar repo for your desired language. This requires a quick Google search, like <code>&lt;lang&gt; treesitter grammar</code>. This &quot;grammar&quot; repo contains instructions and rules on how to turn a source text in that language into a syntax tree.</p>
<p>Next, install the <a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter CLI</a>, clone the language&#39;s grammar repo, and build it. Below is JSON as an example, but the pattern is the same for any language: clone <code>tree-sitter-&lt;lang&gt;</code> and build it to <code>&lt;lang&gt;.so</code>.</p>
<pre><code class="language-bash">git clone https://github.com/tree-sitter/tree-sitter-json
cd tree-sitter-json
tree-sitter build -o ~/.config/nvim/parser/json.so
</code></pre>
<p>Neovim searches for parsers as <code>parser/{lang}.*</code> in every directory on its <code>runtimepath</code>, so dropping <code>json.so</code> into <code>~/.config/nvim/parser/</code> is all you need to do.</p>
<h2>Queries</h2>
<p>Once we have the syntax tree, treesitter can run queries in order to do jobs like highlighting, folding, etc. A query is written as an <a href="https://en.wikipedia.org/wiki/S-expression">S-expression</a> pattern (sort of like regex but for the syntax tree instead of raw text) that matches the right parts of the tree for a given job.</p>
<p>Queries also assign matches with capture names, which are labels that look like <code>@keyword</code>, <code>@string</code>, or <code>@comment</code>. Neovim uses these labels to decide what should be highlighted, folded, or otherwise processed.</p>
<p>Queries live as <code>.scm</code> files under a <code>queries/&lt;lang&gt;/</code> directory on the <code>runtimepath</code>, one file per job:</p>
<ul>
<li><code>queries/&lt;lang&gt;/highlights.scm</code> for highlighting</li>
<li><code>queries/&lt;lang&gt;/folds.scm</code> for folding</li>
<li><code>queries/&lt;lang&gt;/injections.scm</code> for embedded languages</li>
</ul>
<p>For the seven built-in languages, Neovim already bundles their queries along with the parsers. For anything else, drop your own into <code>~/.config/nvim/queries/&lt;lang&gt;/</code>.</p>
<p>The big query corpus that <code>nvim-treesitter</code> shipped for hundreds of languages lives in <a href="https://github.com/nvim-treesitter/nvim-treesitter/tree/main/runtime/queries">its repo</a>, so you can copy the ones you need into your config. Note that your file replaces the bundled queries for that language (first on <code>runtimepath</code> wins), so prepend <code>; extends</code> to it if you mean to add to them instead.</p>
<h2>Putting it together</h2>
<p>Great work making it here. Let&#39;s run this to start highlighting:</p>
<pre><code class="language-lua">vim.treesitter.start()
</code></pre>
<p>With no arguments, treesitter attaches the highlighter to the current buffer using the language for its filetype. It also turns off the old regex syntax highlighting. Re-enable that with <code>vim.bo.syntax = &#39;ON&#39;</code> if some plugin still needs it.</p>
<p>To auto run this everywhere, put the call in a <code>FileType</code> autocommand:</p>
<pre><code class="language-lua">vim.api.nvim_create_autocmd(&#39;FileType&#39;, {
  callback = function(args)
    pcall(vim.treesitter.start, args.buf)
  end,
})
</code></pre>
<p>The <code>pcall</code> swallows the error for filetypes with no parser installed. For folding, set two options:</p>
<pre><code class="language-lua">vim.wo.foldmethod = &#39;expr&#39;
vim.wo.foldexpr = &#39;v:lua.vim.treesitter.foldexpr()&#39;
</code></pre>
<p>Unfortunately, Neovim doesn&#39;t provide indentation out of the box. To set it up, find it in the archived <code>nvim-treesitter</code> code, although note that it&#39;s still experimental. For most languages, I think Neovim&#39;s built-in indent rules are good enough.</p>
<h2>When to skip the plugin</h2>
<p>Just because you can doesn&#39;t mean you should. When going plugin-free, you give up things like <code>:TSInstall</code>, automatic parser updates, the giant query corpus, and textobjects like <code>vaf</code> to select a function. If you use those, keep the plugin for convenience. Its <code>main</code> branch still does this job well and now calls these same core APIs under the hood.</p>
<p>But by setting up treesitter yourself, you get something with no moving parts that can break: no plugin rewrite forcing a migration, the whole thing is a parser file, a few queries, and a small snippet of Lua.</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tree-sitter migration guide for Neovim 0.12]]></title>
            <link>https://qu8n.com/posts/treesitter-migration-guide-for-nvim-0-12</link>
            <guid isPermaLink="false">https://qu8n.com/posts/treesitter-migration-guide-for-nvim-0-12</guid>
            <pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A quick guide to updating your nvim-treesitter configs for Neovim 0.12.]]></description>
            <content:encoded><![CDATA[<p>import { NeovimTweet } from &#39;./neovim-tweet.tsx&#39;;</p>
<NeovimTweet />

<p>Neovim recently updated to 0.12 with major improvements to the built-in LSP client and better terminal handling among <a href="https://gpanders.com/blog/whats-new-in-neovim-0.12/">other nice things</a>.</p>
<p>This update broke my setup: opening Neovim afterwards showed a wall of <code>attempt to call method &#39;start&#39; (a nil value)</code> error from <code>nvim-treesitter</code> along with a full stack trace.</p>
<p>This guide summarizes what I did to get it all working again.</p>
<Callout title="What is treesitter?" type="info">
  Treesitter helps Neovim and other IDEs like VSCode understand the structure of the code. This is then used to power many code editor features like highlighting, indentation, and code folding.

<p>  To nerd out a bit, treesitter works by building a concrete syntax tree (CST) of the source code under the hood. This CST is <em>roughly</em> similar to the DOM tree for HTML, both being structured representations of the underlying text.</p>
<p>  The tree-sitter docs has a cool <a href="https://tree-sitter.github.io/tree-sitter/7-playground.html">playground</a> that lets you see the syntax tree for a piece of code in real time.
</Callout></p>
<h2>What to change in <code>nvim-treesitter</code></h2>
<p>Neovim bundles the treesitter runtime, but most people use the <a href="https://github.com/nvim-treesitter/nvim-treesitter"><code>nvim-treesitter</code></a> plugin to manage parser installation and configuration.</p>
<p>That plugin got a <a href="https://github.com/nvim-treesitter/nvim-treesitter/discussions/7901">full rewrite</a> on a new <code>main</code> branch. The old <code>master</code> branch is frozen and won&#39;t work with Neovim 0.12.</p>
<h3>1. Install the <code>tree-sitter</code> CLI</h3>
<p>The new <code>nvim-treesitter</code> compiles parsers locally and depends on the <a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter CLI</a>. The old version bundled everything. The new one doesn&#39;t so we have to install it manually.</p>
<h3>2. Reinstall the plugin</h3>
<Callout type="warn">
  These code examples reflect my [lazy.nvim](https://github.com/folke/lazy.nvim) plugin manager. Adjust for your plugin manager if you use something else.
</Callout>

<p>The default branch changed from <code>master</code> to <code>main</code>. Update your plugin spec to pull from the new branch:</p>
<pre><code class="language-lua">{
  &#39;nvim-treesitter/nvim-treesitter&#39;,
  branch = &#39;master&#39;, -- [!code --]
  branch = &#39;main&#39;, -- optional as main is default, but I like being explicit // [!code ++]
  ...
},
</code></pre>
<p>Find <code>nvim-treesitter</code> in the lazy.nvim UI, press <code>x</code> to remove the old version, then <code>:Lazy update nvim-treesitter</code> to pull the new <code>main</code> branch.</p>
<h3>3. Fix the module name</h3>
<p>The old config module <code>nvim-treesitter.configs</code> no longer exists. The new plugin exports <code>setup()</code> from the top-level <code>nvim-treesitter</code> module. If you have a <code>main</code> field in your lazy.nvim spec, update it:</p>
<pre><code class="language-lua">main = &#39;nvim-treesitter.configs&#39;, -- [!code --]
main = &#39;nvim-treesitter&#39;, -- [!code ++]
</code></pre>
<h3>4. Enable highlighting and indentation yourself</h3>
<p>The old plugin had a module system where you&#39;d enable highlighting and indentation by passing options to <code>setup()</code>.</p>
<p>The new plugin simplifies the API and just handles parser installation, so we need to enable these features ourselves via a <code>FileType</code> autocmd:</p>
<pre><code class="language-lua">opts = {
  highlight = { enable = true }, -- [!code --]
  indent = { enable = true }, -- [!code --]
  -- ...
},
init = function()
  vim.api.nvim_create_autocmd(&#39;FileType&#39;, { -- [!code ++]
    callback = function() -- [!code ++]
      -- Enable treesitter highlighting and disable regex syntax // [!code ++]
      pcall(vim.treesitter.start) -- [!code ++]
      -- Enable treesitter-based indentation // [!code ++]
      vim.bo.indentexpr = &quot;v:lua.require&#39;nvim-treesitter&#39;.indentexpr()&quot; -- [!code ++]
    end, -- [!code ++]
  }) -- [!code ++]
  -- ...
end,
</code></pre>
<h3>5. Replace <code>ensure_installed</code></h3>
<p><code>ensure_installed</code> is no longer a config option. You call the install API yourself instead.</p>
<p>Here&#39;s an <code>init</code> callback that diffs against already-installed parsers so it doesn&#39;t reinstall everything on every startup:</p>
<pre><code class="language-lua">init = function()
  local ensureInstalled = {
    &#39;lua&#39;, &#39;python&#39;, &#39;typescript&#39;,
    -- ... your parsers
  }
  local alreadyInstalled = require(&#39;nvim-treesitter.config&#39;).get_installed()
  local parsersToInstall = vim.iter(ensureInstalled)
    :filter(function(parser)
      return not vim.tbl_contains(alreadyInstalled, parser)
    end)
    :totable()
  require(&#39;nvim-treesitter&#39;).install(parsersToInstall)
end,
</code></pre>
<h2>Textobjects (optional)</h2>
<p>If you use <a href="https://github.com/nvim-treesitter/nvim-treesitter-textobjects"><code>nvim-treesitter-textobjects</code></a>, same story. You need its <code>main</code> branch too:</p>
<pre><code class="language-lua">{
  &#39;nvim-treesitter/nvim-treesitter-textobjects&#39;,
  branch = &#39;master&#39;, -- [!code --]
  branch = &#39;main&#39;, -- [!code ++]
  ...
}
</code></pre>
<h2>Last steps</h2>
<ol>
<li><code>:Lazy update nvim-treesitter</code> to pull the new <code>main</code> branch</li>
<li><code>:TSUninstall all</code> to remove old parsers compiled by the <code>master</code> branch</li>
<li>Restart Neovim</li>
<li><code>:TSUpdate</code> to rebuild parsers with the new compiler</li>
<li><code>:checkhealth nvim-treesitter</code> to make sure everything&#39;s wired up</li>
</ol>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessing the terminal and running Claude Code from my phone]]></title>
            <link>https://qu8n.com/posts/running-claude-code-from-my-phone</link>
            <guid isPermaLink="false">https://qu8n.com/posts/running-claude-code-from-my-phone</guid>
            <pubDate>Mon, 22 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[A guide to controlling Claude Code running on your laptop from your phone over SSH using Termius, Tailscale, and tmux.]]></description>
            <content:encoded><![CDATA[<p>import { Info } from &#39;lucide-react&#39;;
import { YacineTweet } from &#39;./yacine-tweet.tsx&#39;;</p>
<img alt="hero" src={__img0} placeholder="blur" />

<h2>Introduction</h2>
<p>When I run Claude Code on a larger task, I often end up waiting on it while wanting to step away: coffee, a meeting, or a walk. I didn’t want to keep checking my laptop just to see whether it needed input or had finished.</p>
<p>Over the past month, I’ve been using a simple setup that lets me:</p>
<ul>
<li>keep Claude Code running on my Mac,</li>
<li>attach to the same session from my phone,</li>
<li>and get notified when it needs attention.</li>
</ul>
<YacineTweet />

<p>Mine is a simple workflow with SSH, tmux, and a couple of notification hooks, but it’s been reliable enough that I now use it almost daily.</p>
<p>This post documents my personal setup. If you’re on different devices or platforms, most of the ideas still apply.</p>
<h2>Prerequisites</h2>
<p>At a high level, the idea is:</p>
<ul>
<li>Claude Code runs on my Mac.</li>
<li>tmux keeps the session alive.</li>
<li>my phone SSHs in over Tailscale.</li>
<li>Claude Code hooks send notifications when it stops or needs input.</li>
</ul>
<p>Install the following tools on your Mac and/or iPhone:</p>
<table>
<thead>
<tr>
<th>Tool to install</th>
<th>Mac</th>
<th>iPhone</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://claude.com/product/claude-code">Claude Code</a></td>
<td>✅</td>
<td></td>
<td>Anthropic’s AI coding assistant</td>
</tr>
<tr>
<td><a href="https://github.com/tmux/tmux/wiki">tmux</a></td>
<td>✅</td>
<td></td>
<td>Access the same Claude Code session on your Mac and iPhone</td>
</tr>
<tr>
<td><a href="https://tailscale.com/download">Tailscale</a></td>
<td>✅</td>
<td>✅</td>
<td>Secure the SSH connection between your Mac and iPhone</td>
</tr>
<tr>
<td><a href="https://termius.com">Termius</a></td>
<td></td>
<td>✅</td>
<td>SSH client for your iPhone to connect to your Mac</td>
</tr>
<tr>
<td><a href="https://github.com/julienXX/terminal-notifier#download">terminal-notifier</a></td>
<td>✅</td>
<td></td>
<td>Get notifications on your Mac on Claude Code events</td>
</tr>
<tr>
<td><a href="https://ntfy.sh/">ntfy</a></td>
<td></td>
<td>✅</td>
<td>Get push notifications on your iPhone on Claude Code events</td>
</tr>
</tbody></table>
<p>I chose this combination of tools based on my familiarity with them (like tmux), cost (Termius and ntfy have generous free tiers), and ease of use (Tailscale).</p>
<p>I also wanted to (1) share the same Claude Code sessions between devices and (2) get notified on both devices when Claude Code needs my attention.</p>
<h2>Enable Remote Login on your Mac</h2>
<p><em>This step allows your Mac to accept SSH connections from other devices, enabling your iPhone to connect to it remotely.</em></p>
<ol>
<li>On your Mac, choose Apple menu , then click System Settings.</li>
<li>Click General ⚙️ in the sidebar, then click Sharing. (You may need to scroll down.)</li>
<li>Click <Info className="inline h-4 w-4" /> next to Remote Login.</li>
<li>Turn on Remote Login.</li>
<li>Turn on &quot;Allow full disk access for remote users&quot;.</li>
</ol>
<h2>Set up Tailscale</h2>
<p><em>Tailscale creates a VPN between your devices, protecting the SSH connection between your Mac and iPhone.</em></p>
<ol>
<li>Sign in with the same Tailscale account on both your Mac and iPhone.</li>
<li>Turn on Tailscale on both devices.</li>
<li>On your Mac, click on the Tailscale menu bar icon.</li>
<li>Note your Mac&#39;s Tailscale IP address (it will look like <code>100.x.x.x</code>). You&#39;ll use this IP address in the next step.</li>
</ol>
<h2>Set up Termius</h2>
<p><em>Termius is an SSH client that lets your iPhone connect to your Mac&#39;s terminal, allowing you to control Claude Code remotely.</em></p>
<h3>Generate a Termius SSH key</h3>
<ol>
<li>On your iPhone, open Termius.</li>
<li>Go to the &quot;Keychain&quot;, and tap the add button (+), then &quot;Generate Key&quot;.</li>
<li>Tap &quot;Save&quot; to create the key with default settings.</li>
<li>Tap on the newly created key, then export icon, and then &quot;Copy Public Key&quot;. We&#39;ll use this in the next step.</li>
</ol>
<h3>Add the Termius key to Mac</h3>
<ol>
<li><p>Return to your Mac and create the <code>~/.ssh/authorized_keys</code> file if it doesn&#39;t exist:</p>
<pre><code class="language-bash"> mkdir -p ~/.ssh &amp;&amp; touch ~/.ssh/authorized_keys
</code></pre>
</li>
<li><p>Ensure proper permissions:</p>
<pre><code class="language-bash"> chmod 700 ~/.ssh &amp;&amp; chmod 600 ~/.ssh/authorized_keys
</code></pre>
</li>
<li><p>Append the copied public key from Termius to the end of the <code>authorized_keys</code> file:</p>
<pre><code class="language-bash">echo &quot;REPLACE_THIS_WITH_YOUR_TERMIUS_PUBLIC_KEY&quot; &gt;&gt; ~/.ssh/authorized_keys
</code></pre>
<Callout>
  Your Termius public key should look something like "

<p>  <code>ssh-ed25519 AAAA... Generated By Termius</code></p>
<p>  &quot;.</p>
</Callout></li>
</ol>
<h3>Add your Mac as a Host in Termius</h3>
<ol>
<li>Return to Termius on your iPhone.</li>
<li>Go to &quot;Hosts&quot;, and tap the add button (+), then &quot;New Host&quot;.</li>
<li>Enter the following details and leave other fields as default:<ul>
<li>&quot;Label&quot;: Any custom identifier for your Mac (e.g., &quot;My Personal Mac&quot;).</li>
<li>&quot;IP or Hostname&quot;: Enter the Mac&#39;s Tailscale IP noted earlier (e.g., <code>100.123.45.67</code>).</li>
<li>&quot;Username&quot;: Your Mac login username.</li>
<li>&quot;SSH.id, Key, Certificate, FIDO2&quot;: Select the SSH key generated earlier.</li>
</ul>
</li>
<li>Tap &quot;Save&quot;, then &quot;Connect&quot;.</li>
</ol>
<h2>Share the same Claude Code session</h2>
<p><em>tmux lets us access the same terminal session from both the Mac and iPhone. You can start a Claude Code session on your Mac, then seamlessly switch to your iPhone to continue from exactly where you left off and vice versa.</em></p>
<p>Before connecting from your iPhone, create a tmux session on your Mac:</p>
<pre><code class="language-bash">tmux new -s session_name
</code></pre>
<p>Next, navigate to your code directory and start Claude Code:</p>
<pre><code class="language-bash">claude
</code></pre>
<p>You can now work normally on your Mac. The tmux session persists in the background even if you close the terminal window.</p>
<p>Next, switch back to Termius on your iPhone and run:</p>
<pre><code class="language-bash">tmux attach -t session_name
</code></pre>
<p>You&#39;ll see the exact same session that&#39;s running on your Mac, with all history and state intact.</p>
<p>Both devices can now view and control the same Claude Code session simultaneously. Any commands you type on your iPhone will appear on your Mac in real-time, and vice versa.</p>
<Callout title="Tip" type="info">
  New to `tmux`? Here are some handy commands:

<ul>
<li>Create a new session: <code>tmux new -s session_name</code></li>
<li>Attach to a session: <code>tmux attach -t session_name</code></li>
<li>Detach from session: <code>Ctrl + b</code>, then <code>d</code></li>
<li>Create a new window (tab) within a session: <code>Ctrl + b</code>, then <code>c</code></li>
<li>List sessions: <code>tmux ls</code></li>
<li>Kill session: <code>tmux kill-session -t session_name</code></li>
<li>See more in this <a href="https://tmuxcheatsheet.com/">cheat sheet</a></Callout></li>
</ul>
<h2>Set up notifications</h2>
<p><em>Get notified on both your Mac and iPhone when Claude Code completes tasks or needs your attention.</em></p>
<h3>Subscribe to a topic in ntfy</h3>
<p><em>ntfy is a simple push notification service that works via HTTP. We&#39;ll use it to send notifications from Claude Code running on your Mac to your iPhone.</em></p>
<ol>
<li>On your iPhone, open the ntfy app.</li>
<li>Tap the &quot;+&quot; button to add a new subscription.</li>
<li>Enter a unique topic name (e.g., <code>my-topic</code>) and tap &quot;Subscribe&quot;. If you use ntfy without sign-up, the topic is essentially a password, so pick something that&#39;s not easily guessable. If you purchase ntfy Pro, you can reserve topic names instead.</li>
</ol>
<h3>Configure Claude Code hooks</h3>
<p><em>With <a href="https://code.claude.com/docs/en/hooks-guide">Claude Code hooks</a>, we can trigger custom commands when certain events occur. We&#39;ll set up hooks to send notifications when Claude Code needs input and when it finishes working.</em></p>
<ol>
<li>Add the following to your global Claude Code settings:</li>
</ol>
<pre><code class="language-json">{
  &quot;hooks&quot;: {
    &quot;Notification&quot;: [
      {
        &quot;matcher&quot;: &quot;&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;terminal-notifier -title \&quot;🔔 Claude Code\&quot; -message \&quot;I need your input\&quot; -sound default -activate &#39;com.googlecode.iterm2&#39; &amp;&amp; curl -d \&quot;🔔 Claude Code: I need your input\&quot; ntfy.sh/my-topic&quot;
          }
        ]
      }
    ],
    &quot;Stop&quot;: [
      {
        &quot;matcher&quot;: &quot;&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;terminal-notifier -title \&quot;✅ Claude Code\&quot; -message \&quot;I&#39;m done!\&quot; -sound default -activate &#39;com.googlecode.iterm2&#39; &amp;&amp; curl -d \&quot;✅ Claude Code: I&#39;m done!\&quot; ntfy.sh/my-topic&quot;
          }
        ]
      }
    ]
  },
}
</code></pre>
<ol start="2">
<li>Replace <code>my-topic</code> in the <code>curl</code> commands with the topic name you subscribed to in ntfy.</li>
<li>I use <a href="https://iterm2.com/">iTerm2</a> as my terminal app. If you use a different one, replace <code>&#39;com.googlecode.iterm2&#39;</code> with the appropriate bundle identifier obtained from running <code>osascript -e &#39;id of app &quot;Insert app name&quot;&#39;</code>.</li>
<li>The <code>command</code> does two things: (1) send a notification to your Mac using <code>terminal-notifier</code>, and (2) send a push notification to your iPhone using <code>curl</code> to post to your subscribed ntfy topic. If you only want to be notified on one device, you can remove the other part of the command.</li>
<li>When you&#39;re done, save the settings file and restart Claude Code to apply the new settings.</li>
</ol>
<h2>Reflections</h2>
<p>The biggest win isn’t coding from my phone. It’s not feeling glued to my desk while Claude works. I can walk away then engage when there’s something to do.</p>
<p>But this setup isn&#39;t perfect:</p>
<ul>
<li>Typing on the phone feels clunky for anything more than a quick response, although I suspect I could get faster with practice (see <a href="https://www.reddit.com/r/programming/comments/1hay1z3/developer_wrote_25k_lines_of_neovim_plugin_code/">this NeoVim plugin author</a> who wrote 25K+ lines of code on his phone).</li>
<li>I&#39;m usually in front of my laptop, so the phone notifications are often more distracting than helpful. I ended up disabling the ntfy notifications for now. What would be nice is more granular control over when and how I&#39;m notified.</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Implementing Google SSO in Rails 7]]></title>
            <link>https://qu8n.com/posts/implementing-google-sso-in-rails-7</link>
            <guid isPermaLink="false">https://qu8n.com/posts/implementing-google-sso-in-rails-7</guid>
            <pubDate>Mon, 11 Jul 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[This article covers the minimum needed to install Google SSO with Devise in a Ruby on Rails 7 app.]]></description>
            <content:encoded><![CDATA[<p>This is an external post published by the Dev Genius newsletter on Medium. View it <a href="https://medium.com/dev-genius/how-to-add-google-sign-in-sso-with-devise-to-a-ruby-on-rails-7-app-6d8c5ef7641b">here</a>.</p>
]]></content:encoded>
        </item>
    </channel>
</rss>