<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/xsl" href="https://perrotta.dev/rss.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>¬ just serendipity 🍀</title>
    <link>https://perrotta.dev/</link>
    <description>Recent content on ¬ just serendipity 🍀</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <managingEditor>serendipity@perrotta.dev (Thiago Perrotta)</managingEditor>
    <webMaster>serendipity@perrotta.dev (Thiago Perrotta)</webMaster>
    <copyright>© 2013 - present Thiago Perrotta ·
  a fork of [hugo ʕ•ᴥ•ʔ bear](https://github.com/janraasch/hugo-bearblog/)
</copyright>
    <atom:link href="https://perrotta.dev/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>markdown: nested code fences
      </title>
      <link>https://perrotta.dev/2026/08/markdown-nested-code-fences/</link>
      <pubDate>Mon, 17 Aug 2026 01:27:47 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/08/markdown-nested-code-fences/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: a code block that quotes a markdown file swallows the&#xA;first closing fence it meets, which is the &lt;em&gt;inner&lt;/em&gt; one.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://perrotta.dev/2026/01/adrs-and-llms/&#34;&gt;ADRs and LLMs&lt;/a&gt; post quotes a&#xA;&lt;code&gt;README.md&lt;/code&gt; that itself contains a fenced template. Written naively:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;```markdown&#xA;# README&#xA;&#xA;Use this template:&#xA;&#xA;```markdown&#xA;# ADR-NNNN: Title&#xA;```&#xA;&#xA;That was the template.&#xA;```&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://spec.commonmark.org/0.31.2/#fenced-code-blocks&#34;&gt;CommonMark spec&lt;/a&gt;&#xA;says who wins:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The content of the code block consists of all subsequent lines, until a&#xA;closing code fence of the same type as the code block began with (backticks&#xA;or tildes), and with at least as many backticks or tildes as the opening code&#xA;fence.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Three backticks close three backticks, so the template&amp;rsquo;s closing fence&#xA;terminates the &lt;em&gt;outer&lt;/em&gt; block. Everything after it leaks into the page:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-html&#34;&gt;&amp;lt;div class=&amp;#34;codeblock&amp;#34; data-lang=&amp;#34;markdown&amp;#34;&amp;gt;&#xA;  &amp;lt;!-- header --&amp;gt;&#xA;&amp;lt;p&amp;gt;That was the template.&amp;lt;/p&amp;gt;&#xA;&amp;lt;div class=&amp;#34;codeblock&amp;#34;&amp;gt;&#xA;   &amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-plaintext&amp;#34; data-lang=&amp;#34;plaintext&amp;#34;&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&#xA;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Two code blocks and a stray paragraph, where one code block was meant.&lt;/p&gt;&#xA;&lt;p&gt;The fix is a longer outer fence — four backticks, or more:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;````markdown&#xA;# README&#xA;&#xA;Use this template:&#xA;&#xA;```markdown&#xA;# ADR-NNNN: Title&#xA;```&#xA;&#xA;That was the template.&#xA;````&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Two details make this worth writing down.&lt;/p&gt;&#xA;&lt;p&gt;The damage is quiet. On the real file the outer fence was &lt;code&gt;shell&lt;/code&gt;, so Hugo&#xA;rendered a plausible block that had merely lost its last line. Comparing the&#xA;rendered code block before and after the fix:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;-lang: shell&#xA;&amp;#43;lang: markdown&#xA; …What becomes easier or more difficult to do because of this change?&#xA;&amp;#43;```&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;And &lt;code&gt;markdownlint&lt;/code&gt; was happy the whole time:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% prek run markdownlint --files content/posts/2026-01-11-adrs-and-llms.md&#xA;markdownlint-cli2........................................................Passed&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Rule of thumb: count the backticks of the innermost block, then add one for&#xA;every level above it.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: markdown: nested code fences&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/meta/&#34;&gt;#meta&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>microSD card: wiping before selling
      </title>
      <link>https://perrotta.dev/2026/08/microsd-card-wiping-before-selling/</link>
      <pubDate>Sun, 16 Aug 2026 15:20:10 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>macos</category>
      <category>privacy</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/08/microsd-card-wiping-before-selling/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: I want to sell a 32 GB microSD card, previously used to&#xA;boot a Raspberry Pi.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% diskutil list disk4&#xA;/dev/disk4 (external, physical):&#xA;   #:                       TYPE NAME                    SIZE       IDENTIFIER&#xA;   0:     FDisk_partition_scheme                        *32.0 GB    disk4&#xA;   1:             Windows_FAT_32 BOOT                    268.4 MB   disk4s1&#xA;   2:                      Linux                         31.7 GB    disk4s2&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;For that, I would like to securely wipe it. &lt;code&gt;dd&lt;/code&gt; is my preferred tool to do so.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% diskutil unmountDisk /dev/disk4&#xA;Unmount of all volumes on disk4 was successful&#xA;% sudo dd if=/dev/zero of=/dev/rdisk4 bs=4m&#xA;dd: invalid number: &amp;#39;4m&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That is GNU &lt;code&gt;dd&lt;/code&gt; from coreutils (installed via homebrew) shadowing the BSD one&#xA;in &lt;code&gt;PATH&lt;/code&gt;. BSD &lt;code&gt;dd&lt;/code&gt; takes &lt;code&gt;bs=4m&lt;/code&gt;, GNU &lt;code&gt;dd&lt;/code&gt; takes &lt;code&gt;4M&lt;/code&gt;. It&amp;rsquo;s hard to standardize&#xA;and agree upon something so simple, eh?&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;/dev/rdisk4&lt;/code&gt; rather than &lt;code&gt;/dev/disk4&lt;/code&gt; because the raw node skips the buffer&#xA;cache and thus it is several times faster:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% sudo dd if=/dev/zero of=/dev/rdisk4 bs=4M status=progress&#xA;dd: error writing &amp;#39;/dev/rdisk4&amp;#39;: Input/output error&#xA;7633&amp;#43;0 records in&#xA;7632&amp;#43;0 records out&#xA;32010928128 bytes (32 GB, 30 GiB) copied, 3550.08 s, 9.0 MB/s&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;code&gt;Input/output error&lt;/code&gt; is the happy ending: &lt;code&gt;dd&lt;/code&gt; ran off the end of the&#xA;device. 32010928128 bytes is precisely what &lt;code&gt;diskutil info&lt;/code&gt; reported for the&#xA;disk size, so every addressable byte got a zero.&lt;/p&gt;&#xA;&lt;p&gt;The process took ~59min at ~9.0 MB/s.&lt;/p&gt;&#xA;&lt;p&gt;Then we&amp;rsquo;ll partition it again so that the buyer&amp;rsquo;s camera or laptop will&#xA;properly recognize it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% diskutil eraseDisk FAT32 SDCARD MBRFormat /dev/disk4&#xA;Started erase on disk4&#xA;Creating the partition map&#xA;Formatting disk4s1 as MS-DOS (FAT32) with name SDCARD&#xA;512 bytes per physical sector&#xA;/dev/rdisk4s1: 62488736 sectors in 1952773 FAT32 clusters (16384 bytes/cluster)&#xA;Finished erase on disk4&#xA;% diskutil list disk4&#xA;/dev/disk4 (external, physical):&#xA;   #:                       TYPE NAME                    SIZE       IDENTIFIER&#xA;   0:     FDisk_partition_scheme                        *32.0 GB    disk4&#xA;   1:                 DOS_FAT_32 SDCARD                  32.0 GB    disk4s1&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Now we can finally post it for sale.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: microSD card: wiping before selling&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/privacy/&#34;&gt;#privacy&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>homebrew: cask drift after an in-app update
      </title>
      <link>https://perrotta.dev/2026/08/homebrew-cask-drift-after-an-in-app-update/</link>
      <pubDate>Sun, 16 Aug 2026 14:11:17 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/08/homebrew-cask-drift-after-an-in-app-update/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: every &lt;code&gt;brew upgrade&lt;/code&gt; died on a cask whose app had&#xA;upgraded itself years ago.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew upgrade&#xA;==&amp;gt; Upgrading 1 outdated package:&#xA;1password 7.9.2 -&amp;gt; 8.12.33&#xA;==&amp;gt; Upgrading 1password&#xA;  7.9.2 -&amp;gt; 8.12.33&#xA;Error: 1password: It seems the App source &amp;#39;/Applications/1Password 7.app&amp;#39; is not there.&#xA;==&amp;gt; Purging files for version 8.12.33 of Cask 1password&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That path stopped existing when 1Password 8 renamed its bundle from&#xA;&lt;code&gt;1Password 7.app&lt;/code&gt; to &lt;code&gt;1Password.app&lt;/code&gt;. The app had updated itself in place,&#xA;behind Homebrew&amp;rsquo;s back:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% defaults read /Applications/1Password.app/Contents/Info.plist CFBundleShortVersionString&#xA;8.12.30&#xA;% brew list --cask --versions 1password&#xA;1password 7.9.2&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Two disagreeing sources of truth. &lt;code&gt;--adopt&lt;/code&gt; looked like the obvious cure, and&#xA;wasn&amp;rsquo;t — the stale receipt still routes into the upgrade code path, which still&#xA;looks for the v7 bundle:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew install --cask --adopt 1password&#xA;==&amp;gt; Upgrading 1password&#xA;  7.9.2 -&amp;gt; 8.12.33&#xA;Error: 1password: It seems the App source &amp;#39;/Applications/1Password 7.app&amp;#39; is not there.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The receipt is the whole problem, so look at what it actually holds:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% find /opt/homebrew/Caskroom/1password -maxdepth 2&#xA;/opt/homebrew/Caskroom/1password&#xA;/opt/homebrew/Caskroom/1password/.metadata&#xA;/opt/homebrew/Caskroom/1password/.metadata/config.json&#xA;/opt/homebrew/Caskroom/1password/.metadata/7.9.2&#xA;/opt/homebrew/Caskroom/1password/7.9.2&#xA;% ls -A /opt/homebrew/Caskroom/1password/7.9.2/&#xA;% du -sh /opt/homebrew/Caskroom/1password&#xA;8.0K&#x9;/opt/homebrew/Caskroom/1password&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Eight kilobytes of bookkeeping and an empty version directory. Nothing in there&#xA;is the application. Delete it, then reinstall over the app that is really on&#xA;disk:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% rm -rf /opt/homebrew/Caskroom/1password&#xA;% brew install --cask --force 1password&#xA;==&amp;gt; Fetching downloads for: 1password&#xA;✔︎ Cask 1password (8.12.33)&#xA;Warning: It seems there is already an App at &amp;#39;/Applications/1Password.app&amp;#39;; overwriting.&#xA;==&amp;gt; Installing Cask 1password&#xA;==&amp;gt; Removing App &amp;#39;/Applications/1Password.app&amp;#39;&#xA;==&amp;gt; Moving App &amp;#39;1Password.app&amp;#39; to &amp;#39;/Applications/1Password.app&amp;#39;&#xA;🍺  1password was successfully installed!&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Both sides agree again:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% defaults read /Applications/1Password.app/Contents/Info.plist CFBundleShortVersionString&#xA;8.12.33&#xA;% brew list --cask --versions 1password&#xA;1password 8.12.33&#xA;% brew outdated --cask | grep 1password&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Quitting the app first is mandatory, and this one ignored two polite&#xA;&lt;code&gt;osascript&lt;/code&gt; requests before a plain &lt;code&gt;kill&lt;/code&gt; took:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% osascript -e &amp;#39;quit app &amp;#34;1Password&amp;#34;&amp;#39;&#xA;% pgrep -x 1Password&#xA;740&#xA;% kill 740&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The general shape: a cask whose app ships its own updater will drift, and&#xA;Homebrew only notices when the vendor also renames the bundle. &lt;code&gt;--adopt&lt;/code&gt; is for&#xA;an app Homebrew never tracked; for an app it tracked &lt;em&gt;wrongly&lt;/em&gt;, the receipt has&#xA;to go first.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: homebrew: cask drift after an in-app update&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>new tag: bloggify
      </title>
      <link>https://perrotta.dev/2026/08/new-tag-bloggify/</link>
      <pubDate>Sun, 16 Aug 2026 13:50:00 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/08/new-tag-bloggify/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/06/bloggify/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: some readers may not want AI-assisted posts in their feed&#xA;reader, even with my edits. I support making it easier for people to opt out&#xA;from such posts. They should be able to filter them out without unsubscribing&#xA;from the whole blog.&lt;/p&gt;&#xA;&lt;p&gt;Every post the &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;&#xA;skill drafts already carries a footer disclosing it. What was missing was a&#xA;machine-readable signal, not just a line at the bottom a feed reader can&amp;rsquo;t act&#xA;on. So I grepped for the marker and tagged every post that has it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% grep -rl &amp;#34;Drafted with \[\`/bloggify\`\]&amp;#34; content/posts/ | wc -l&#xA;27&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git show 79018e8cf1 -- content/posts/2026-06-09-bloggify.md&#xA;diff --git content/posts/2026-06-09-bloggify.md content/posts/2026-06-09-bloggify.md&#xA;index b0a5a8ee86..9d15d656fd 100644&#xA;--- content/posts/2026-06-09-bloggify.md&#xA;&amp;#43;&amp;#43;&amp;#43; content/posts/2026-06-09-bloggify.md&#xA;@@ -3,6 &amp;#43;3,7 @@ title: &amp;#34;bloggify&amp;#34;&#xA; date: 2026-06-09T16:50:11&amp;#43;02:00&#xA; tags:&#xA;   - ai&#xA;&amp;#43;  - bloggify&#xA;   - claude&#xA;   - dev&#xA;   - meta&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Every post&amp;rsquo;s &lt;code&gt;tags:&lt;/code&gt; list ends up in the RSS &lt;code&gt;&amp;lt;item&amp;gt;&lt;/code&gt; as a &lt;code&gt;&amp;lt;category&amp;gt;&lt;/code&gt;&#xA;element, one per tag (see &lt;code&gt;layouts/_default/rss.xml&lt;/code&gt;). So &lt;code&gt;bloggify&lt;/code&gt; is now a&#xA;regular, filterable category in the feed, same as any other tag.&lt;/p&gt;&#xA;&lt;p&gt;For those of you who use &lt;a href=&#34;https://miniflux.app/&#34;&gt;Miniflux&lt;/a&gt;, filtering it out is the same trick from&#xA;&lt;a href=&#34;https://perrotta.dev/2026/02/miniflux-block-entry-matching-url/&#34;&gt;blocking an entry matching a URL&lt;/a&gt; and the &lt;a href=&#34;https://perrotta.dev/2024/12/miniflux-global-blocklist/&#34;&gt;global&#xA;blocklist&lt;/a&gt;, except on the&#xA;&lt;code&gt;EntryTag&lt;/code&gt; field instead of &lt;code&gt;EntryURL&lt;/code&gt; or &lt;code&gt;EntryTitle&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;EntryTag=(?i)^bloggify$&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Added as a global block rule in miniflux, and every future &lt;code&gt;/bloggify&lt;/code&gt;-drafted&#xA;post never reaches the inbox. &lt;code&gt;/tags/bloggify/&lt;/code&gt; on the blog itself works too,&#xA;for anyone who&amp;rsquo;d rather subscribe to the opposite: only the AI-assisted posts.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: new tag: bloggify&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/meta/&#34;&gt;#meta&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: prompt before dangerous commands
      </title>
      <link>https://perrotta.dev/2026/08/pi-prompt-before-dangerous-commands/</link>
      <pubDate>Sun, 16 Aug 2026 13:28:44 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/08/pi-prompt-before-dangerous-commands/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-block-dangerous-commands-syntax-aware/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: my Pi extension blocked dangerous shell commands outright,&#xA;even when I had already preserved the state they could destroy.&lt;/p&gt;&#xA;&lt;p&gt;The guard parses every &lt;code&gt;bash&lt;/code&gt; tool call with tree-sitter, catching commands such&#xA;as &lt;code&gt;rm -rf&lt;/code&gt;, &lt;code&gt;terraform destroy&lt;/code&gt;, and destructive Git operations. Today it did&#xA;exactly what I had asked:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git -C &amp;#34;$(brew --repo thiagowfx/pancake)&amp;#34; reset --hard origin/master&#xA;git reset --hard is blocked - discards changes irreversibly&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Blocking remains the right default, but an interactive session can ask me. Pi&#xA;extensions can intercept the&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/extensions.md#tool_call&#34;&gt;&lt;code&gt;tool_call&lt;/code&gt; event&lt;/a&gt;&#xA;and open a confirmation dialog through &lt;code&gt;ctx.ui&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The change in&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/commit/b5cb42f8deafae176ecddc12fccbd7c04ca264dd&#34;&gt;&lt;code&gt;dangerous-command-guard/guard.ts&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;-  pi.on(&amp;#34;tool_call&amp;#34;, async (event) =&amp;gt; {&#xA;&amp;#43;  pi.on(&amp;#34;tool_call&amp;#34;, async (event, ctx) =&amp;gt; {&#xA;     if (event.toolName !== &amp;#34;bash&amp;#34;) return;&#xA;&#xA;     const command = event.input.command;&#xA;     if (typeof command !== &amp;#34;string&amp;#34;) return;&#xA;&#xA;     const blocked = await findBlockedCommand(command);&#xA;-    if (blocked) return { block: true, reason: blocked.reason };&#xA;&amp;#43;    if (!blocked) return;&#xA;&amp;#43;    if (!ctx.hasUI) return { block: true, reason: blocked.reason };&#xA;&amp;#43;&#xA;&amp;#43;    const allowed = await ctx.ui.confirm(&#xA;&amp;#43;      &amp;#34;Allow dangerous command?&amp;#34;,&#xA;&amp;#43;      `${blocked.command}\n\n${blocked.reason}`,&#xA;&amp;#43;    );&#xA;&amp;#43;    if (!allowed) return { block: true, reason: &amp;#34;Blocked by user&amp;#34; };&#xA;   });&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Interactive Pi now pauses for approval. Print and JSON modes have no UI, so they&#xA;still fail closed. Safe commands never prompt.&lt;/p&gt;&#xA;&lt;p&gt;The tests exercise all three decisions:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;✔ extension prompts before dangerous bash tool calls (0.66225ms)&#xA;ℹ tests 48&#xA;ℹ pass 48&#xA;ℹ fail 0&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Same guardrails, with an escape hatch operated by a human rather than a model.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: prompt before dangerous commands&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: am I sandboxed?
      </title>
      <link>https://perrotta.dev/2026/08/pi-am-i-sandboxed/</link>
      <pubDate>Sat, 15 Aug 2026 00:56:29 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/08/pi-am-i-sandboxed/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: asking a coding agent whether it runs in a sandbox&#xA;returns a confident answer derived from its system prompt, not from the machine&#xA;it is actually running on.&lt;/p&gt;&#xA;&lt;p&gt;I asked &lt;a href=&#34;https://github.com/earendil-works/pi-mono&#34;&gt;Pi&lt;/a&gt; exactly that. The reply&#xA;was assured:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;No — pi itself doesn&amp;rsquo;t sandbox tool execution. When I run &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, or&#xA;&lt;code&gt;write&lt;/code&gt;, they execute directly on your machine with your user&amp;rsquo;s permissions,&#xA;in the current working directory (&lt;code&gt;/Users/thiago.perrotta/.pi&lt;/code&gt;). [&amp;hellip;] But&#xA;there&amp;rsquo;s no OS-level sandbox (no container, no seccomp, no filesystem jail).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;It&amp;rsquo;s plausible, well-formatted, and&amp;hellip;&lt;strong&gt;wrong&lt;/strong&gt;!. One typo-ridden command was&#xA;enough to break the illusion:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ls ~/worksppace&#xA;ls: cannot access &amp;#39;/home/hostuser/worksppace&amp;#39;: No such file or directory&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;~&lt;/code&gt; is not &lt;code&gt;/Users/thiago.perrotta&lt;/code&gt;. The follow-up probe:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pwd; uname -a; whoami; cat /etc/os-release&#xA;/Users/thiago.perrotta/.pi&#xA;Linux lima-rancher-desktop 6.18.37-0-virt #1-Alpine SMP PREEMPT_DYNAMIC 2026-06-29 10:52:25 aarch64 GNU/Linux&#xA;hostuser&#xA;PRETTY_NAME=&amp;#34;Debian GNU/Linux 12 (bookworm)&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A macOS-shaped &lt;code&gt;pwd&lt;/code&gt; inside an aarch64 Debian container, on an Alpine&#xA;&lt;a href=&#34;https://lima-vm.io/&#34;&gt;Lima&lt;/a&gt; VM, under Rancher Desktop. &lt;em&gt;Containerception&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;And the giveaway at the filesystem root:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ls -la / | head -5&#xA;total 68&#xA;drwxr-xr-x   1 root root 4096 Aug 15 00:54 .&#xA;drwxr-xr-x   1 root root 4096 Aug 15 00:54 ..&#xA;-rwxr-xr-x   1 root root    0 Aug 15 00:54 .dockerenv&#xA;drwxr-xr-x   3 root root 4096 Aug 15 00:54 Users&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;/.dockerenv&lt;/code&gt;, plus a &lt;code&gt;/Users&lt;/code&gt; bind mount recreating just enough of the host&#xA;path for the system prompt&amp;rsquo;s &lt;code&gt;cwd&lt;/code&gt; to resolve.&lt;/p&gt;&#xA;&lt;p&gt;The model was not lying; it had no way to know. Its self-description comes from&#xA;tokens someone else wrote, while the ground truth sits one &lt;code&gt;uname&lt;/code&gt; away. The&#xA;useful lesson is that agent introspection is a documentation lookup, not a&#xA;measurement — for anything security-relevant, make it run the command.&lt;/p&gt;&#xA;&lt;p&gt;The sandbox was &lt;a href=&#34;https://perrotta.dev/2026/02/cco-claude-condom-sandbox/&#34;&gt;cco&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: am I sandboxed?&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>git: cd from worktree to main checkout
      </title>
      <link>https://perrotta.dev/2026/08/git-cd-from-worktree-to-main-checkout/</link>
      <pubDate>Wed, 12 Aug 2026 17:22:01 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/08/git-cd-from-worktree-to-main-checkout/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2024/10/cdg-change-directory-to-the-git-root/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: make my &lt;code&gt;cdg&lt;/code&gt; shortcut jump from a linked Git worktree&#xA;back to the main checkout when already at the worktree root.&lt;/p&gt;&#xA;&lt;p&gt;The shortcut originally stopped at the current checkout&amp;rsquo;s root:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;alias cdg=&amp;#39;cd &amp;#34;$(git root)&amp;#34;&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I wanted two consecutive calls from a worktree subdirectory to behave like&#xA;the following:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;worktree/subdirectory&#xA;        ↓ cdg&#xA;worktree (at the root)&#xA;        ↓ cdg&#xA;main checkout (at the root)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;An alias cannot succinctly express the necessary conditional behavior, whereas&#xA;an external script cannot change its parent shell&amp;rsquo;s working directory. A shell&#xA;function can:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;cdg() {&#xA;&#x9;# This file is sourced by bash and zsh, both of which support local.&#xA;&#x9;# shellcheck disable=SC3043&#xA;&#x9;local line main_is_bare main_worktree root worktrees&#xA;&#xA;&#x9;root=$(git rev-parse --path-format=absolute --show-toplevel) || return&#xA;&#xA;&#x9;if [ &amp;#34;$(pwd -P)&amp;#34; != &amp;#34;$root&amp;#34; ]; then&#xA;&#x9;&#x9;cd &amp;#34;$root&amp;#34; || return&#xA;&#x9;&#x9;return&#xA;&#x9;fi&#xA;&#xA;&#x9;worktrees=$(git worktree list --porcelain) || return&#xA;&#x9;main_worktree=&#xA;&#x9;main_is_bare=false&#xA;&#x9;while IFS= read -r line; do&#xA;&#x9;&#x9;case &amp;#34;$line&amp;#34; in&#xA;&#x9;&#x9;&#x9;worktree\ *) main_worktree=${line#worktree } ;;&#xA;&#x9;&#x9;&#x9;bare) main_is_bare=true ;;&#xA;&#x9;&#x9;&#x9;&amp;#39;&amp;#39;) break ;;&#xA;&#x9;&#x9;esac&#xA;&#x9;done &amp;lt;&amp;lt;-EOF&#xA;&#x9;$worktrees&#xA;&#x9;EOF&#xA;&#xA;&#x9;if [ &amp;#34;$main_is_bare&amp;#34; = false ] &amp;amp;&amp;amp; [ -n &amp;#34;$main_worktree&amp;#34; ] &amp;amp;&amp;amp; [ &amp;#34;$main_worktree&amp;#34; != &amp;#34;$root&amp;#34; ]; then&#xA;&#x9;&#x9;cd &amp;#34;$main_worktree&amp;#34; || return&#xA;&#x9;fi&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The first entry from &lt;code&gt;git worktree list --porcelain&lt;/code&gt; is the main checkout. At&#xA;its root, &lt;code&gt;cdg&lt;/code&gt; remains a no-op. If that entry is a bare repository, it also&#xA;stays put instead of entering the Git directory.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: git: cd from worktree to main checkout&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ cmux: search all windows
      </title>
      <link>https://perrotta.dev/2026/08/cmux-search-all-windows/</link>
      <pubDate>Wed, 12 Aug 2026 11:41:09 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/08/cmux-search-all-windows/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: given a &lt;code&gt;cmux&lt;/code&gt; session with a dozen&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/cmux-search-all-windows/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; workspaces, how can you&#xA;quickly find the one you&amp;rsquo;re looking for?&lt;/p&gt;&#xA;&lt;p&gt;Keyboard shortcut (macOS): &lt;code&gt;Cmd + ⌥ + F&lt;/code&gt;.&#xA;&lt;a href=&#34;https://cmux.com/docs/keyboard-shortcuts&#34;&gt;Source&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s also a system tray icon for &lt;code&gt;cmux&lt;/code&gt; with a &lt;code&gt;Search All Windows...&lt;/code&gt; menu&#xA;item.&lt;/p&gt;&#xA;&lt;p&gt;How did I look this up? Via &lt;a href=&#34;https://kagi.com/search?q=cmux&amp;#43;search&amp;#43;shorcut%3F&amp;amp;r=no_region&amp;amp;sh=LJvrhfVS1VIXBrbO4rOJrw&#34;&gt;Kagi Search&lt;/a&gt;.&#xA;My query was &lt;code&gt;cmux search shorcut?&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The question mark (&lt;code&gt;?&lt;/code&gt;)&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/cmux-search-all-windows/#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt; at the end triggers the &lt;a href=&#34;https://help.kagi.com/kagi/ai/quick-answer.html&#34;&gt;quick&#xA;answer&lt;/a&gt; feature, including&#xA;links to sources:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Kagi&amp;rsquo;s Quick Answer quickly produces a summary of the results across the pages&#xA;returned and provides references to the pages that are used. This&#xA;functionality allows you to quickly consume the desired information from the&#xA;search while giving you the pointers to dive deeper into the information if&#xA;desired.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Whenever I know exactly the shape of the problem that I want to look up and&#xA;solve, and whenever it is byte-sized enough, I often resort to this approach; it&#xA;is very efficient.&lt;/p&gt;&#xA;&lt;p&gt;In the &lt;del&gt;old&lt;/del&gt; pre-LLM days, I would instead scour &lt;a href=&#34;https://stackoverflow.com/&#34;&gt;Stack&#xA;Overflow&lt;/a&gt; or the upstream documentations.&lt;/p&gt;&#xA;&lt;p&gt;In the early LLM days, I would ask &lt;a href=&#34;https://chatgpt.com/&#34;&gt;ChatGPT&lt;/a&gt; or&#xA;&lt;a href=&#34;https://duck.ai/&#34;&gt;Duck&lt;/a&gt;, but I&amp;rsquo;d be limited to the training cut-off of the&#xA;underlying model.&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;&lt;em&gt;&amp;ldquo;&amp;ldquo;&amp;ldquo;This should not happen&amp;rdquo;&amp;rdquo;&amp;rdquo;&lt;/em&gt;. We&amp;rsquo;re poor humans with poor attention&#xA;spans. Managing more than 4-7 workspaces at once is counter-productive due&#xA;to context switching tax. One &lt;em&gt;should not&lt;/em&gt; end up in this situation in the&#xA;first place.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/cmux-search-all-windows/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;The friction of adding the question mark is important and deliberate, it&#xA;is what enables the decision to get a digest of &lt;del&gt;slop&lt;/del&gt; LLM results. I&#xA;quite dislike the approach of &lt;a href=&#34;http://google.com/&#34;&gt;other search engines&lt;/a&gt;,&#xA;unconditionally shoving questionable AI results on everyone&amp;rsquo;s face.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/cmux-search-all-windows/#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: cmux: search all windows&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bestof/&#34;&gt;#bestof&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>hwatch
      </title>
      <link>https://perrotta.dev/2026/08/hwatch/</link>
      <pubDate>Tue, 11 Aug 2026 01:29:14 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/08/hwatch/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://github.com/blacknon/hwatch&#34;&gt;hwatch&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;hwatch: alternative watch command with history, diff view, JSONL logging, and&#xA;change hooks. since 2018.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Or, even better, from its man page &lt;a href=&#34;https://manpages.debian.org/testing/hwatch/hwatch.1.en.html&#34;&gt;&lt;code&gt;hwatch(1)&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;hwatch is like watch command, repeatedly executes a command and displays its&#xA;output.  However, the output results can be scrolled and displayed.  In&#xA;addition, the difference of the execution result is recorded with the time&#xA;stamp, and it can be checked later.  When checking, it is also possible to&#xA;display the diff with the previous difference together.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;This is the first project I&amp;rsquo;ve ever stumbled upon that (i) is written in Rust,&#xA;and (ii) doesn&amp;rsquo;t brag about being written in Rust. Am I having a dream? Not&#xA;kidding, there&amp;rsquo;s not a single mention of Rust in its&#xA;&lt;a href=&#34;https://github.com/blacknon/hwatch&#34;&gt;README.md&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;As usual, it is available via your favorite &lt;a href=&#34;https://repology.org/project/hwatch/versions&#34;&gt;package&#xA;manager&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I particularly like:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;that records the result of command execution, can display it history and diffs [&amp;hellip;]&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;A simple way to test it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% hwatch git status&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then make a silly change like:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% touch foo&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It behaves just like vanilla&#xA;&lt;a href=&#34;https://man.archlinux.org/man/watch.1&#34;&gt;&lt;code&gt;watch(1)&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;watch runs command repeatedly, displaying its output and errors (the first&#xA;screenful). This allows you to watch the program output change over time. By&#xA;default, command is run every 2 seconds and watch will run until interrupted.&#xA;A header informs of the start and running time of command as well as its exit&#xA;code.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&amp;hellip;plus a few goodies such as history tracking.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;h&lt;/code&gt; opens a pop-up with in-app keyboard shortcuts&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/hwatch/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;--aftercommand&lt;/code&gt; (hook) is also interesting:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% hwatch --aftercommand &amp;#39;/usr/bin/afplay /System/Library/Sounds/Glass.aiff&amp;#39; git st&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&amp;hellip;makes a chime sound upon each diff&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/hwatch/#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;A poor man&amp;rsquo;s stopwatch:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% hwatch --aftercommand &amp;#39;/usr/bin/afplay /System/Library/Sounds/Glass.aiff&amp;#39; --interval 1 date&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;Why not &lt;code&gt;?&lt;/code&gt;? :(&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/hwatch/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;There&amp;rsquo;s also a native &lt;code&gt;--beep&lt;/code&gt; option but it did not work in my terminal.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/hwatch/#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: hwatch&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>homebrew: replace deprecated and orphaned packages
      </title>
      <link>https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/</link>
      <pubDate>Mon, 10 Aug 2026 15:10:42 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;brew doctor&lt;/code&gt; found one deprecated cask and one installed&#xA;keg whose formula had disappeared.&lt;/p&gt;&#xA;&lt;h2 id=&#34;tflint&#34;&gt;&#xA;  tflint&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/#tflint&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Warning: Some installed casks are deprecated or disabled.&#xA;You should find replacements for the following casks:&#xA;flameshot&#xA;&#xA;Warning: Some installed kegs have no formulae!&#xA;You should find replacements for the following formulae:&#xA;  tflint&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Homebrew/core &lt;a href=&#34;https://github.com/Homebrew/homebrew-core/commit/6cefefcbba24e4a1093dfd2e189f2cb01170e45f&#34;&gt;removed TFLint&lt;/a&gt;&#xA;after its executable became subject to both MPL 2.0 and BUSL 1.1. Upstream now&#xA;maintains &lt;a href=&#34;https://github.com/terraform-linters/homebrew-tap&#34;&gt;&lt;code&gt;terraform-linters/tap&lt;/code&gt;&lt;/a&gt;&#xA;as a cask, so migrate the old formula instead of merely reinstalling it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew uninstall tflint&#xA;Uninstalling /opt/homebrew/Cellar/tflint/0.61.0... (8 files, 51.8MB)&#xA;&#xA;% brew install terraform-linters/tap/tflint&#xA;==&amp;gt; Tapping terraform-linters/tap&#xA;Tapped 1 cask (15 files, 14.7KB).&#xA;==&amp;gt; Installing Cask tflint&#xA;==&amp;gt; Linking Binary &amp;#39;tflint&amp;#39; to &amp;#39;/opt/homebrew/bin/tflint&amp;#39;&#xA;🍺  tflint was successfully installed!&#xA;&#xA;% tflint --version&#xA;TFLint version 0.64.0&#xA;&amp;#43; ruleset.terraform (0.15.0-bundled)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;flameshot--shottr&#34;&gt;&#xA;  Flameshot → Shottr&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/#flameshot--shottr&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://formulae.brew.sh/cask/flameshot&#34;&gt;Flameshot cask&lt;/a&gt; was deprecated&#xA;because it does not pass macOS Gatekeeper checks. I replaced it with&#xA;&lt;a href=&#34;https://shottr.cc/&#34;&gt;Shottr&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew uninstall --cask flameshot&#xA;==&amp;gt; Uninstalling Cask flameshot&#xA;==&amp;gt; Removing App &amp;#39;/Applications/flameshot.app&amp;#39;&#xA;==&amp;gt; Purging files for version 14.0.0,14.0 of Cask flameshot&#xA;&#xA;% brew install --cask shottr&#xA;==&amp;gt; Installing Cask shottr&#xA;==&amp;gt; Moving App &amp;#39;Shottr.app&amp;#39; to &amp;#39;/Applications/Shottr.app&amp;#39;&#xA;🍺  shottr was successfully installed!&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The corresponding &lt;code&gt;Brewfile&lt;/code&gt; &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/commit/79cd3dd4eb448d4a3bc5f66a124fd60d3f131d58&#34;&gt;change&lt;/a&gt; keeps the migration reproducible:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;&amp;#43;tap &amp;#34;terraform-linters/tap&amp;#34;&#xA;&#xA;-brew &amp;#34;tflint&amp;#34;&#xA;&#xA;-cask &amp;#34;flameshot&amp;#34;&#xA;&amp;#43;cask &amp;#34;shottr&amp;#34;&#xA;&amp;#43;cask &amp;#34;tflint&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Both offending entries are now gone:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew doctor 2&amp;gt;&amp;amp;1 | grep -E &amp;#39;flameshot|tflint|deprecated|no formulae&amp;#39;&#xA;&#xA;% brew list --cask | grep -E &amp;#39;^(flameshot|shottr|tflint)$&amp;#39;&#xA;shottr&#xA;tflint&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: homebrew: replace deprecated and orphaned packages&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>homebrew: turn go install into a formula
      </title>
      <link>https://perrotta.dev/2026/08/homebrew-turn-go-install-into-a-formula/</link>
      <pubDate>Mon, 10 Aug 2026 14:58:29 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>coding</category>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/08/homebrew-turn-go-install-into-a-formula/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2025/10/distributing-my-own-scripts-via-homebrew/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;a href=&#34;https://meat.dev/&#34;&gt;meat&lt;/a&gt; only documents a &lt;code&gt;go install&lt;/code&gt;&#xA;command, but I want Homebrew to manage it&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/homebrew-turn-go-install-into-a-formula/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The existing installation lived under &lt;code&gt;$GOPATH&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% go install meat.dev/cmd/meat@latest&#xA;% which meat&#xA;/Users/thiago.perrotta/go/bin/meat&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;There are no tags or releases. &lt;code&gt;@latest&lt;/code&gt; currently resolves to a Go&#xA;pseudo-version:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% go list -m -json meat.dev@latest&#xA;{&#xA;        &amp;#34;Path&amp;#34;: &amp;#34;meat.dev&amp;#34;,&#xA;        &amp;#34;Version&amp;#34;: &amp;#34;v0.0.0-20260803201634-f39f41dfe7b5&amp;#34;,&#xA;        &amp;#34;Query&amp;#34;: &amp;#34;latest&amp;#34;,&#xA;        &amp;#34;Time&amp;#34;: &amp;#34;2026-08-03T20:16:34Z&amp;#34;,&#xA;        &amp;#34;Dir&amp;#34;: &amp;#34;/Users/thiago.perrotta/go/pkg/mod/meat.dev@v0.0.0-20260803201634-f39f41dfe7b5&amp;#34;,&#xA;        &amp;#34;GoMod&amp;#34;: &amp;#34;/Users/thiago.perrotta/go/pkg/mod/cache/download/meat.dev/@v/v0.0.0-20260803201634-f39f41dfe7b5.mod&amp;#34;,&#xA;        &amp;#34;GoVersion&amp;#34;: &amp;#34;1.24.13&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The pseudo-version gives us commit &lt;code&gt;f39f41dfe7b5&lt;/code&gt;; the module&amp;rsquo;s vanity import&#xA;page points at &lt;code&gt;github.com/boldsoftware/meat&lt;/code&gt;. Hash its source archive:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% curl -LfsS https://github.com/boldsoftware/meat/archive/f39f41dfe7b5.tar.gz -o /tmp/meat.tar.gz&#xA;% shasum -a 256 /tmp/meat.tar.gz&#xA;faf4831aa3fa866168191b21414698f407f1d473c1572e4cc3942e2c595db6bd  /tmp/meat.tar.gz&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;We can do better. Let&amp;rsquo;s create a homebrew package for it. &lt;a href=&#34;https://github.com/thiagowfx/homebrew-taps/blob/master/Formula/meat.rb&#34;&gt;&lt;code&gt;Formula/meat.rb&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ruby&#34;&gt;class Meat &amp;lt; Formula&#xA;  desc &amp;#34;Abridge code diffs into reading diffs&amp;#34;&#xA;  homepage &amp;#34;https://meat.dev&amp;#34;&#xA;  url &amp;#34;https://github.com/boldsoftware/meat/archive/f39f41dfe7b5b37a12b35fdfbaecc7e779855bd3.tar.gz&amp;#34;&#xA;  version &amp;#34;0.0.0-20260803201634-f39f41dfe7b5&amp;#34;&#xA;  sha256 &amp;#34;faf4831aa3fa866168191b21414698f407f1d473c1572e4cc3942e2c595db6bd&amp;#34;&#xA;  license &amp;#34;Apache-2.0&amp;#34;&#xA;  head &amp;#34;https://github.com/boldsoftware/meat.git&amp;#34;, branch: &amp;#34;main&amp;#34;&#xA;&#xA;  depends_on &amp;#34;go&amp;#34; =&amp;gt; :build&#xA;&#xA;  def install&#xA;    system &amp;#34;go&amp;#34;, &amp;#34;build&amp;#34;, *std_go_args, &amp;#34;./cmd/meat&amp;#34;&#xA;  end&#xA;&#xA;  test do&#xA;    assert_match &amp;#34;abridge a diff&amp;#34;, shell_output(&amp;#34;#{bin}/meat --help 2&amp;gt;&amp;amp;1&amp;#34;)&#xA;  end&#xA;end&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://docs.brew.sh/Formula-Cookbook#standard-arguments&#34;&gt;&lt;code&gt;std_go_args&lt;/code&gt;&lt;/a&gt; builds the requested package into Homebrew&amp;rsquo;s &lt;code&gt;bin&lt;/code&gt; directory. No&#xA;manual &lt;code&gt;GOBIN&lt;/code&gt; or linker flags needed.&lt;/p&gt;&#xA;&lt;p&gt;Replace the old binary and let Homebrew take over:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% rm ~/go/bin/meat&#xA;% brew install thiagowfx/taps/meat&#xA;==&amp;gt; Installing meat from thiagowfx/taps&#xA;==&amp;gt; go build ./cmd/meat&#xA;🍺  /opt/homebrew/Cellar/meat/0.0.0-20260803201634-f39f41dfe7b5: 6 files, 6.6MB, built in 2 seconds&#xA;&#xA;% which meat&#xA;/opt/homebrew/bin/meat&#xA;% brew list --versions meat&#xA;meat 0.0.0-20260803201634-f39f41dfe7b5&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Same binary, now owned by the package manager.&lt;/p&gt;&#xA;&lt;p&gt;We can trivially uninstall it on-demand:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;brew uninstall meat&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;In 2026 there&amp;rsquo;s no excuse to ship software with &lt;code&gt;go install&lt;/code&gt;. It feels&#xA;sloppy, unless it&amp;rsquo;s a prototype or an experimental project. Creating a&#xA;package is trivial, even without LLMs.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/homebrew-turn-go-install-into-a-formula/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: homebrew: turn go install into a formula&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>terraform: fileset does not respect gitignore
      </title>
      <link>https://perrotta.dev/2026/08/terraform-fileset-does-not-respect-gitignore/</link>
      <pubDate>Fri, 07 Aug 2026 11:27:13 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>aws</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2026/08/terraform-fileset-does-not-respect-gitignore/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: a terraform project planned clean locally (&lt;code&gt;terraform plan&lt;/code&gt;), but CI insisted on a diff. Every apply fixed it for one side and broke&#xA;it for the other, back-and-forth.&lt;/p&gt;&#xA;&lt;p&gt;The culprit was a &lt;code&gt;null_resource&lt;/code&gt; that rebuilds an AWS Lambda for a NodeJS app&#xA;bundle whenever its sources change, keyed on a hash of the whole directory:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;resource &amp;#34;null_resource&amp;#34; &amp;#34;lambda_build&amp;#34; {&#xA;  triggers = {&#xA;    dir_sha = sha256(join(&amp;#34;&amp;#34;, [for f in sort(fileset(&amp;#34;${path.module}/lambda&amp;#34;, &amp;#34;**&amp;#34;)) : &amp;#34;${f}:${filesha256(&amp;#34;${path.module}/lambda/${f}&amp;#34;)}&amp;#34;]))&#xA;  }&#xA;&#xA;  provisioner &amp;#34;local-exec&amp;#34; {&#xA;    command = &amp;#34;cd ${path.module}/lambda &amp;amp;&amp;amp; npm ci &amp;amp;&amp;amp; npm run build&amp;#34;&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/functions/fileset&#34;&gt;&lt;code&gt;fileset()&lt;/code&gt;&lt;/a&gt;&#xA;walks the filesystem. It has no idea &lt;code&gt;.gitignore&lt;/code&gt; exists. So &lt;code&gt;node_modules/&lt;/code&gt;&#xA;and &lt;code&gt;dist/&lt;/code&gt;, present locally after a build, but absent in a fresh CI checkout,&#xA;creates the discrepancy.&lt;/p&gt;&#xA;&lt;p&gt;A minimal reproduction test case, with &lt;code&gt;node_modules/&lt;/code&gt; and &lt;code&gt;dist/&lt;/code&gt; gitignored:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git ls-files lambda | wc -l&#xA;2&#xA;% echo &amp;#39;sha256(join(&amp;#34;&amp;#34;, [for f in sort(fileset(&amp;#34;./lambda&amp;#34;, &amp;#34;**&amp;#34;)) : &amp;#34;${f}:${filesha256(&amp;#34;./lambda/${f}&amp;#34;)}&amp;#34;]))&amp;#39; | terraform console&#xA;&amp;#34;a0af67e204ae2b73a7452d0c2755be1f665b377b5d49e7cc520bd31607adb832&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then with &lt;code&gt;npm ci &amp;amp;&amp;amp; npm run build&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% find lambda -type f | wc -l&#xA;4&#xA;% git status --porcelain | wc -l&#xA;0&#xA;% echo &amp;#39;sha256(join(&amp;#34;&amp;#34;, [for f in sort(fileset(&amp;#34;./lambda&amp;#34;, &amp;#34;**&amp;#34;)) : &amp;#34;${f}:${filesha256(&amp;#34;./lambda/${f}&amp;#34;)}&amp;#34;]))&amp;#39; | terraform console&#xA;&amp;#34;728a40d04abc2b8292d57465d1f037f833a67d4cc0607a6c64cef487292d9be6&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Git says nothing changed; Terraform says everything did (ugh!).&lt;/p&gt;&#xA;&lt;p&gt;That&amp;rsquo;s the issue. CI plans one value, my laptop plans the other, and each&#xA;apply sets up the next one to &amp;ldquo;detect drift&amp;rdquo; again.&lt;/p&gt;&#xA;&lt;p&gt;Nothing in AWS had changed. The deployed artifact was byte-identical the whole&#xA;time; only the build trigger churned.&lt;/p&gt;&#xA;&lt;p&gt;The fix is to hash the build &lt;em&gt;inputs&lt;/em&gt;, not the build &lt;em&gt;directory&lt;/em&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;dir_sha = sha256(join(&amp;#34;&amp;#34;, [for f in sort(setunion(&#xA;  fileset(&amp;#34;${path.module}/lambda&amp;#34;, &amp;#34;src/**&amp;#34;),&#xA;  [&amp;#34;package.json&amp;#34;, &amp;#34;package-lock.json&amp;#34;],&#xA;)) : &amp;#34;${f}:${filesha256(&amp;#34;${path.module}/lambda/${f}&amp;#34;)}&amp;#34;]))&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Same value on both sides, and still sensitive to anything that actually matters:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% # dirty tree, after npm ci&#xA;&amp;#34;a0af67e204ae2b73a7452d0c2755be1f665b377b5d49e7cc520bd31607adb832&amp;#34;&#xA;% # pristine `git archive` checkout&#xA;&amp;#34;a0af67e204ae2b73a7452d0c2755be1f665b377b5d49e7cc520bd31607adb832&amp;#34;&#xA;% # after editing lambda/src/handler.ts (hypotetically)&#xA;&amp;#34;53ef69e287a65f9785cf05985942a5c62a8175348c91c7d9c83039bfc31b8714&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;terraform console&lt;/code&gt; is the right tool here — it evaluates an expression against&#xA;the real filesystem without an apply, so both variants can be diffed against a&#xA;pristine &lt;code&gt;git archive&lt;/code&gt; tree and a working one before committing anything.&lt;/p&gt;&#xA;&lt;p&gt;The general shape: any &lt;code&gt;fileset()&lt;/code&gt; over a directory that also holds build&#xA;artifacts, virtualenvs, or vendored dependencies is environment-dependent, and&#xA;will read as permanent drift for exactly as long as nobody looks closely.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: terraform: fileset does not respect gitignore&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/aws/&#34;&gt;#aws&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>↗ Reply to: The Wisdom Paradox - Sketchplanations
      </title>
      <link>https://perrotta.dev/2026/08/reply-to-the-wisdom-paradox-sketchplanations/</link>
      <pubDate>Fri, 07 Aug 2026 10:42:27 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>commentary</category>
      <category>pkm</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/08/reply-to-the-wisdom-paradox-sketchplanations/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;Wisdom is a strange thing.&lt;/p&gt;&#xA;&lt;p&gt;The Wisdom Paradox is this: when we realise we are less wise than we thought,&#xA;we become wiser.&lt;/p&gt;&#xA;&lt;p&gt;[&amp;hellip;]&lt;/p&gt;&#xA;&lt;p&gt;Good judgment comes from experience, and experience comes from bad judgment.&lt;/p&gt;&#xA;&lt;p&gt;The naivety of youth is a lovely thing. As a teenager, you can be sure that&#xA;the grown-ups are doing it all wrong, and the path to better is obvious. Then,&#xA;as an adult, it somehow seems more difficult than before.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Some 20 years ago I became familiar with this principle, and it&amp;rsquo;s beautiful.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s no greater feeling of satisfaction than learning and overcoming your own&#xA;ignorance.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Reply to: The Wisdom Paradox - Sketchplanations&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/categories/commentary/&#34;&gt;%commentary&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>atuin: hooks for LLM agent shell history
      </title>
      <link>https://perrotta.dev/2026/08/atuin-hooks-for-llm-agent-shell-history/</link>
      <pubDate>Thu, 06 Aug 2026 13:21:31 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>claude</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/08/atuin-hooks-for-llm-agent-shell-history/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Today I learned&lt;/strong&gt;: &lt;a href=&#34;https://atuin.sh/&#34;&gt;Atuin&lt;/a&gt; ships&#xA;&lt;a href=&#34;https://docs.atuin.sh/18.18/guide/agent-hooks/&#34;&gt;&lt;em&gt;hooks&lt;/em&gt;&lt;/a&gt; that capture commands&#xA;executed by &lt;del&gt;AI&lt;/del&gt; LLM coding agents into the same shell history as mine.&lt;/p&gt;&#xA;&lt;p&gt;As of today there are hooks for Claude Code, OpenAI Codex and Pi.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% atuin hook --help&#xA;Manage AI-agent shell hooks&#xA;&#xA;Usage: atuin hook&#xA;       atuin hook &amp;lt;COMMAND&amp;gt;&#xA;&#xA;Commands:&#xA;  install  Install hooks for an AI agent to capture commands in atuin history&#xA;  help     Print this message or the help of the given subcommand(s)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;install --help&lt;/code&gt; doesn&amp;rsquo;t list which agents are supported, but a bogus one&#xA;does (or simply look at their web documentation):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% atuin hook install invalid-agent&#xA;Error: unknown agent: invalid-agent. Supported agents: claude-code, codex, pi&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Installing the hook for &lt;a href=&#34;https://pi.dev/&#34;&gt;pi&lt;/a&gt; is straightforward:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% atuin hook install pi&#xA;pi extension: installed atuin extension&#xA;&#xA;Atuin extension installed for pi. Extension: ~/.pi/agent/extensions/atuin.ts&#xA;Reload pi with `/reload` or restart pi.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It drops a small extension that wraps every &lt;code&gt;bash&lt;/code&gt; tool call with&#xA;&lt;code&gt;atuin history start --author pi&lt;/code&gt; / &lt;code&gt;history end&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const historyId = await startHistory(pi, ctx.cwd, command);&#xA;if (historyId) pending.set(event.toolCallId, historyId);&#xA;&#xA;// ...&#xA;&#xA;await endHistory(pi, ctx.cwd, historyId, exitCodeFromResult(event.result, event.isError));&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Commands run by the agent land in the same database as everything typed by&#xA;hand (by me!); there&amp;rsquo;s no separate table, just an &lt;code&gt;--author&lt;/code&gt; tag:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% atuin search --help | grep -A2 author&#xA;      --author &amp;lt;AUTHOR&amp;gt;&#xA;          Filter by author. Supports $all-user (non-agents), $all-agent, or literal names.&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% atuin search --author pi&#xA;% atuin search --author &amp;#39;$all-user&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;One shared history to rule them all.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: atuin: hooks for LLM agent shell history&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>dual-review: address review findings automatically
      </title>
      <link>https://perrotta.dev/2026/08/dual-review-address-review-findings-automatically/</link>
      <pubDate>Wed, 05 Aug 2026 14:01:30 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>claude</category>
      <category>coding</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/08/dual-review-address-review-findings-automatically/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/06/claude-code-ship-your-skills-as-a-plugin-marketplace/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;My&#xA;&lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/dual-review/SKILL.md&#34;&gt;&lt;code&gt;dual-review&lt;/code&gt;&lt;/a&gt;&#xA;skill runs two independent LLM reviewers over a commit / diff / PR, validates&#xA;every finding against the code, and hands back one action plan — Blockers,&#xA;Important, Suggestions.&lt;/p&gt;&#xA;&lt;p&gt;Recently I added a flag argument that closes the loop:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt; argument-hint: &amp;#34;[branch|staged|all] [--pr NUMBER] [--post]&amp;#34;&#xA;&amp;#43;argument-hint: &amp;#34;[branch|staged|all] [--pr NUMBER] [--post] [--address]&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt; - `--post`: post final action plan to target pull request after showing it; never post otherwise&#xA;&amp;#43;- `--address`: after showing (and, if requested, posting) the action plan, fix Blockers and Important findings directly in the reviewed tree; never fix Suggestions without explicit confirmation&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;&amp;#43;## 7. Address Findings (only with `--address`)&#xA;&amp;#43;&#xA;&amp;#43;Skip this section entirely unless `--address` was requested.&#xA;&amp;#43;&#xA;&amp;#43;1. Work in the same tree that was reviewed (existing worktree or user&amp;#39;s checkout for `staged`/`all`); never create a second copy of the changes.&#xA;&amp;#43;2. Address every Blocker and every Important finding. For Suggestions, ask the user which (if any) to apply; default to none.&#xA;&amp;#43;3. For each finding: re-read the exact `file:line` locations, apply the `Fix:` recommendation, and fix every sibling location listed under that finding, not just the first instance.&#xA;&amp;#43;4. If a finding is ambiguous, requires a design decision, or the recommended fix turns out to be wrong once you&amp;#39;re in the code, stop and ask the user instead of guessing.&#xA;&amp;#43;5. After edits, rerun any locally available checks that cover the change (tests, linters, type-checks) before treating a finding as resolved.&#xA;&amp;#43;6. Stage only the files touched while addressing findings. Commit with a message describing which findings were fixed (e.g. `address dual-review findings`), listing them briefly in the body.&#xA;&amp;#43;7. If scope was `branch`/`--pr`, push to the branch. If the action plan was posted (`--post`), leave a short follow-up comment noting the findings were addressed and pushed; do not repost the full plan.&#xA;&amp;#43;8. Report: which findings were fixed, which were skipped and why, and which need user input.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The important line is the second one: Suggestions never get auto-applied,&#xA;only Blockers and Important.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;/dual-review --post --address&lt;/code&gt; now reviews, posts the plan, fixes&#xA;what&amp;rsquo;s fixable, and pushes.&lt;/p&gt;&#xA;&lt;p&gt;Why two review(er)s instead of one? Because LLMs are non-deterministic.&lt;/p&gt;&#xA;&lt;p&gt;Why not more than two review(er)s then? Tokens ($$$) and time. It&amp;rsquo;s a balance.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: dual-review: address review findings automatically&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/meta/&#34;&gt;#meta&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: remove redundant github MCP
      </title>
      <link>https://perrotta.dev/2026/08/pi-remove-redundant-github-mcp/</link>
      <pubDate>Mon, 03 Aug 2026 18:47:51 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/08/pi-remove-redundant-github-mcp/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2025/10/mcp-model-context-protocol-considered-harmful/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;ve finally removed the GitHub MCP server from my &lt;a href=&#34;https://pi.dev/&#34;&gt;Pi&lt;/a&gt;&#xA;configuration, after piloting it for a while with &lt;a href=&#34;https://github.com/nicobailon/pi-mcp-adapter&#34;&gt;&lt;code&gt;pi-mcp-adapter&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;GitHub already has a mature CLI (&lt;code&gt;gh&lt;/code&gt;) installed and authenticated:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% gh --version&#xA;gh version 2.97.0 (2026-07-31)&#xA;https://github.com/cli/cli/releases/tag/v2.97.0&#xA;&#xA;% gh auth status&#xA;github.com&#xA;  ✓ Logged in to github.com account thiagowfx (keyring)&#xA;  - Active account: true&#xA;  - Git operations protocol: ssh&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;gh&lt;/code&gt; handles pull requests, issues, workflows, and releases directly. Anything&#xA;else remains available through &lt;code&gt;gh api&lt;/code&gt;, including GraphQL. The MCP server added&#xA;another interface without adding capability.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt; {&#xA;   &amp;#34;mcpServers&amp;#34;: {&#xA;-    &amp;#34;github&amp;#34;: {&#xA;-      &amp;#34;auth&amp;#34;: &amp;#34;bearer&amp;#34;,&#xA;-      &amp;#34;bearerToken&amp;#34;: &amp;#34;!gh auth token&amp;#34;,&#xA;-      &amp;#34;url&amp;#34;: &amp;#34;https://api.githubcopilot.com/mcp&amp;#34;&#xA;-    },&#xA;     &amp;#34;slack-mcp&amp;#34;: {&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;MCP remains useful for APIs without a better (or any) CLI, but it should be&#xA;avoided wherever possible.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: remove redundant github MCP&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>DNS: negative cache behind a local resolver
      </title>
      <link>https://perrotta.dev/2026/07/dns-negative-cache-behind-a-local-resolver/</link>
      <pubDate>Fri, 31 Jul 2026 14:02:20 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/07/dns-negative-cache-behind-a-local-resolver/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: a newly created Route 53 record resolved through public DNS,&#xA;but &lt;del&gt;Chrome&lt;/del&gt; Brave and macOS still returned &lt;code&gt;DNS_PROBE_POSSIBLE&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;It smells like a DNS caching issue.&lt;/p&gt;&#xA;&lt;p&gt;Flushing &lt;code&gt;mDNSResponder&lt;/code&gt; did nothing though.&lt;/p&gt;&#xA;&lt;p&gt;The system resolver was &lt;strong&gt;Tailscale&lt;/strong&gt;&amp;rsquo;s local DNS&#xA;proxy, so I asked that resolver rather than trusting &lt;code&gt;dig&lt;/code&gt;&amp;rsquo;s default path:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% dig &amp;#43;time=3 &amp;#43;tries=1 @100.100.100.100 kargo.tools.example-corp.net A &amp;#43;noall &amp;#43;answer &amp;#43;authority &amp;#43;comments&#xA;; &amp;lt;&amp;lt;&amp;gt;&amp;gt; DiG 9.10.6 &amp;lt;&amp;lt;&amp;gt;&amp;gt; &amp;#43;time=3 &amp;#43;tries=1 @100.100.100.100 kargo.tools.example-corp.net A &amp;#43;noall &amp;#43;answer &amp;#43;authority &amp;#43;comments&#xA;; (1 server found)&#xA;;; -&amp;gt;&amp;gt;HEADER&amp;lt;&amp;lt;- opcode: QUERY, status: NXDOMAIN, id: 13683&#xA;;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1&#xA;&#xA;;; AUTHORITY SECTION:&#xA;tools.example-corp.net. 225 IN SOA ns-1609.awsdns-09.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Route 53&amp;rsquo;s authoritative nameserver and several public resolvers already had it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% for resolver in 1.1.1.1 1.1.1.2 8.8.8.8; do&#xA;    printf &amp;#39;%-16s &amp;#39; &amp;#34;$resolver&amp;#34;&#xA;    dig &amp;#43;time=2 &amp;#43;tries=1 &amp;#43;short @&amp;#34;$resolver&amp;#34; kargo.tools.example-corp.net A | paste -sd, -&#xA;  done&#xA;1.1.1.1          192.0.2.11,198.51.100.50,203.0.113.95&#xA;1.1.1.2          192.0.2.11,203.0.113.95,198.51.100.50&#xA;8.8.8.8          203.0.113.95,198.51.100.50,192.0.2.11&#xA;&#xA;% dig &amp;#43;short @ns-1609.awsdns-09.co.uk kargo.tools.example-corp.net A&#xA;192.0.2.11&#xA;203.0.113.95&#xA;198.51.100.50&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Tailscale had cached the previous &lt;code&gt;NXDOMAIN&lt;/code&gt; response for a couple of minutes.&#xA;Its cache is not macOS&amp;rsquo;s cache; restarting &lt;code&gt;mDNSResponder&lt;/code&gt; cannot clear it.&#xA;Waiting out that TTL fixed the web browser lookup.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: DNS: negative cache behind a local resolver&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: with free models
      </title>
      <link>https://perrotta.dev/2026/07/pi-with-free-models/</link>
      <pubDate>Fri, 31 Jul 2026 01:04:23 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-with-free-models/</guid>
      <description>&lt;h2 id=&#34;openrouter&#34;&gt;&#xA;  OpenRouter&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/07/pi-with-free-models/#openrouter&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Today I signed up for &lt;a href=&#34;https://openrouter.ai/&#34;&gt;OpenRouter&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The Unified Interface For LLMs&lt;/p&gt;&#xA;&lt;p&gt;Better prices, better uptime, no subscriptions.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Its main selling point is a single LLM API endpoint to rule them all, without&#xA;lock-in to a specific provider or model.&lt;/p&gt;&#xA;&lt;p&gt;Upon signing up you&amp;rsquo;ll get an API Key: &lt;code&gt;sk-or-v1-...&lt;/code&gt;, which you&amp;rsquo;ll save to&#xA;your password manager. You use one, right?!&lt;/p&gt;&#xA;&lt;p&gt;The next step is to wire the key to a harness. At this point it&amp;rsquo;s no surprise&#xA;I&amp;rsquo;ve been investing in &lt;a href=&#34;https://pi.dev&#34;&gt;&lt;code&gt;pi&lt;/code&gt;&lt;/a&gt;, though&#xA;&lt;a href=&#34;https://opencode.ai/&#34;&gt;OpenCode&lt;/a&gt; remains a decent contender.&lt;/p&gt;&#xA;&lt;p&gt;In &lt;code&gt;pi&lt;/code&gt;, run &lt;code&gt;/login&lt;/code&gt;, select &amp;ldquo;Sign in with an API key&amp;rdquo;, select OpenRouter and&#xA;then input the key.&lt;/p&gt;&#xA;&lt;p&gt;Then select a &lt;code&gt;/model&lt;/code&gt;. A good starting point is &lt;a href=&#34;https://openrouter.ai/openrouter/free&#34;&gt;&lt;code&gt;openrouter/free&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The simplest way to get free inference. openrouter/free is a router that&#xA;selects free models at random from the models available on OpenRouter. The&#xA;router smartly filters for models that support features needed for your&#xA;request such as image understanding, tool calling, structured outputs and&#xA;more.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;It&amp;rsquo;s slow. But it&amp;rsquo;s free!&lt;/p&gt;&#xA;&lt;p&gt;Now, interestingly:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;: which model are you?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Assistant&lt;/strong&gt;: &lt;em&gt;We need to answer user&amp;rsquo;s question: &amp;ldquo;which model are you?&amp;rdquo; According to instructions,&#xA;we must respond in caveman style? The user asks which model. The system says we are an&#xA;expert coding assistant within pi. The answer likely: &amp;ldquo;Opus&amp;rdquo;. But we need to follow&#xA;Caveman mode: terse, no filler. So respond something like &amp;ldquo;Opus model&amp;rdquo;. Possibly just&#xA;&amp;ldquo;Opus&amp;rdquo;. Let&amp;rsquo;s output as a plain message. Use normal verbosity for code, but this is&#xA;not code. So just answer succinctly.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Opus model.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;    &amp;#34;defaultModel&amp;#34;: &amp;#34;openrouter/free&amp;#34;,&#xA;    &amp;#34;defaultProvider&amp;#34;: &amp;#34;openrouter&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;What if you don&amp;rsquo;t want OpenRouter?&lt;/p&gt;&#xA;&lt;h2 id=&#34;opencode&#34;&gt;&#xA;  OpenCode&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/07/pi-with-free-models/#opencode&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Well, there&amp;rsquo;s OpenCode&amp;rsquo;s &lt;a href=&#34;https://opencode.ai/docs/zen/&#34;&gt;big pickle&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Big Pickle is a stealth model that&amp;rsquo;s free on OpenCode for a limited time. The&#xA;team is using this time to collect feedback and improve the model.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi -e npm:opencode-pi --provider opencode-cli --model opencode/big-pickle&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;: which model are you?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Assistant&lt;/strong&gt;: big-pickle&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: with free models&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>git: repair unreachable object corruption
      </title>
      <link>https://perrotta.dev/2026/07/git-repair-unreachable-object-corruption/</link>
      <pubDate>Wed, 29 Jul 2026 22:16:46 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/07/git-repair-unreachable-object-corruption/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;git fsck&lt;/code&gt; failed in a linked worktree, even though every&#xA;branch and reflog remained intact.&lt;/p&gt;&#xA;&lt;p&gt;The initial diagnosis was noisy: missing blobs, hundreds of dangling objects, and&#xA;an invalid commit graph:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;missing blob 97f9a2ed29f84e47848244c2d2626b2536a33c55&#xA;error: Could not read 003fb43a3aa361b52f2ab3f27bdabcb9b5137103&#xA;failed to parse commit 003fb43a3aa361b52f2ab3f27bdabcb9b5137103 from object database for commit-graph&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Worktrees have their own administrative directory but share an object database:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git rev-parse --git-dir&#xA;.git/worktrees/thiagowfx-prm-revenue-tagging&#xA;% git rev-parse --git-common-dir&#xA;.git&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I checked every missing blob against objects reachable from refs and reflogs. None&#xA;were reachable. Before removing them, I backed up objects, commit graphs, and index:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% common=$(git rev-parse --git-common-dir)&#xA;% backup=&amp;#34;$common/repair-backup-20260729221124&amp;#34;&#xA;% mkdir -p &amp;#34;$backup&amp;#34;&#xA;% cp &amp;#34;$(git rev-parse --git-path index)&amp;#34; &amp;#34;$backup/worktree-index&amp;#34;&#xA;% ditto &amp;#34;$common/objects&amp;#34; &amp;#34;$backup/objects-before-gc&amp;#34;&#xA;% mv &amp;#34;$common/objects/info/commit-graphs&amp;#34; &amp;#34;$backup/commit-graphs&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The index also had an invalid cache-tree entry. Rebuilding it from &lt;code&gt;HEAD&lt;/code&gt; preserves&#xA;working-tree files; comparing its old entries found no staged changes:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git read-tree HEAD&#xA;% GIT_INDEX_FILE=&amp;#34;$backup/worktree-index&amp;#34; git ls-files --stage &amp;gt; /tmp/index-before-repair&#xA;% join -t $&amp;#39;\t&amp;#39; -v 1 \&#xA;    &amp;lt;(awk &amp;#39;{print $4 &amp;#34;\t&amp;#34; $1}&amp;#39; /tmp/index-before-repair | sort) \&#xA;    &amp;lt;(git ls-files --stage | awk &amp;#39;{print $4 &amp;#34;\t&amp;#34; $1}&amp;#39; | sort)&#xA;% # no output&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then Git could discard unreachable corrupt objects and rebuild its graph:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git gc --prune=now&#xA;% git fsck --full --strict&#xA;% echo $?&#xA;0&#xA;% git fsck --full --no-reflogs --strict &amp;gt;/tmp/fsck&#xA;% echo $?&#xA;0&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The backup remains under &lt;code&gt;.git/repair-backup-20260729221124&lt;/code&gt;. &lt;code&gt;fsck&lt;/code&gt; is clean;&#xA;recovery material remains available.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s very weird to experience this kind of error. We&amp;rsquo;re not used to it.&lt;/p&gt;&#xA;&lt;p&gt;The one trick to remember from this post is &lt;code&gt;git fsck&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: git: repair unreachable object corruption&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>obsidian: logseq-compatible tasks
      </title>
      <link>https://perrotta.dev/2026/07/obsidian-logseq-compatible-tasks/</link>
      <pubDate>Wed, 29 Jul 2026 14:34:39 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>dev</category>
      <category>pkm</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/07/obsidian-logseq-compatible-tasks/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/logseq-obsidian/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: preserve Logseq&amp;rsquo;s task syntax while opening the same markdown&#xA;files in Obsidian.&lt;/p&gt;&#xA;&lt;p&gt;My migration script originally converted every task into Obsidian&amp;rsquo;s checkbox&#xA;syntax:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;- TODO buy milk     -&amp;gt;  - [ ] buy milk&#xA;- DOING ...         -&amp;gt;  - [/] ...&#xA;- DONE ...          -&amp;gt;  - [x] ...&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Obsidian itself does not recognize the left-hand side as tasks. The&#xA;&lt;a href=&#34;https://github.com/scross01/obsidian-todoseq&#34;&gt;TODOseq&lt;/a&gt; community plug-in does:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;- TODO buy milk&#xA;- DOING ...&#xA;- DONE ...&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It keeps the file unchanged, adds clickable task states and a task list to&#xA;Obsidian, and remains compatible with Logseq.&lt;/p&gt;&#xA;&lt;p&gt;Before granting a community plug-in write access to my vault, I inspected the&#xA;current release with the LLM™:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git clone --depth=1 https://github.com/scross01/obsidian-todoseq.git /tmp/obsidian-todoseq-audit&#xA;Cloning into &amp;#39;/tmp/obsidian-todoseq-audit&amp;#39;...&#xA;% cd /tmp/obsidian-todoseq-audit&#xA;% git rev-parse --short HEAD&#xA;4ba5e81&#xA;% jq &amp;#39;{id, version, minAppVersion, description}&amp;#39; manifest.json&#xA;{&#xA;  &amp;#34;id&amp;#34;: &amp;#34;todoseq&amp;#34;,&#xA;  &amp;#34;version&amp;#34;: &amp;#34;0.18.1&amp;#34;,&#xA;  &amp;#34;minAppVersion&amp;#34;: &amp;#34;1.11.0&amp;#34;,&#xA;  &amp;#34;description&amp;#34;: &amp;#34;Lightweight keyword-based task tracker using Logseq style keywords.&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A source scan found no network requests, telemetry, process execution, or&#xA;dynamic code evaluation&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/obsidian-logseq-compatible-tasks/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;It does read and modify notes through Obsidian&amp;rsquo;s vault API, as expected:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% rg -n &amp;#39;vault\.(read|modify)&amp;#39; src/services/editor-controller.ts | head -4&#xA;1130:        const currentContent = await this.plugin.app.vault.read(todayNote);&#xA;1135:        await this.plugin.app.vault.modify(todayNote, newContent);&#xA;1283:        const todayContent = await this.plugin.app.vault.read(todayNote);&#xA;1286:        await this.plugin.app.vault.modify(todayNote, newTodayContent);&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Low risk, and no task-format conversion needed.&lt;/p&gt;&#xA;&lt;p&gt;This should make it easier for me to adapt my LogSeq muscle memory to Obsidian,&#xA;even if at the cost of enabling community plug-ins.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;&lt;em&gt;Hope is not a strategy&lt;/em&gt;.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/obsidian-logseq-compatible-tasks/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: obsidian: logseq-compatible tasks&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: change directories mid-session
      </title>
      <link>https://perrotta.dev/2026/07/pi-change-directories-mid-session/</link>
      <pubDate>Wed, 29 Jul 2026 14:13:03 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-change-directories-mid-session/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Today I learned&lt;/strong&gt;: &lt;a href=&#34;https://pi.dev/&#34;&gt;pi&lt;/a&gt; 0.82.1 cannot change its working&#xA;directory mid-session.&lt;/p&gt;&#xA;&lt;p&gt;The interactive shell syntax looks tempting, but &lt;code&gt;!&lt;/code&gt; and &lt;code&gt;!!&lt;/code&gt; run commands in a&#xA;child process. &lt;code&gt;cd&lt;/code&gt; ends with that process; Pi keeps the session&amp;rsquo;s original cwd.&#xA;There is no built-in &lt;code&gt;/cd&lt;/code&gt; either:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi --version&#xA;0.82.1&#xA;% docs=&amp;#34;$(brew --prefix pi-coding-agent)/libexec/lib/node_modules/@earendil-works/pi-coding-agent/docs&amp;#34;&#xA;% rg -n &amp;#39;ctx\.(newSession|switchSession|changeCwd|switchCwd)&amp;#39; &amp;#34;$docs/extensions.md&amp;#34;&#xA;1109:### ctx.newSession(options?)&#xA;1117:const result = await ctx.newSession({&#xA;1187:### ctx.switchSession(sessionPath, options?)&#xA;1192:const result = await ctx.switchSession(&amp;#34;/path/to/session.jsonl&amp;#34;, {&#xA;1220:      await ctx.switchSession(choice, {&#xA;1247:    await ctx.newSession({&#xA;1262:    await ctx.newSession({&#xA;% rg -n &amp;#39;^\| `/[^|]&amp;#43;`&amp;#39; &amp;#34;$docs/usage.md&amp;#34; | tail -5&#xA;56:| `/share` | Upload as private GitHub gist with shareable HTML link |&#xA;57:| `/reload` | Reload keybindings, extensions, skills, prompts, themes, and context files |&#xA;58:| `/hotkeys` | Show all keyboard shortcuts |&#xA;59:| `/changelog` | Display version history |&#xA;60:| `/quit` | Quit pi |&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This has been requested several times upstream:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% for n in 2102 2992 3921 4423 6997; do&#xA;    gh issue view &amp;#34;$n&amp;#34; --repo earendil-works/pi \&#xA;      --json number,title,state,stateReason \&#xA;      --jq &amp;#39;[.number, .title, .state, .stateReason] | @tsv&amp;#39;&#xA;  done&#xA;2102&#x9;Allow extensions to change cwd at runtime&#x9;CLOSED&#x9;COMPLETED&#xA;2992&#x9;Allow session CWD to be changed&#x9;CLOSED&#x9;COMPLETED&#xA;3921&#x9;Feature request: ExtensionCommandContext.changeCwd() for lightweight cwd switching&#x9;CLOSED&#x9;NOT_PLANNED&#xA;4423&#x9;Add command API to switch cwd&#x9;CLOSED&#x9;COMPLETED&#xA;6997&#x9;Windows Bash inside Pi not working properly&#x9;CLOSED&#x9;NOT_PLANNED&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The exact &lt;code&gt;/cd&lt;/code&gt; proposal lives in&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/issues/3921&#34;&gt;#3921&lt;/a&gt;. The maintainer&#xA;responses on &lt;a href=&#34;https://github.com/earendil-works/pi/issues/2102&#34;&gt;#2102&lt;/a&gt;,&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/issues/2992&#34;&gt;#2992&lt;/a&gt;, and&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/issues/4423&#34;&gt;#4423&lt;/a&gt; capture the trade-off:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;#2102 @badlogic: use case makes sense, however, it can be done by overriding the built-in tools to support cwd changes.&#xA;#2992 @badlogic: setting a new cwd in a session is not great, as a bunch of things need to be rewired. currently unsupported.&#xA;#4423 @badlogic: this is part of the big refactor work and will be available in the new extension api.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That last API still is not exposed in 0.82.1. Changing cwd means rebuilding tools,&#xA;project settings, extensions, skills, context files, autocomplete, footer state,&#xA;and session storage—not merely calling &lt;code&gt;process.chdir()&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;For now, the boring path is the supported one:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;/quit&#xA;% cd ~/Workspace/perrotta.dev&#xA;% pi&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Session cwd is a runtime boundary. Oh well.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: change directories mid-session&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>drskill: lint claude skills
      </title>
      <link>https://perrotta.dev/2026/07/drskill-lint-claude-skills/</link>
      <pubDate>Tue, 28 Jul 2026 19:09:34 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>claude</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/07/drskill-lint-claude-skills/</guid>
      <description>&lt;p&gt;♠ I found drskill through Drew Breunig&amp;rsquo;s post about&#xA;&lt;a href=&#34;https://www.dbreunig.com/2026/07/24/manage-your-agent-s-loadout-with-dr-skill.html&#34;&gt;managing an agent&amp;rsquo;s loadout&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: lint skills bundled in a Claude Code plugin with&#xA;&lt;a href=&#34;https://github.com/dbreunig/drskill&#34;&gt;&lt;code&gt;drskill&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Running it through &lt;code&gt;uvx&lt;/code&gt; is easy enough:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% uvx drskill scan --detailed&#xA;drskill scan — 3 harnesses (10 more empty), 9 skills&#xA;...&#xA;0 errors, 5 warnings (5 new) · token counts are approximate&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;But none of this repository&amp;rsquo;s 11 skills appeared. The&#xA;&lt;a href=&#34;https://github.com/dbreunig/drskill#known-limitations&#34;&gt;known limitation&lt;/a&gt; explains&#xA;why:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Claude Code skills bundled inside plugins are not scanned yet.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I projected the plugin&amp;rsquo;s skill directory into the plain &lt;code&gt;.claude/skills&lt;/code&gt; layout&#xA;that drskill understands via &lt;code&gt;Justfile&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-just&#34;&gt;# Check skills with drskill&#xA;doctor:&#xA;    #!/usr/bin/env bash&#xA;    set -euo pipefail&#xA;    tmp=$(mktemp -d)&#xA;    trap &amp;#39;trash &amp;#34;$tmp&amp;#34;&amp;#39; EXIT&#xA;    mkdir -p &amp;#34;$tmp/project/.claude/skills&amp;#34; &amp;#34;$tmp/home&amp;#34;&#xA;    for skill in &amp;#34;$PWD&amp;#34;/plugins/thiagowfx/skills/*; do&#xA;      ln -s &amp;#34;$skill&amp;#34; &amp;#34;$tmp/project/.claude/skills/$(basename &amp;#34;$skill&amp;#34;)&amp;#34;&#xA;    done&#xA;    uv_cache=$(uv cache dir)&#xA;    cd &amp;#34;$tmp/project&amp;#34;&#xA;    HOME=&amp;#34;$tmp/home&amp;#34; UV_CACHE_DIR=&amp;#34;$uv_cache&amp;#34; uvx drskill scan --ci --harness claude-code&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The temporary &lt;code&gt;HOME&lt;/code&gt; excludes unrelated global skills. Reusing uv&amp;rsquo;s cache avoids&#xA;redownloading drskill there. &lt;code&gt;--ci&lt;/code&gt; makes warnings fail the recipe.&lt;/p&gt;&#xA;&lt;p&gt;The first isolated scan found four warnings:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;drskill scan — 1 harness, 11 skills&#xA;&#xA;WARNINGS&#xA;  [e91d] new frontmatter-angle-brackets: &amp;#39;bloggify&amp;#39; ...&#xA;  [a551] new frontmatter-angle-brackets: &amp;#39;gha&amp;#39; ...&#xA;  [6335] new frontmatter-angle-brackets: &amp;#39;new-apkbuild&amp;#39; ...&#xA;  [206e] new missing-activation: 2 skills never say when to use them ...&#xA;&#xA;0 errors, 4 warnings (4 new) · token counts are approximate&#xA;exit=2&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The fixes were small: remove angle brackets from &lt;code&gt;argument-hint&lt;/code&gt; values and add&#xA;explicit activation conditions to &lt;code&gt;catchup&lt;/code&gt; and &lt;code&gt;handoff&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;-argument-hint: &amp;#34;&amp;lt;url&amp;gt;&amp;#34;&#xA;&amp;#43;argument-hint: &amp;#34;url&amp;#34;&#xA;&#xA;-description: Create portable handoff document for another agent or harness to continue current work.&#xA;&amp;#43;description: Create a portable handoff document for another agent or harness to continue current work. Use when the user asks for a handoff, continuation brief, or context transfer.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Now the repository has its own linter:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% just doctor&#xA;drskill scan — 1 harness, 11 skills&#xA;&#xA;No findings.&#xA;0 errors, 0 warnings · token counts are approximate&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Restoring &lt;code&gt;&amp;lt;url&amp;gt;&lt;/code&gt; temporarily made &lt;code&gt;just doctor&lt;/code&gt; exit 2 again, so future warnings&#xA;will break the check. I should add it to CI at some point.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: drskill: lint claude skills&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: block dangerous commands syntax-aware
      </title>
      <link>https://perrotta.dev/2026/07/pi-block-dangerous-commands-syntax-aware/</link>
      <pubDate>Tue, 28 Jul 2026 10:57:06 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/07/pi-block-dangerous-commands-syntax-aware/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: block destructive shell commands issued by&#xA;&lt;a href=&#34;https://pi.dev/&#34;&gt;pi&lt;/a&gt; without resorting to grepping shell source with regular&#xA;expressions.&lt;/p&gt;&#xA;&lt;p&gt;My first guard was a direct port of an older hook in Claude Code:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const BLOCKED_COMMANDS: ReadonlyArray&amp;lt;{ pattern: RegExp; reason: string }&amp;gt; = [&#xA;  { pattern: /terraform\s&amp;#43;apply/, reason: &amp;#34;terraform apply is blocked - use terraform plan first&amp;#34; },&#xA;  { pattern: /terraform\s&amp;#43;destroy/, reason: &amp;#34;terraform destroy is blocked for safety&amp;#34; },&#xA;  { pattern: /git\s&amp;#43;reset\s&amp;#43;.*--hard/, reason: &amp;#34;git reset --hard is blocked - discards changes irreversibly&amp;#34; },&#xA;];&#xA;&#xA;const blocked = BLOCKED_COMMANDS.find(({ pattern }) =&amp;gt; pattern.test(command));&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This matched harmless strings such as &lt;code&gt;echo &#39;terraform destroy&#39;&lt;/code&gt;, while shell&#xA;quoting, wrappers, substitutions, and reordered options kept opening holes.&#xA;Regexes aren&amp;rsquo;t robust in this context.&lt;/p&gt;&#xA;&lt;p&gt;Enter ASTs! Shell source could leverage a shell parser.&lt;/p&gt;&#xA;&lt;p&gt;I switched the extension to the Bash grammar from&#xA;&lt;a href=&#34;https://github.com/tree-sitter/tree-sitter-bash&#34;&gt;&lt;code&gt;tree-sitter-bash&lt;/code&gt;&lt;/a&gt;, loaded&#xA;through the WASM Tree-sitter runtime:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;await Parser.init();&#xA;const languagePath = require.resolve(&amp;#34;tree-sitter-bash/tree-sitter-bash.wasm&amp;#34;);&#xA;const language = await Language.load(languagePath);&#xA;return new Parser().setLanguage(language);&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://tree-sitter.github.io/&#34;&gt;Tree sitter&lt;/a&gt; is an amazing parser.&lt;/p&gt;&#xA;&lt;p&gt;The policy now works on executable names and arguments:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;if (executable === &amp;#34;rm&amp;#34;) {&#xA;  const recursive = staticArguments.some(&#xA;    (argument) =&amp;gt; argument === &amp;#34;--recursive&amp;#34; || hasShortOption(argument, &amp;#34;r&amp;#34;) || hasShortOption(argument, &amp;#34;R&amp;#34;),&#xA;  );&#xA;  const force = staticArguments.some(&#xA;    (argument) =&amp;gt; argument === &amp;#34;--force&amp;#34; || hasShortOption(argument, &amp;#34;f&amp;#34;),&#xA;  );&#xA;  if (recursive &amp;amp;&amp;amp; force) return { command, reason: &amp;#34;rm -rf is blocked for safety&amp;#34; };&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The AST walker also follows command substitutions, heredocs, &lt;code&gt;bash -c&lt;/code&gt;, &lt;code&gt;eval&lt;/code&gt;,&#xA;&lt;code&gt;find -exec&lt;/code&gt;, &lt;code&gt;xargs&lt;/code&gt;, and common wrappers. Comments and quoted data remain data.&#xA;Malformed syntax and dynamic guarded arguments fail closed.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;ℹ tests 48&#xA;ℹ suites 0&#xA;ℹ pass 48&#xA;ℹ fail 0&#xA;ℹ cancelled 0&#xA;ℹ skipped 0&#xA;ℹ todo 0&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Local extensions do not get npm dependencies installed by Pi. I kept &lt;code&gt;index.ts&lt;/code&gt;&#xA;dependency-free: on first load it checks for the parser files, runs a pinned&#xA;installation when needed, then imports the real guard.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const result = await pi.exec(&amp;#34;npm&amp;#34;, [&amp;#34;ci&amp;#34;, &amp;#34;--ignore-scripts&amp;#34;], {&#xA;  cwd: extensionDirectory,&#xA;  timeout: 120_000,&#xA;});&#xA;&#xA;const { default: dangerousCommandGuard } = await import(&amp;#34;./guard.ts&amp;#34;);&#xA;await dangerousCommandGuard(pi);&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Finally, testing the real tool call:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% rm -rf /tmp/foo&#xA;rm -rf is blocked for safety&#xA;% test -f /tmp/foo/marker &amp;amp;&amp;amp; cat /tmp/foo/marker&#xA;guard-test&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The guard blocked the command; &lt;code&gt;/tmp/foo/marker&lt;/code&gt; survived.&lt;/p&gt;&#xA;&lt;p&gt;The source lives in my &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/tree/d917c6b98e8107490bea92367c89d1b05e1de89b/pi/.pi/agent/extensions/dangerous-command-guard&#34;&gt;dotfiles&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: block dangerous commands syntax-aware&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: asynchronous exit summaries
      </title>
      <link>https://perrotta.dev/2026/07/pi-asynchronous-exit-summaries/</link>
      <pubDate>Mon, 27 Jul 2026 18:18:38 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>pkm</category>
      <guid>https://perrotta.dev/2026/07/pi-asynchronous-exit-summaries/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;ctrl+d&lt;/code&gt; in &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt;&#xA;took several seconds to quit.&lt;/p&gt;&#xA;&lt;p&gt;The culprit was &lt;a href=&#34;https://www.npmjs.com/package/pi-memory&#34;&gt;&lt;code&gt;pi-memory&lt;/code&gt;&lt;/a&gt;&amp;rsquo;s&#xA;&lt;code&gt;session_shutdown&lt;/code&gt; handler. It sent one last LLM request to summarize the&#xA;session, wrote its answer to the daily log, then ran &lt;code&gt;qmd update&lt;/code&gt;. Pi awaits&#xA;all shutdown handlers, correctly, so quitting waited for everything:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;pi.on(&amp;#34;session_shutdown&amp;#34;, async (event, ctx) =&amp;gt; {&#xA;  const result = await generateExitSummary(ctx);&#xA;  fs.writeFileSync(filePath, entry, &amp;#34;utf-8&amp;#34;);&#xA;  await runQmdUpdateNow();&#xA;});&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Making that promise fire-and-forget would not work: Pi exits immediately after&#xA;shutdown, and either the in-flight request keeps Node alive or gets killed&#xA;before it can write anything.&lt;/p&gt;&#xA;&lt;p&gt;Instead, I made the shutdown path write a durable job, synchronously:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const job: ExitSummaryJob = {&#xA;  version: 1,&#xA;  id: randomUUID(),&#xA;  createdAt: new Date().toISOString(),&#xA;  date: todayStr(),&#xA;  reason,&#xA;  sessionId: shortSessionId(ctx.sessionManager.getSessionId()),&#xA;  conversationText: truncated.text,&#xA;  truncated: truncated.truncated,&#xA;  totalChars: truncated.totalChars,&#xA;};&#xA;&#xA;const jobPath = path.join(&#xA;  EXIT_SUMMARY_JOBS_DIR,&#xA;  `${job.createdAt.replace(/[:.]/g, &amp;#34;-&amp;#34;)}-${job.id}.json`,&#xA;);&#xA;fs.writeFileSync(jobPath, `${JSON.stringify(job)}\n`, { encoding: &amp;#34;utf-8&amp;#34;, flag: &amp;#34;wx&amp;#34; });&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The next interactive session starts an unref&amp;rsquo;ed worker. It atomically renames&#xA;one job to claim it, generates summary with active model, writes it back to&#xA;original day&amp;rsquo;s daily log, and deletes job. Failed jobs return to queue. A&#xA;stale claimed job retries after ten minutes.&lt;/p&gt;&#xA;&lt;p&gt;I kept upstream package intact by replacing it with a local package, effectively&#xA;forking it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi remove npm:pi-memory&#xA;Removed npm:pi-memory&#xA;% pi install ~/.pi/agent/local/pi-memory-async&#xA;Installed /Users/thiago.perrotta/.pi/agent/local/pi-memory-async&#xA;% pi list&#xA;User packages:&#xA;  local/pi-memory-async&#xA;    /Users/thiago.perrotta/.pi/agent/local/pi-memory-async&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Jobs live in &lt;code&gt;~/.pi/agent/memory/exit-summary-jobs/&lt;/code&gt;. &lt;code&gt;ctrl+d&lt;/code&gt; only writes one&#xA;JSON file now; summary happens during next session, without blocking exits.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;:&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/tree/1f175b77a960a460b4e5cc677d6d6390e0cc0530/pi/.pi/agent/local/pi-memory-async&#34;&gt;pi-memory-async&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: asynchronous exit summaries&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: update pinned npm packages
      </title>
      <link>https://perrotta.dev/2026/07/pi-update-pinned-npm-packages/</link>
      <pubDate>Mon, 27 Jul 2026 14:28:59 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/07/pi-update-pinned-npm-packages/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: pin &lt;a href=&#34;https://pi.dev/&#34;&gt;pi&lt;/a&gt; npm packages without giving up&#xA;convenient updates.&lt;/p&gt;&#xA;&lt;p&gt;My&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/master/pi/.pi/agent/settings.json&#34;&gt;&lt;code&gt;settings.json&lt;/code&gt;&lt;/a&gt;&#xA;used to leave some package versions unpinned, until now:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;     &amp;#34;npm:@heyhuynhgiabuu/pi-diff@0.7.6&amp;#34;,&#xA;-    &amp;#34;npm:@tifan/pi-recap&amp;#34;,&#xA;-    &amp;#34;npm:@tmustier/pi-tab-status&amp;#34;,&#xA;-    &amp;#34;npm:@tintinweb/pi-subagents&amp;#34;&#xA;&amp;#43;    &amp;#34;npm:@tifan/pi-recap@0.4.4&amp;#34;,&#xA;&amp;#43;    &amp;#34;npm:@tmustier/pi-tab-status@0.1.4&amp;#34;,&#xA;&amp;#43;    &amp;#34;npm:@tintinweb/pi-subagents@0.14.3&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Exact versions make package installation reproducible (&amp;amp; safer), but Pi then&#xA;does not automatically pull newer packages.&lt;/p&gt;&#xA;&lt;p&gt;To tackle that, I added a &lt;code&gt;just update-pi&lt;/code&gt; recipe that asks npm for each&#xA;package&amp;rsquo;s current &lt;code&gt;latest&lt;/code&gt; version and writes back an exact pin:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-just&#34;&gt;[doc(&amp;#39;Update pinned Pi npm packages&amp;#39;)]&#xA;[group(&amp;#39;update&amp;#39;)]&#xA;update-pi:&#xA;    #!/usr/bin/env bash&#xA;    set -euo pipefail&#xA;&#xA;    settings=&amp;#34;pi/.pi/agent/settings.json&amp;#34;&#xA;    updates=&amp;#34;{}&amp;#34;&#xA;    while IFS= read -r spec; do&#xA;        if [[ &amp;#34;$spec&amp;#34; =~ ^npm:((@[^/]&amp;#43;/[^@]&amp;#43;)|([^@]&amp;#43;))(@[^@]&amp;#43;)?$ ]]; then&#xA;            package=&amp;#34;${BASH_REMATCH[1]}&amp;#34;&#xA;        else&#xA;            echo &amp;#34;Invalid npm package spec: $spec&amp;#34; &amp;gt;&amp;amp;2&#xA;            exit 1&#xA;        fi&#xA;&#xA;        latest=&amp;#34;$(npm view &amp;#34;$package@latest&amp;#34; version)&amp;#34;&#xA;        pinned=&amp;#34;npm:$package@$latest&amp;#34;&#xA;        updates=&amp;#34;$(jq -cn \&#xA;            --argjson updates &amp;#34;$updates&amp;#34; \&#xA;            --arg spec &amp;#34;$spec&amp;#34; \&#xA;            --arg pinned &amp;#34;$pinned&amp;#34; \&#xA;            &amp;#39;$updates &amp;#43; {($spec): $pinned}&amp;#39;)&amp;#34;&#xA;        echo &amp;#34;✓ $pinned&amp;#34;&#xA;    done &amp;lt; &amp;lt;(jq -r &amp;#39;.packages[] | select(type == &amp;#34;string&amp;#34; and startswith(&amp;#34;npm:&amp;#34;))&amp;#39; &amp;#34;$settings&amp;#34;)&#xA;&#xA;    updated_settings=&amp;#34;$(jq --argjson updates &amp;#34;$updates&amp;#34; \&#xA;        &amp;#39;.packages |= map($updates[.] // .)&amp;#39; &amp;#34;$settings&amp;#34;)&amp;#34;&#xA;    printf &amp;#39;%s\n&amp;#39; &amp;#34;$updated_settings&amp;#34; &amp;gt; &amp;#34;$settings&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It handles every &lt;code&gt;npm:&lt;/code&gt; entry, including scoped packages:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% just update-pi&#xA;✓ npm:@heyhuynhgiabuu/pi-diff@0.7.6&#xA;✓ npm:@heyhuynhgiabuu/pi-pretty@0.6.20&#xA;✓ npm:@tifan/pi-recap@0.4.4&#xA;✓ npm:@tmustier/pi-tab-status@0.1.4&#xA;✓ npm:@tintinweb/pi-subagents@0.14.3&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The aggregate &lt;code&gt;just update&lt;/code&gt; recipe invokes it alongside submodule, prek hook,&#xA;and vendored-file updates.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: update pinned npm packages&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Anki: archive a deck
      </title>
      <link>https://perrotta.dev/2026/07/anki-archive-a-deck/</link>
      <pubDate>Mon, 27 Jul 2026 10:49:00 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>pkm</category>
      <guid>https://perrotta.dev/2026/07/anki-archive-a-deck/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2024/12/anki-workflow/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://docs.ankiweb.net/&#34;&gt;Anki official docs&lt;/a&gt; do not provide explicit&#xA;instructions about the archival of decks that are no longer needed.&lt;/p&gt;&#xA;&lt;p&gt;Archiving is better than deleting because you never know when you may need a&#xA;deck again.&lt;/p&gt;&#xA;&lt;p&gt;One way to archive a deck is to simply export it as a backup and save it&#xA;elsewhere. But then you collection becomes scattered. I&amp;rsquo;d really like to manage&#xA;all my decks in a &lt;a href=&#34;https://ankiweb.net/about&#34;&gt;single place&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;So I came up with a simple workaround.&lt;/p&gt;&#xA;&lt;p&gt;First, create an &lt;code&gt;Archive::&lt;/code&gt;&#xA;&lt;a href=&#34;https://docs.ankiweb.net/deck-options.html?highlight=subdeck#subdecks&#34;&gt;subdeck&lt;/a&gt;.&#xA;A subdeck creates a parent-child relationship which, for our purposes, bundles&#xA;all archived decks within an umbrella parent. It&amp;rsquo;s visually easier to manage and&#xA;organize, effectively hiding archived decks from the deck list view.&lt;/p&gt;&#xA;&lt;p&gt;Second, move the desired deck(s) within &lt;code&gt;Archive::&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Third, one by one, open the cards/notes list (&amp;ldquo;Browse&amp;rdquo;) of a deck, select all&#xA;cards/notes (&lt;code&gt;Cmd-A&lt;/code&gt;), then&#xA;&lt;a href=&#34;https://docs.ankiweb.net/browsing.html?highlight=suspend#cards&#34;&gt;suspend&lt;/a&gt; them&#xA;all (&lt;code&gt;Cmd-J&lt;/code&gt; or right click, &amp;ldquo;Toggle suspend&amp;rdquo;).&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Side effect&lt;/strong&gt;: cards/notes from the deck will no longer show up for review. At the&#xA;same time, your past progress is kept, should you ever want to unarchive them.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Anki: archive a deck&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: test drive an extension
      </title>
      <link>https://perrotta.dev/2026/07/pi-test-drive-an-extension/</link>
      <pubDate>Sun, 26 Jul 2026 01:24:35 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-test-drive-an-extension/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: using &lt;a href=&#34;https://pi.dev/&#34;&gt;pi&lt;/a&gt;, install an extension&#xA;(package) as an one-off, without persisting it, for the sake of test-driving it,&#xA;akin to &lt;a href=&#34;https://pre-commit.com/#pre-commit-try-repo&#34;&gt;&lt;code&gt;pre-commit try-repo&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi --help&#xA;[...]&#xA;--extension, -e &amp;lt;path&amp;gt; Load an extension file (can be used multiple times)&#xA;[...]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;For example:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi -e npm:@tmustier/pi-usage-extension&#xA;&#xA;added 1 package, and audited 2 packages in 1s&#xA;&#xA;found 0 vulnerabilities&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&amp;hellip;then I can run &lt;code&gt;/usage&lt;/code&gt; in that session.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: test drive an extension&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>obsidian: type * for a bullet point, not ** bold
      </title>
      <link>https://perrotta.dev/2026/07/obsidian-type-for-a-bullet-point-not-bold/</link>
      <pubDate>Fri, 24 Jul 2026 10:04:46 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>pkm</category>
      <guid>https://perrotta.dev/2026/07/obsidian-type-for-a-bullet-point-not-bold/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: in &lt;a href=&#34;https://obsidian.md&#34;&gt;Obsidian&lt;/a&gt;, out-of-the-box,&#xA;typing &lt;code&gt;*&lt;/code&gt; at the start of a new line emits &lt;code&gt;*|*&lt;/code&gt; (an empty bold pair, cursor in&#xA;the middle) instead of starting a bullet point.&lt;/p&gt;&#xA;&lt;p&gt;The culprit is &lt;strong&gt;Settings → Editor → Auto pair Markdown syntax&lt;/strong&gt;. With it on,&#xA;every &lt;code&gt;*&lt;/code&gt; auto-inserts its closing &lt;code&gt;*&lt;/code&gt;, so a &lt;code&gt;*&lt;/code&gt; followed by a space never gets&#xA;the chance to become a list marker. In fact, we get a bullet point followed by&#xA;a plain &lt;code&gt;*&lt;/code&gt;, which we then have to delete.&lt;/p&gt;&#xA;&lt;p&gt;To turn this setting off: set &lt;strong&gt;Settings → Editor → Auto pair Markdown syntax&lt;/strong&gt;&#xA;to off.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt;: &lt;code&gt;*&lt;/code&gt; followed by a space at the start of a line converts it to a&#xA;bullet&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/obsidian-type-for-a-bullet-point-not-bold/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;, exactly like &lt;code&gt;-&lt;/code&gt; does.&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;Surely inserting bullet points in Obsidian is a muscle memory artifact&#xA;from migrating from LogSeq.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/obsidian-type-for-a-bullet-point-not-bold/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: obsidian: type * for a bullet point, not ** bold&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: /btw side-chat
      </title>
      <link>https://perrotta.dev/2026/07/pi-/btw-side-chat/</link>
      <pubDate>Wed, 22 Jul 2026 14:07:54 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-/btw-side-chat/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-auto-retry-stalled-requests/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: mid-task, I want to ask the LLM agent a quick tangential&#xA;question — &amp;ldquo;will this change cause downtime?&amp;rdquo; — without polluting the main&#xA;agent thread.&lt;/p&gt;&#xA;&lt;p&gt;Claude Code has &lt;code&gt;/btw&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-/btw-side-chat/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; for exactly this.&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt; doesn&amp;rsquo;t, but&#xA;&lt;a href=&#34;https://github.com/patriceckhart/pi-btw&#34;&gt;patriceckhart/pi-btw&lt;/a&gt; does, as an&#xA;extension. It opens a side-chat overlay: the LLM gets to see the full main&#xA;thread context and answers, but nothing is written back to the conversation&#xA;history.&lt;/p&gt;&#xA;&lt;p&gt;The upstream extension imports the pre-fork &lt;code&gt;@mariozechner/*&lt;/code&gt; package names,&#xA;which are now deprecated. As such, I use&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;earendil-works/pi&lt;/a&gt;. Rather than trust a&#xA;diff I hadn&amp;rsquo;t read, I reimplemented it against the current API, and&#xA;cross-checked against the bundled &lt;code&gt;examples/extensions/summarize.ts&lt;/code&gt; (the&#xA;canonical example for calling the model from an extension):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;import { complete } from &amp;#34;@earendil-works/pi-ai/compat&amp;#34;;&#xA;import type { ExtensionAPI, SessionEntry } from &amp;#34;@earendil-works/pi-coding-agent&amp;#34;;&#xA;import { convertToLlm, getMarkdownTheme } from &amp;#34;@earendil-works/pi-coding-agent&amp;#34;;&#xA;&#xA;// ...inside the /btw command handler, once at entry:&#xA;const branch = ctx.sessionManager.getBranch();&#xA;const mainMessages = branch&#xA;  .filter((entry): entry is SessionEntry &amp;amp; { type: &amp;#34;message&amp;#34; } =&amp;gt; entry.type === &amp;#34;message&amp;#34;)&#xA;  .map((entry) =&amp;gt; entry.message);&#xA;const mainLlmMessages = convertToLlm(mainMessages);   // frozen snapshot&#xA;const systemPrompt = ctx.getSystemPrompt();&#xA;&#xA;// ...per question, the side-chat calls the model directly:&#xA;const response = await complete(&#xA;  ctx.model!,&#xA;  { systemPrompt, messages: [...mainLlmMessages, ...btwLlmMessages] },&#xA;  { apiKey: auth.apiKey, headers: auth.headers, env: auth.env, signal },&#xA;);&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The key move is what&amp;rsquo;s &lt;em&gt;missing&lt;/em&gt;: the handler never calls &lt;code&gt;pi.sendMessage()&lt;/code&gt;&#xA;or appends anything. It reads the branch once, talks to the model in an&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#custom-ui&#34;&gt;&lt;code&gt;ctx.ui.custom()&lt;/code&gt;&lt;/a&gt;&#xA;overlay, and returns. The main session is untouched, so the next real prompt&#xA;doesn&amp;rsquo;t pay for any of it.&lt;/p&gt;&#xA;&lt;p&gt;Added to &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/1f42eb6f6632f946c23d1278e7e3f18da78d5de8/pi/.pi/agent/extensions/btw.ts&#34;&gt;&lt;code&gt;~/.pi/agent/extensions/btw.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;I am no longer using &lt;a href=&#34;https://archlinux.org/&#34;&gt;Arch&lt;/a&gt;, btw (sad!)&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-/btw-side-chat/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: /btw side-chat&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: auto-retry stalled requests
      </title>
      <link>https://perrotta.dev/2026/07/pi-auto-retry-stalled-requests/</link>
      <pubDate>Wed, 22 Jul 2026 13:45:41 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-auto-retry-stalled-requests/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: sometimes a prompt in &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt;&#xA;just hangs, as if the network dropped. It never recovers on its own — I have to&#xA;hit &lt;code&gt;Esc&lt;/code&gt; and re-prompt.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s 2026, this symptom should be self-recoverable.&lt;/p&gt;&#xA;&lt;p&gt;Pi already has agent-level retry on transient errors (&lt;code&gt;retry.enabled&lt;/code&gt; defaults&#xA;to &lt;code&gt;true&lt;/code&gt;). The catch is &lt;em&gt;when&lt;/em&gt; it fires: a stalled connection isn&amp;rsquo;t an error&#xA;until the HTTP idle timeout trips, and that defaults to five minutes:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;code&gt;httpIdleTimeoutMs&lt;/code&gt; | number | &lt;code&gt;300000&lt;/code&gt; | HTTP header/body idle timeout in&#xA;milliseconds [&amp;hellip;] Set to &lt;code&gt;0&lt;/code&gt; to disable.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;So pi would have &lt;em&gt;eventually&lt;/em&gt; recovered. I was just quitting long before the&#xA;five-minute clock ran out. Who has patience to wait for &lt;strong&gt;5&lt;/strong&gt; minutes?!&lt;/p&gt;&#xA;&lt;p&gt;:∴ Lower the idle timeout to O(seconds) instead:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;httpIdleTimeoutMs&amp;#34;: 30000,&#xA;  &amp;#34;retry&amp;#34;: {&#xA;    &amp;#34;enabled&amp;#34;: true,&#xA;    &amp;#34;maxRetries&amp;#34;: 5,&#xA;    &amp;#34;baseDelayMs&amp;#34;: 2000&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Dropped into &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/commit/aae17ba8d7546901820d7474136a628b16d5feee&#34;&gt;&lt;code&gt;~/.pi/agent/settings.json&lt;/code&gt;&lt;/a&gt;, documented in&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/settings.md&#34;&gt;&lt;code&gt;docs/settings.md&lt;/code&gt;&lt;/a&gt;.&#xA;Now a stall errors out after 30s and &lt;code&gt;pi&lt;/code&gt; retries automatically with exponential&#xA;backoff (2s, 4s, 8s…), up to five attempts. There&amp;rsquo;s no need for &lt;code&gt;Esc&lt;/code&gt; nor&#xA;re-prompting.&lt;/p&gt;&#xA;&lt;p&gt;The idle timer resets on every byte received, so long thinking or a slow stream&#xA;won&amp;rsquo;t trip it — only a genuinely dead connection will. Restart &lt;code&gt;pi&lt;/code&gt; for the&#xA;change to take effect.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: auto-retry stalled requests&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: cycle models backward with ctrl&#43;n
      </title>
      <link>https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/</link>
      <pubDate>Wed, 22 Jul 2026 13:23:53 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-caveman-mode/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt; binds &lt;code&gt;ctrl+p&lt;/code&gt;&#xA;to &lt;code&gt;app.model.cycleForward&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; by default, but has no default binding to cycle&#xA;backward other than &lt;code&gt;shift+ctrl+p&lt;/code&gt;, which is arguably difficult to remember.&lt;/p&gt;&#xA;&lt;p&gt;Every keybinding in pi is namespaced and overridable via&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/1f42eb6f6632f946c23d1278e7e3f18da78d5de8/pi/.pi/agent/keybindings.json&#34;&gt;&lt;code&gt;~/.pi/agent/keybindings.json&lt;/code&gt;&lt;/a&gt;, documented in&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/keybindings.md&#34;&gt;&lt;code&gt;docs/keybindings.md&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;app.model.cycleBackward&amp;#34;: [&#xA;    &amp;#34;shift&amp;#43;ctrl&amp;#43;p&amp;#34;,&#xA;    &amp;#34;ctrl&amp;#43;n&amp;#34;&#xA;  ]&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;ctrl+n&lt;/code&gt; is also the default for &lt;code&gt;app.session.toggleNamedFilter&lt;/code&gt;, but that&#xA;only fires inside the session picker, not while a model selector or the main&#xA;editor has focus.&lt;/p&gt;&#xA;&lt;p&gt;Run &lt;code&gt;/reload&lt;/code&gt; in pi to pick up the keybinding addition without restarting the&#xA;session.&lt;/p&gt;&#xA;&lt;p&gt;∴ &lt;code&gt;ctrl+p&lt;/code&gt;/&lt;code&gt;ctrl+n&lt;/code&gt; now cycle models forward/backward, matching the readline&#xA;(emacs!) history bindings I already have muscle memory for.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;It cycles through models selected in &lt;code&gt;/scoped-models&lt;/code&gt;.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: cycle models backward with ctrl+n&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: caveman mode
      </title>
      <link>https://perrotta.dev/2026/07/pi-caveman-mode/</link>
      <pubDate>Wed, 22 Jul 2026 12:14:47 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-caveman-mode/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2025/12/pi/&#34;&gt;Previously&lt;/a&gt;,&#xA;&lt;a href=&#34;https://perrotta.dev/2026/06/caveman-talk-like-caveman-save-tokens/&#34;&gt;previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: adopt &lt;a href=&#34;https://github.com/JuliusBrussee/caveman&#34;&gt;caveman&lt;/a&gt;&#xA;in &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Pi isn&amp;rsquo;t one of the 30+ agents caveman auto-detects. The install matrix&#xA;(&lt;code&gt;INSTALL.md&lt;/code&gt;) lists agent detection rules one by one — &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;cursor&lt;/code&gt;,&#xA;&lt;code&gt;windsurf&lt;/code&gt;, &lt;code&gt;opencode&lt;/code&gt;, &lt;code&gt;openclaw&lt;/code&gt;, etc — and pi isn&amp;rsquo;t in it. Running the&#xA;one-liner installer here would just skip everything silently. Booo!&lt;/p&gt;&#xA;&lt;p&gt;Rather than run someone else&amp;rsquo;s &lt;code&gt;curl | bash&lt;/code&gt; against an agent it wasn&amp;rsquo;t built&#xA;for, &amp;ldquo;&amp;ldquo;&amp;ldquo;I&amp;rdquo;&amp;rdquo;&amp;rdquo; reimplemented the terse-speak part natively, since &lt;code&gt;pi&lt;/code&gt; already ships the&#xA;two primitives needed: &lt;a href=&#34;https://agentskills.io/specification&#34;&gt;skills&lt;/a&gt; and&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md&#34;&gt;extensions&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;A&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/7c495489141fd6036e4be70de9a065c53f7a8394/pi/.pi/agent/extensions/caveman.ts&#34;&gt;skill&lt;/a&gt;&#xA;for &lt;code&gt;/skill:caveman&lt;/code&gt; documentation, and an extension that rewrites the system&#xA;prompt on every turn via &lt;code&gt;pi.on(&amp;quot;before_agent_start&amp;quot;, (event) =&amp;gt; ...)&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const INSTRUCTIONS = `Caveman mode: on. Respond terse. Drop articles (a/an/the), filler (just/really/basically), pleasantries, hedging. Fragments OK. Short synonyms. Technical terms exact. Pattern: [thing] [action] [reason]. [next step]. Example — not: &amp;#34;Sure! I&amp;#39;d be happy to help you with that.&amp;#34; yes: &amp;#34;Bug in auth middleware. Fix:&amp;#34;&#xA;Boundaries, always: code blocks, commit messages, and PR descriptions stay normal full verbosity, never compressed. Drop caveman style for security warnings, irreversible/destructive actions, or when the user seems confused — resume caveman after.`;&#xA;&#xA;export default function (pi: ExtensionAPI) {&#xA;  let on = true; // auto-on by default, every session&#xA;&#xA;  const applyStatus = (ctx: { ui: { setStatus: (k: string, v?: string) =&amp;gt; void } }) =&amp;gt; {&#xA;    ctx.ui.setStatus(&amp;#34;caveman&amp;#34;, on ? &amp;#34;[caveman]&amp;#34; : undefined);&#xA;  };&#xA;&#xA;  pi.on(&amp;#34;session_start&amp;#34;, async (_event, ctx) =&amp;gt; applyStatus(ctx));&#xA;&#xA;  pi.on(&amp;#34;before_agent_start&amp;#34;, (event) =&amp;gt; {&#xA;    if (!on) return;&#xA;    return { systemPrompt: `${event.systemPrompt}\n\n${INSTRUCTIONS}` };&#xA;  });&#xA;&#xA;  pi.registerCommand(&amp;#34;caveman&amp;#34;, {&#xA;    description: &amp;#34;Toggle caveman terse-response mode: on|off (default: on)&amp;#34;,&#xA;    handler: async (args, ctx) =&amp;gt; {&#xA;      const arg = args.trim().toLowerCase();&#xA;      if (arg === &amp;#34;off&amp;#34; || arg === &amp;#34;stop&amp;#34; || arg === &amp;#34;normal&amp;#34;) on = false;&#xA;      else if (arg === &amp;#34;on&amp;#34; || arg === &amp;#34;&amp;#34;) on = true;&#xA;      else return ctx.ui.notify(`Unknown arg &amp;#34;${arg}&amp;#34;. Use: on | off`, &amp;#34;error&amp;#34;);&#xA;      applyStatus(ctx);&#xA;      ctx.ui.notify(on ? &amp;#34;Caveman mode on.&amp;#34; : &amp;#34;Caveman mode off.&amp;#34;, &amp;#34;info&amp;#34;);&#xA;    },&#xA;  });&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#before_agent_start&#34;&gt;&lt;code&gt;before_agent_start&lt;/code&gt;&lt;/a&gt;&#xA;fires on &lt;strong&gt;every prompt&lt;/strong&gt;, before the agent loop starts, so&#xA;the instructions are re-appended each turn instead of relying on one message&#xA;that could get pruned from context. Dropped in&#xA;&lt;code&gt;~/.pi/agent/extensions/caveman.ts&lt;/code&gt;, auto-discovered, no &lt;code&gt;/reload&lt;/code&gt; needed after&#xA;the first load.&lt;/p&gt;&#xA;&lt;p&gt;Unlike the Claude Code plugin, I skipped &lt;code&gt;lite&lt;/code&gt;/&lt;code&gt;full&lt;/code&gt;/&lt;code&gt;ultra&lt;/code&gt;/&lt;code&gt;wenyan&lt;/code&gt; — I&#xA;called that bloat back in June, and I&amp;rsquo;m not adding it to my own version&#xA;either. On or off is enough.&lt;/p&gt;&#xA;&lt;p&gt;The result, &lt;em&gt;unprompted&lt;/em&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;user&lt;/strong&gt;: caveman on or off now?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;assistant&lt;/strong&gt;: Caveman: on.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;user&lt;/strong&gt;: how to turn it off?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;assistant&lt;/strong&gt;: &lt;code&gt;/caveman off&lt;/code&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I also skipped the parts that don&amp;rsquo;t have a pi equivalent: the statusline&#xA;savings counter, &lt;code&gt;/caveman-stats&lt;/code&gt;, the &lt;code&gt;caveman-shrink&lt;/code&gt; MCP proxy that rewrites&#xA;tool descriptions (a proxy that rewrites what an agent sees is exactly the&#xA;kind of thing worth reading before installing). The 40-line extension above&#xA;covers what I actually wanted.&lt;/p&gt;&#xA;&lt;p&gt;One rule baked into both the skill and the extension: code, commits, and PRs&#xA;stay full verbosity, never compressed.&lt;/p&gt;&#xA;&lt;p&gt;This experience was an eye-opener for me to realize how bloated the upstream&#xA;caveman project became.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: caveman mode&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ logseq → obsidian
      </title>
      <link>https://perrotta.dev/2026/07/logseq-obsidian/</link>
      <pubDate>Mon, 20 Jul 2026 18:26:33 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>dev</category>
      <category>pkm</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/07/logseq-obsidian/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/brew-pin/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: LogSeq v2 is moving its graph format from markdown files&#xA;to a sqlite database:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://discuss.logseq.com/t/whats-new-with-logseq-db-may-16th-2026/35020&#34;&gt;What&amp;rsquo;s New with Logseq DB — May 16th 2026&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://logseq.io/p/e3YDyX5AYr&#34;&gt;Big update: Logseq is splitting into two versions&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;This is &lt;em&gt;not OK&lt;/em&gt; for my workflow.&lt;/p&gt;&#xA;&lt;p&gt;The whole point of adopting LogSeq was, just like&#xA;&lt;a href=&#34;https://obsidian.md/&#34;&gt;Obsidian&lt;/a&gt;, the promise of&#xA;managing my notes in &lt;a href=&#34;https://obsidian.md/about&#34;&gt;plain markdown files&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Even though this migration is supposed to improve the performance of the app, it&#xA;ends up breaking a fundamental reason why I decided to use it in the first&#xA;place.&lt;/p&gt;&#xA;&lt;p&gt;Furthermore, the Second Brain app of my choice is supposed to be boring. It is&#xA;a big deal to wake up one day and find that I can no longer open my notes&#xA;graph with the new version of the app. This is a no-go.&lt;/p&gt;&#xA;&lt;p&gt;Well, classic LogSeq (dubbed &amp;ldquo;Logseq OG&amp;rdquo;) will be &lt;a href=&#34;https://github.com/logseq/og&#34;&gt;kept&#xA;around&lt;/a&gt;, but I can only assume that from this&#xA;point on it will become poorly maintained — it will never get the same amount of&#xA;resources and attention as the new version.&lt;/p&gt;&#xA;&lt;p&gt;Also, even if I really wanted to use the new version, it turns out it does not&#xA;work on mobile (iOS), which is a dealbreaker, therefore I would be forced to&#xA;stay stuck with OG for a while. Heck, the iOS app hasn&amp;rsquo;t been updated in 2y&#xA;anyway, and it shows.&lt;/p&gt;&#xA;&lt;p&gt;There will be no hard feelings, this is a peaceful breakup; I enjoyed LogSeq,&#xA;it&amp;rsquo;s simpler and more streamlined than Obsidian; as such, I will keep&#xA;recommending it to most people.&lt;/p&gt;&#xA;&lt;p&gt;That said, I&amp;rsquo;ve always been pondering whether to switch back to Obsidian.&#xA;There&amp;rsquo;s no doubt that Obsidian is more actively maintained and better suited for&#xA;Gen AI / LLM workflows these days (see &lt;a href=&#34;https://obsidian.md/cli&#34;&gt;Obsidian CLI&lt;/a&gt;&#xA;for starters).&lt;/p&gt;&#xA;&lt;p&gt;Why did I switch to LogSeq from Obsidian in the first place? Because Obsidian&#xA;was too much for me. It was more complicated (in terms of its feature set) than&#xA;warranted for my purposes. It offered more than I needed. That&amp;rsquo;s not necessarily&#xA;a bad thing. It&amp;rsquo;s like saying (hypothetically) &amp;ldquo;I prefer &lt;code&gt;nano&lt;/code&gt; instead of&#xA;&lt;code&gt;vim&lt;/code&gt;&amp;rdquo;, or that I prefer to live in a studio apartment than in a 3-bedroom&#xA;house.&lt;/p&gt;&#xA;&lt;p&gt;Obsidian won&amp;rsquo;t let me down in terms of changing its file format: &lt;a href=&#34;https://stephango.com/file-over-app&#34;&gt;file over&#xA;app&lt;/a&gt; is one of their core&#xA;&lt;a href=&#34;https://stephango.com/self-guarantee&#34;&gt;promises&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;File over app is a philosophy: if you want to create digital artifacts that&#xA;last, they must be files you can control, in formats that are easy to retrieve&#xA;and read. Use tools that give you this freedom.&lt;/p&gt;&#xA;&lt;p&gt;File over app is an appeal to tool makers: accept that all software is&#xA;ephemeral, and give people ownership over their data.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;This is very important. My Second Brain app must not lock me in. LogSeq, upon&#xA;the switch to a sqlite database, breaks this invariant. I want — I need — boring&#xA;markdown files. Do not break the beauty and simplicity of this setup.&lt;/p&gt;&#xA;&lt;h2 id=&#34;migration&#34;&gt;&#xA;  Migration&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/07/logseq-obsidian/#migration&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;LogSeq and Obsidian are similar but not identical.&lt;/p&gt;&#xA;&lt;p&gt;In order to bridge the gap, a small python script (drafted by Claude Fable 5,&#xA;reviewed by me) walked the existing LogSeq Graph and wrote to a fresh Obsidian&#xA;Vault, leaving the source untouched, should I decide to roll back.&lt;/p&gt;&#xA;&lt;p&gt;These are some mappings (LogSeq → Obsidian):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;journals/2024_03_04.md         -&amp;gt;  journals/2024-03-04.md&#xA;pages/Foo___Bar.md             -&amp;gt;  Foo/Bar.md          (namespaces)&#xA;- TODO buy milk                -&amp;gt;  - [ ] buy milk&#xA;- DOING ...                    -&amp;gt;  - [/] ...&#xA;- DONE ...                     -&amp;gt;  - [x] ...&#xA;[[Feb 9th, 2026]]              -&amp;gt;  [[2026-02-09]]&#xA;((67ea7dba-3200-...))          -&amp;gt;  [[2025-03-31#^67ea7dba]]&#xA;{{embed ((67ea7dba-3200-...))}} -&amp;gt; ![[2025-03-31#^67ea7dba]]&#xA;{{video https://...}}          -&amp;gt;  plain URL&#xA;id:: 67ea7dba-3200-...         -&amp;gt;  ^67ea7dba block anchor (when referenced)&#xA;collapsed:: true, :LOGBOOK:    -&amp;gt;  dropped&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Plus one line of config so that the LogSeq journals become Obsidian daily notes:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;    &amp;#34;folder&amp;#34;: &amp;#34;journals&amp;#34;,&#xA;    &amp;#34;format&amp;#34;: &amp;#34;YYYY-MM-DD&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&amp;hellip;and then I spent some time trying to dumb down Obsidian, disabling core&#xA;plug-ins I do not need at this time. There&amp;rsquo;s no need for community plug-ins for&#xA;now (if ever).&lt;/p&gt;&#xA;&lt;p&gt;So far, I only migrated my corp (work) notes. I&amp;rsquo;ll need to migrate my personal&#xA;PKM as well, but I am happy to simply pilot this migration at work first, so&#xA;that I don&amp;rsquo;t get to regret twice.&lt;/p&gt;&#xA;&lt;p&gt;The migration script:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;#!/usr/bin/env python3&#xA;&amp;#34;&amp;#34;&amp;#34;Migrate LogSeq graph -&amp;gt; Obsidian vault. Source is never modified.&amp;#34;&amp;#34;&amp;#34;&#xA;import re&#xA;import shutil&#xA;from pathlib import Path&#xA;&#xA;SRC = Path(&amp;#34;/Users/thiago.perrotta/Library/Mobile Documents/iCloud~com~logseq~logseq/{redacted}&amp;#34;)&#xA;DST = Path(&amp;#34;/Users/thiago.perrotta/Library/Mobile Documents/iCloud~md~obsidian/{redacted}&amp;#34;)&#xA;&#xA;UUID = r&amp;#34;[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}&amp;#34;&#xA;MONTHS = {m: i &amp;#43; 1 for i, m in enumerate(&#xA;    [&amp;#34;Jan&amp;#34;, &amp;#34;Feb&amp;#34;, &amp;#34;Mar&amp;#34;, &amp;#34;Apr&amp;#34;, &amp;#34;May&amp;#34;, &amp;#34;Jun&amp;#34;, &amp;#34;Jul&amp;#34;, &amp;#34;Aug&amp;#34;, &amp;#34;Sep&amp;#34;, &amp;#34;Oct&amp;#34;, &amp;#34;Nov&amp;#34;, &amp;#34;Dec&amp;#34;])}&#xA;&#xA;MARKER_MAP = {&#xA;    &amp;#34;TODO&amp;#34;: &amp;#34;[ ]&amp;#34;, &amp;#34;LATER&amp;#34;: &amp;#34;[ ]&amp;#34;, &amp;#34;WAITING&amp;#34;: &amp;#34;[ ]&amp;#34;,&#xA;    &amp;#34;DOING&amp;#34;: &amp;#34;[/]&amp;#34;, &amp;#34;NOW&amp;#34;: &amp;#34;[/]&amp;#34;, &amp;#34;IN-PROGRESS&amp;#34;: &amp;#34;[/]&amp;#34;,&#xA;    &amp;#34;DONE&amp;#34;: &amp;#34;[x]&amp;#34;,&#xA;    &amp;#34;CANCELED&amp;#34;: &amp;#34;[-]&amp;#34;, &amp;#34;CANCELLED&amp;#34;: &amp;#34;[-]&amp;#34;,&#xA;}&#xA;&#xA;&#xA;def journal_name(stem: str) -&amp;gt; str:&#xA;    return stem.replace(&amp;#34;_&amp;#34;, &amp;#34;-&amp;#34;)&#xA;&#xA;&#xA;def dest_rel(src_file: Path) -&amp;gt; Path:&#xA;    &amp;#34;&amp;#34;&amp;#34;Map a source md file to its vault-relative destination path.&amp;#34;&amp;#34;&amp;#34;&#xA;    if src_file.parent.name == &amp;#34;journals&amp;#34;:&#xA;        return Path(&amp;#34;journals&amp;#34;) / (journal_name(src_file.stem) &amp;#43; &amp;#34;.md&amp;#34;)&#xA;    # namespace pages: A___B.md -&amp;gt; A/B.md; everything else under pages/&#xA;    parts = src_file.stem.split(&amp;#34;___&amp;#34;)&#xA;    return Path(*parts[:-1]) / (parts[-1] &amp;#43; &amp;#34;.md&amp;#34;) if len(parts) &amp;gt; 1 else Path(&amp;#34;pages&amp;#34;) / src_file.name&#xA;&#xA;&#xA;def page_ref(src_file: Path) -&amp;gt; str:&#xA;    &amp;#34;&amp;#34;&amp;#34;Wiki-link target for a source file (what goes inside [[...]]).&amp;#34;&amp;#34;&amp;#34;&#xA;    if src_file.parent.name == &amp;#34;journals&amp;#34;:&#xA;        return journal_name(src_file.stem)&#xA;    return &amp;#34;/&amp;#34;.join(src_file.stem.split(&amp;#34;___&amp;#34;))&#xA;&#xA;&#xA;def collect_ids():&#xA;    &amp;#34;&amp;#34;&amp;#34;uuid -&amp;gt; (link target, anchor) for every id:: definition; also which uuids are referenced.&amp;#34;&amp;#34;&amp;#34;&#xA;    defined, referenced = {}, set()&#xA;    for f in sorted(SRC.glob(&amp;#34;pages/*.md&amp;#34;)) &amp;#43; sorted(SRC.glob(&amp;#34;journals/*.md&amp;#34;)):&#xA;        text = f.read_text(encoding=&amp;#34;utf-8&amp;#34;)&#xA;        for m in re.finditer(rf&amp;#34;^\s*id:: ({UUID})\s*$&amp;#34;, text, re.M):&#xA;            defined[m.group(1)] = (page_ref(f), m.group(1)[:8])&#xA;        for m in re.finditer(rf&amp;#34;\(\(({UUID})\)\)&amp;#34;, text):&#xA;            referenced.add(m.group(1))&#xA;    return defined, referenced&#xA;&#xA;&#xA;def convert(text: str, defined, referenced) -&amp;gt; str:&#xA;    lines = text.split(&amp;#34;\n&amp;#34;)&#xA;    out = []&#xA;    in_fence = False&#xA;    in_logbook = False&#xA;&#xA;    def resolve_ref(m):&#xA;        uuid = m.group(1)&#xA;        if uuid in defined:&#xA;            target, anchor = defined[uuid]&#xA;            return f&amp;#34;[[{target}#^{anchor}]]&amp;#34;&#xA;        return m.group(0)&#xA;&#xA;    def resolve_embed(m):&#xA;        uuid = m.group(1)&#xA;        if uuid in defined:&#xA;            target, anchor = defined[uuid]&#xA;            return f&amp;#34;![[{target}#^{anchor}]]&amp;#34;&#xA;        return m.group(0)&#xA;&#xA;    def date_link(m):&#xA;        mon, day, year = m.group(1), int(m.group(2)), int(m.group(4))&#xA;        return f&amp;#34;[[{year:04d}-{MONTHS[mon]:02d}-{day:02d}]]&amp;#34;&#xA;&#xA;    pending_merge_indent = None  # indent of an empty bullet awaiting its continuation line&#xA;&#xA;    for line in lines:&#xA;        stripped = line.strip()&#xA;&#xA;        # merge continuation text into a bullet whose first line was a property&#xA;        if pending_merge_indent is not None:&#xA;            m = re.match(rf&amp;#34;^{re.escape(pending_merge_indent)}\s\s?(\S.*)$&amp;#34;, line)&#xA;            pending_merge_indent = None&#xA;            if m and out:&#xA;                out[-1] = out[-1] &amp;#43; m.group(1)&#xA;                continue&#xA;&#xA;        # LogSeq opens fences on bullet lines (&amp;#34;- ```lang&amp;#34;), closes with bare &amp;#34;```&amp;#34;&#xA;        if re.match(r&amp;#34;^\s*(?:[-*&amp;#43;]\s&amp;#43;)?```&amp;#34;, line):&#xA;            in_fence = not in_fence&#xA;            out.append(line)&#xA;            continue&#xA;        if in_fence:&#xA;            out.append(line)&#xA;            continue&#xA;&#xA;        # LOGBOOK drawers&#xA;        if stripped == &amp;#34;:LOGBOOK:&amp;#34;:&#xA;            in_logbook = True&#xA;            continue&#xA;        if in_logbook:&#xA;            if stripped == &amp;#34;:END:&amp;#34;:&#xA;                in_logbook = False&#xA;            continue&#xA;&#xA;        # bullet-form property lines: block whose first line is the property itself&#xA;        # (&amp;#34;- collapsed:: true&amp;#34; with real content on the continuation line below)&#xA;        m = re.match(rf&amp;#34;^(\s*)- (collapsed:: (?:true|false)|logseq\.order-list-type:: number|id:: ({UUID}))\s*$&amp;#34;, line)&#xA;        if m:&#xA;            indent = m.group(1)&#xA;            uuid = m.group(3)&#xA;            if uuid and uuid in referenced:&#xA;                out.append(f&amp;#34;{indent}- ^{uuid[:8]}&amp;#34;)&#xA;            else:&#xA;                out.append(f&amp;#34;{indent}- &amp;#34;)&#xA;                pending_merge_indent = indent&#xA;            continue&#xA;&#xA;        # property lines&#xA;        m = re.match(rf&amp;#34;^\s*id:: ({UUID})\s*$&amp;#34;, line)&#xA;        if m:&#xA;            uuid = m.group(1)&#xA;            if uuid in referenced and out:&#xA;                # anchor goes at end of the block&amp;#39;s first line&#xA;                for i in range(len(out) - 1, -1, -1):&#xA;                    if out[i].strip():&#xA;                        out[i] = out[i].rstrip() &amp;#43; f&amp;#34; ^{uuid[:8]}&amp;#34;&#xA;                        break&#xA;            continue&#xA;        if re.match(r&amp;#34;^\s*collapsed:: (true|false)\s*$&amp;#34;, line):&#xA;            continue&#xA;        if re.match(r&amp;#34;^\s*logseq\.order-list-type:: number\s*$&amp;#34;, line):&#xA;            # convert the previous bullet line to a numbered item; number it by&#xA;            # walking back past deeper lines to the previous same-indent sibling&#xA;            for i in range(len(out) - 1, -1, -1):&#xA;                bm = re.match(r&amp;#34;^(\s*)- (.*)$&amp;#34;, out[i])&#xA;                if bm:&#xA;                    indent = bm.group(1)&#xA;                    n = 1&#xA;                    for j in range(i - 1, -1, -1):&#xA;                        if not out[j].strip():&#xA;                            continue&#xA;                        lead = out[j][: len(out[j]) - len(out[j].lstrip())]&#xA;                        if len(lead) &amp;gt; len(indent):&#xA;                            continue  # child or continuation line&#xA;                        if lead == indent:&#xA;                            nm = re.match(rf&amp;#34;^{re.escape(indent)}(\d&amp;#43;)\. &amp;#34;, out[j])&#xA;                            n = int(nm.group(1)) &amp;#43; 1 if nm else 1&#xA;                        break&#xA;                    out[i] = f&amp;#34;{indent}{n}. {bm.group(2)}&amp;#34;&#xA;                    break&#xA;            continue&#xA;&#xA;        # task markers on bullet lines (not headings)&#xA;        line = re.sub(&#xA;            r&amp;#34;^(\s*)- (TODO|LATER|WAITING|DOING|NOW|IN-PROGRESS|DONE|CANCELED|CANCELLED)\s&amp;#43;&amp;#34;,&#xA;            lambda m: f&amp;#34;{m.group(1)}- {MARKER_MAP[m.group(2)]} &amp;#34;,&#xA;            line,&#xA;        )&#xA;&#xA;        # drop LogSeq priority markers&#xA;        line = re.sub(r&amp;#34;\[#[A-C]\]\s*&amp;#34;, &amp;#34;&amp;#34;, line)&#xA;&#xA;        # embeds and block refs&#xA;        line = re.sub(rf&amp;#34;\{{\{{embed \(\(({UUID})\)\)\}}\}}&amp;#34;, resolve_embed, line)&#xA;        line = re.sub(r&amp;#34;\{\{embed \[\[([^]]&amp;#43;)\]\]\}\}&amp;#34;, r&amp;#34;![[\1]]&amp;#34;, line)&#xA;        line = re.sub(rf&amp;#34;\(\(({UUID})\)\)&amp;#34;, resolve_ref, line)&#xA;        line = re.sub(r&amp;#34;\{\{video ([^}]&amp;#43;)\}\}&amp;#34;, r&amp;#34;\1&amp;#34;, line)&#xA;&#xA;        # date links: [[Feb 9th, 2026]] -&amp;gt; [[2026-02-09]]&#xA;        line = re.sub(&#xA;            r&amp;#34;\[\[(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{1,2})(st|nd|rd|th), (\d{4})\]\]&amp;#34;,&#xA;            date_link, line)&#xA;&#xA;        # asset paths&#xA;        line = line.replace(&amp;#34;](../assets/&amp;#34;, &amp;#34;](assets/&amp;#34;)&#xA;&#xA;        out.append(line)&#xA;&#xA;    result = &amp;#34;\n&amp;#34;.join(out)&#xA;    if result.strip() == &amp;#34;-&amp;#34;:  # LogSeq empty-page placeholder&#xA;        result = &amp;#34;&amp;#34;&#xA;    return result&#xA;&#xA;&#xA;def main():&#xA;    defined, referenced = collect_ids()&#xA;    orphans = referenced - set(defined)&#xA;    if orphans:&#xA;        print(f&amp;#34;WARNING: refs without target: {orphans}&amp;#34;)&#xA;&#xA;    written = 0&#xA;    for f in sorted(SRC.glob(&amp;#34;pages/*.md&amp;#34;)) &amp;#43; sorted(SRC.glob(&amp;#34;journals/*.md&amp;#34;)):&#xA;        rel = dest_rel(f)&#xA;        dst = DST / rel&#xA;        dst.parent.mkdir(parents=True, exist_ok=True)&#xA;        dst.write_text(convert(f.read_text(encoding=&amp;#34;utf-8&amp;#34;), defined, referenced),&#xA;                       encoding=&amp;#34;utf-8&amp;#34;)&#xA;        written &amp;#43;= 1&#xA;&#xA;    assets_dst = DST / &amp;#34;assets&amp;#34;&#xA;    assets_dst.mkdir(exist_ok=True)&#xA;    copied = 0&#xA;    for a in SRC.glob(&amp;#34;assets/*&amp;#34;):&#xA;        if a.name == &amp;#34;.DS_Store&amp;#34;:&#xA;            continue&#xA;        shutil.copy2(a, assets_dst / a.name)&#xA;        copied &amp;#43;= 1&#xA;&#xA;    print(f&amp;#34;wrote {written} notes, copied {copied} assets&amp;#34;)&#xA;&#xA;&#xA;if __name__ == &amp;#34;__main__&amp;#34;:&#xA;    main()&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: logseq → obsidian&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bestof/&#34;&gt;#bestof&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Sports.app
      </title>
      <link>https://perrotta.dev/2026/07/sports.app/</link>
      <pubDate>Mon, 20 Jul 2026 11:35:19 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/07/sports.app/</guid>
      <description>&lt;p&gt;♠ The recently released &lt;a href=&#34;https://apps.apple.com/us/app/apple-sports/id6446788829&#34;&gt;Apple&#xA;Sports&lt;/a&gt;&#xA;&lt;a href=&#34;https://support.apple.com/guide/apple-sports-app/welcome/web&#34;&gt;app&lt;/a&gt; was surely a&#xA;great way to keep up with the World Cup on the go.&lt;/p&gt;&#xA;&lt;p&gt;It is very polished; it does one thing, and does it well. Simple yet effective.&#xA;I do recommend it.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s especially handy when you&amp;rsquo;re not able to watch a match live for whatever&#xA;reason.&lt;/p&gt;&#xA;&lt;p&gt;If you&amp;rsquo;re able to watch a match in real time then it&amp;rsquo;s better to leave your&#xA;phone in airplane mode, otherwise chances are you&amp;rsquo;ll get a goal notification&#xA;from the app before you get to see it on the TV due to broadcasting delays.&#xA;Wild.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Sports.app&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>brew pin
      </title>
      <link>https://perrotta.dev/2026/07/brew-pin/</link>
      <pubDate>Sat, 18 Jul 2026 14:53:04 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>macos</category>
      <category>pkm</category>
      <guid>https://perrotta.dev/2026/07/brew-pin/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;TIL&lt;/strong&gt;: &lt;a href=&#34;https://docs.brew.sh/FAQ#:~:text=brew%20upgrade%20%3Cformula%3E-,How%20do%20I%20stop%20certain%20formulae%20from%20being%20updated%3F,-To%20stop%20something&#34;&gt;&lt;code&gt;brew pin&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;For reasons to be elaborated in another post, I am currently prevented from&#xA;upgrading &lt;a href=&#34;https://logseq.com/&#34;&gt;LogSeq&lt;/a&gt; to its latest version in homebrew:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew outdated&#xA;[...]&#xA;logseq (0.10.14) != 2.0.1&#xA;[...]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://news.ycombinator.com/item?id=48896229&#34;&gt;LogSeq v2&lt;/a&gt; introduces breaking&#xA;changes to my workflow. I found this out the hard way. As put by &lt;code&gt;Valodim&lt;/code&gt; in HN:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;And now after several years of complete stagnation, the supposed improvement&#xA;is a database format to fix their technical issues, so I can no longer keep&#xA;all my data as markdown files? At a time when half the edits are done by&#xA;Claude and tracked with jujustu, this is just not useful for me.&lt;/p&gt;&#xA;&lt;p&gt;All I wanted was the original vision, but with less bugs and more quality of&#xA;life features.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;As such, I would like to settle in the current version for now. Pinning to the&#xA;rescue!&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew pin logseq&#xA;Warning: logseq has `auto_updates true` and may update itself outside Homebrew despite being pinned.&#xA;&#xA;% brew list --pinned&#xA;logseq&#xA;&#xA;% brew outdated&#xA;[...]&#xA;logseq (0.10.14) != 2.0.1 [pinned at 0.10.14]&#xA;[...]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That&amp;rsquo;s all.&lt;/p&gt;&#xA;&lt;p&gt;To unpin it in the future:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew unpin logseq&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: brew pin&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ The lifecycle of physical books
      </title>
      <link>https://perrotta.dev/2026/07/the-lifecycle-of-physical-books/</link>
      <pubDate>Sat, 18 Jul 2026 14:21:02 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bestof</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/07/the-lifecycle-of-physical-books/</guid>
      <description>&lt;p&gt;♠ Typically:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: buy a physical book — via Amazon, bookstores, classifieds&#xA;marketplaces (e.g. Craigslist in the US, Kijiji in Canada, Kleinanzeigen in&#xA;Germany). Bonus points for used copies. This should take up to a week.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: &lt;strong&gt;read&lt;/strong&gt; the &lt;del&gt;goddamn&lt;/del&gt; book. This is arguably the hardest part,&#xA;albeit the most exciting one. As &lt;a href=&#34;https://frantic.im/80-20/&#34;&gt;Alex Kotliarskyi&lt;/a&gt;&#xA;puts it (emphasis mine):&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The 80/20 rule says that 80% value come from 20% effort. What if we apply the&#xA;80/20 rule to the first 80%?&lt;/p&gt;&#xA;&lt;p&gt;So 80% of 80% of value come from 20% of 20% of effort. Or 64% of the value&#xA;come from 4% of the effort. &lt;strong&gt;Apply it again and you&amp;rsquo;ll get ~51% value from&#xA;~1% effort&lt;/strong&gt;. Which is ballpark close to &amp;ldquo;Dimidium facti, qui coepit, habet&amp;rdquo; —&#xA;Well begun is half done.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Once you start, it becomes &lt;em&gt;much easier&lt;/em&gt; to go through it, I promise.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Step 3&lt;/strong&gt;: &lt;strong&gt;Find a way to get rid of the book&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;There&amp;rsquo;s no shame. Have &lt;a href=&#34;https://konmari.com/marie-kondo-gratitude/&#34;&gt;gratitude&lt;/a&gt;,&#xA;appreciate it, and then simply move on. I don&amp;rsquo;t like to stick with physical&#xA;books forever. They &lt;em&gt;unnecessarily&lt;/em&gt; occupy space in my home.&lt;/p&gt;&#xA;&lt;p&gt;There are various ways to do so:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Donating is the easiest. Leave books by the vestibule of your apartment&#xA;building complex with a post-it note stating that it&amp;rsquo;s free&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/the-lifecycle-of-physical-books/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Leave books in public &lt;a href=&#34;https://www.muenchen.de/sehenswuerdigkeiten/oeffentliche-buecherschraenke-muenchen-unsere-tipps&#34;&gt;community&#xA;bookshelves&lt;/a&gt;&#xA;or libraries.&lt;/li&gt;&#xA;&lt;li&gt;Gift a book to a friend. Bonus points for being thoughtful.&lt;/li&gt;&#xA;&lt;li&gt;Sell a book in a classified site. Bonus points for using the same one you got&#xA;it from.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;And now, adding the missing alternative to my &lt;em&gt;repertoire&lt;/em&gt;:&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://www.momox.de/&#34;&gt;momox&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Schnell &amp;amp; einfach Gebrauchtes verkaufen&#xA;Was möchtest du verkaufen?&#xA;Bücher &amp;amp; Medien&#xA;Kleidung&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Momox &lt;a href=&#34;https://en.wikipedia.org/wiki/Momox&#34;&gt;is&lt;/a&gt;&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;a Berlin-based recommerce site for used books and media founded by Christian&#xA;Wegner in 2004.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;In plain English: it&amp;rsquo;s like selling a book in a classified, but without having&#xA;to wait for someone to actually message you&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/the-lifecycle-of-physical-books/#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Scan or input its ISBN, get an estimate of how much you&amp;rsquo;ll get for it (assuming&#xA;it&amp;rsquo;s in good condition), arrange a box, pack the book, ship it &lt;em&gt;for free&lt;/em&gt;, wait,&#xA;and then get paid. It&amp;rsquo;s very streamlined and easy to use.&lt;/p&gt;&#xA;&lt;p&gt;So far, I&amp;rsquo;ve only heard of this concept in Germany. I would love to find out if&#xA;it&amp;rsquo;s possible to follow suit in other countries.&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;&lt;em&gt;Zu verschenken&lt;/em&gt;, as they say it &lt;em&gt;auf Deutsch&lt;/em&gt;. In plain English, &amp;ldquo;for&#xA;free&amp;rdquo; suffices.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/the-lifecycle-of-physical-books/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;And eventually lowball or flake on you.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/the-lifecycle-of-physical-books/#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: The lifecycle of physical books&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bestof/&#34;&gt;#bestof&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ Project Aces OST
      </title>
      <link>https://perrotta.dev/2026/07/project-aces-ost/</link>
      <pubDate>Fri, 17 Jul 2026 12:52:54 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bestof</category>
      <category>gaming</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/07/project-aces-ost/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://acecombat.fandom.com/wiki/Project_Aces&#34;&gt;Project Aces&lt;/a&gt; (from the &lt;a href=&#34;https://en.wikipedia.org/wiki/Ace_Combat&#34;&gt;Ace&#xA;Combat&lt;/a&gt; series) has one of the best&#xA;videogame &lt;a href=&#34;https://en.wikipedia.org/wiki/Soundtrack&#34;&gt;OSTs&lt;/a&gt; ever.&lt;/p&gt;&#xA;&lt;p&gt;I am&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/project-aces-ost/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; finally pleased&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/project-aces-ost/#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt; to find their whole collection available on&#xA;&lt;a href=&#34;https://open.spotify.com/artist/1dhKyn9jcCoyrxGq4tI7Vc&#34;&gt;Spotify&lt;/a&gt; as of 2026.&#xA;Previously, only a handful of them were available.&lt;/p&gt;&#xA;&lt;p&gt;No more fiddling random playlists on &lt;a href=&#34;https://music.youtube.com/&#34;&gt;YouTube&#xA;Music&lt;/a&gt;!&lt;/p&gt;&#xA;&lt;p&gt;See also: &lt;a href=&#34;https://perrotta.dev/radio/&#34;&gt;radio&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://old.reddit.com/r/acecombat/comments/1amjb61/so_project_aces_just_dropped_the_entire/&#34;&gt;I&amp;rsquo;m not the only&#xA;one&lt;/a&gt;.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/project-aces-ost/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;I am also &lt;strong&gt;very&lt;/strong&gt; pleased about the upcoming release of &lt;a href=&#34;https://store.steampowered.com/app/2288340/ACE_COMBAT_8_WINGS_OF_THEVE/&#34;&gt;Ace Combat 8:&#xA;Wings of&#xA;Theve&lt;/a&gt;&#xA;(Oct 2nd, 2026). I wasn&amp;rsquo;t aware of it until now! Too bad I don&amp;rsquo;t currently&#xA;have any hardware to play it, and &lt;a href=&#34;https://daringfireball.net/linked/2026/06/27/microsoft-raises-xbox-prices&#34;&gt;console prices keep&#xA;rising&lt;/a&gt;.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/project-aces-ost/#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Project Aces OST&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bestof/&#34;&gt;#bestof&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/gaming/&#34;&gt;#gaming&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>myrepos: a wip action to find unfinished work
      </title>
      <link>https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/</link>
      <pubDate>Fri, 17 Jul 2026 12:23:10 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/03/mr-update/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: I manage a few dozen repos via&#xA;&lt;a href=&#34;https://myrepos.branchable.com/&#34;&gt;myrepos&lt;/a&gt; (&lt;code&gt;mr&lt;/code&gt;), and I wanted a single command&#xA;to show which ones have work in progress (&amp;ldquo;WIP&amp;rdquo;) — uncommitted changes, dirty&#xA;staging area, stray branches and alike.&lt;/p&gt;&#xA;&lt;p&gt;I want essentially a filtered version of &lt;code&gt;mr ls&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;So I defined my own command. A repo is &lt;em&gt;not&lt;/em&gt; WIP when all three hold:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;the default branch is checked out;&lt;/li&gt;&#xA;&lt;li&gt;no other local branches exist;&lt;/li&gt;&#xA;&lt;li&gt;and the working tree is fully clean.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Anything else gets flagged with a reason.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/28376040a292a9898cca59f8ce27de81482d1a26/mr/.mrconfig.defaults#L14-L23&#34;&gt;The definition&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;# List repos with work in progress: not on default branch, extra local branches, or dirty tree.&#xA;# Run as `mr -m wip`: the -m (minimal) flag drops mr&amp;#39;s per-repo &amp;#34;mr wip:&amp;#34; header and blank&#xA;# lines, leaving only the flagged repos. This cannot be defaulted per-action (mr only honors&#xA;# -m/-q as CLI flags, not via config).&#xA;wip =&#xA; reasons=&amp;#34;&amp;#34;&#xA; cur=$(git symbolic-ref --quiet --short HEAD 2&amp;gt;/dev/null || echo &amp;#34;(detached)&amp;#34;)&#xA; def=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2&amp;gt;/dev/null | sed &amp;#39;s|^origin/||&amp;#39;)&#xA; if [ -z &amp;#34;$def&amp;#34; ]; then if git show-ref --verify --quiet refs/heads/main; then def=main; elif git show-ref --verify --quiet refs/heads/master; then def=master; fi; fi&#xA; if [ -n &amp;#34;$def&amp;#34; ] &amp;amp;&amp;amp; [ &amp;#34;$cur&amp;#34; != &amp;#34;$def&amp;#34; ]; then reasons=&amp;#34;$reasons branch:$cur&amp;#34;; fi&#xA; n=$(git for-each-ref --format=&amp;#39;%(refname)&amp;#39; refs/heads | wc -l | tr -d &amp;#39; &amp;#39;)&#xA; if [ &amp;#34;$n&amp;#34; -gt 1 ]; then reasons=&amp;#34;$reasons &amp;#43;$((n-1))_branches&amp;#34;; fi&#xA; if [ -n &amp;#34;$(git status --porcelain)&amp;#34; ]; then reasons=&amp;#34;$reasons dirty&amp;#34;; fi&#xA; if [ -n &amp;#34;$reasons&amp;#34; ]; then printf &amp;#39;%s [%s]\n&amp;#39; &amp;#34;$MR_REPO&amp;#34; &amp;#34;${reasons# }&amp;#34;; fi&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Naturally, the snippet above is art from the LLM, but it accomplishes exactly&#xA;what I want:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;thiago.perrotta ~&#xA;% mr -m wip&#xA;mr wip: /Users/thiago.perrotta/Corp/gitops&#xA;/Users/thiago.perrotta/Corp/gitops [branch:thiagowfx/acme &amp;#43;1_branches]&#xA;&#xA;mr wip: /Users/thiago.perrotta/Corp/terraform&#xA;/Users/thiago.perrotta/Corp/terraform [&amp;#43;1_branches]&#xA;&#xA;mr wip: /Users/thiago.perrotta/workspace/perrotta.dev&#xA;/Users/thiago.perrotta/workspace/perrotta.dev [&amp;#43;3_branches dirty]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Clean repos stay silent. I get to see all repos with unfinished work, and I can&#xA;see exactly why each one is dirty.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;mr wip&lt;/code&gt; works too but it&amp;rsquo;s noisier. I need to add &lt;code&gt;mr -m wip&lt;/code&gt; to my muscle&#xA;memory&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. Without the &lt;code&gt;-m&lt;/code&gt; flag, &lt;code&gt;mr&lt;/code&gt; prepends a &lt;code&gt;mr wip:&lt;/code&gt; header and a blank&#xA;line for &lt;em&gt;every&lt;/em&gt; repo, WIP or not.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;-m&lt;/code&gt; can&amp;rsquo;t be baked into the action definition — &lt;code&gt;mr&lt;/code&gt; only honors it as a CLI&#xA;flag — so the comment above the action documents it.&lt;/p&gt;&#xA;&lt;p&gt;Previously I&amp;rsquo;d run &lt;code&gt;mr xl&lt;/code&gt; (an alias for &lt;code&gt;git branch -vv&lt;/code&gt; across every repo) and&#xA;eyeball the result — but that&amp;rsquo;s dozens of lines of branch tips for tens of&#xA;repos, and I still have to scan for the ones that are actually behind or dirty.&#xA;&lt;code&gt;wip&lt;/code&gt; does the scanning: it only prints the repos that need attention, with the&#xA;reason attached. Much more ergonomic than visually diffing a wall of green.&lt;/p&gt;&#xA;&lt;p&gt;Runs read-only, so it&amp;rsquo;s safe to run twice.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;Should I rely on this workflow more often, I could leverage &lt;a href=&#34;https://perrotta.dev/2025/05/espanso-hello-world/&#34;&gt;Espanso&lt;/a&gt; or a&#xA;shell alias/function to easen its recall.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: myrepos: a wip action to find unfinished work&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>safari: (permanently) allow pop-ups for a domain
      </title>
      <link>https://perrotta.dev/2026/07/safari-permanently-allow-pop-ups-for-a-domain/</link>
      <pubDate>Thu, 16 Jul 2026 14:01:29 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>macos</category>
      <category>privacy</category>
      <guid>https://perrotta.dev/2026/07/safari-permanently-allow-pop-ups-for-a-domain/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: a site / domain needs pop-ups for OAuth / logins, but I&#xA;don&amp;rsquo;t want to disable Safari&amp;rsquo;s pop-up blocker globally just to appease a couple&#xA;of domains.&lt;/p&gt;&#xA;&lt;p&gt;Safari lets one scope the exception on a per-site basis. The domain has to be&#xA;open in a tab first, so that its entry shows up in the list:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Open the site in a new tab.&lt;/li&gt;&#xA;&lt;li&gt;Click &lt;strong&gt;Safari → Settings → Websites → Pop-up Windows&lt;/strong&gt; (left sidebar).&lt;/li&gt;&#xA;&lt;li&gt;Find the domain under &amp;ldquo;Currently Open Websites&amp;rdquo; and set it to &lt;strong&gt;Allow&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The blocker stays on everywhere else, as it should be.&lt;/p&gt;&#xA;&lt;p&gt;The default at the bottom (&amp;ldquo;When visiting other websites&amp;rdquo;) is the global toggle;&#xA;leave it on &lt;strong&gt;Block and Notify&lt;/strong&gt; so only the sites I&amp;rsquo;ve whitelisted get through.&lt;/p&gt;&#xA;&lt;p&gt;That per-site list is the same panel that governs camera, microphone,&#xA;location, and auto-play permissions.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: safari: (permanently) allow pop-ups for a domain&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/privacy/&#34;&gt;#privacy&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>claude code: replace a hand-rolled loop with /goal
      </title>
      <link>https://perrotta.dev/2026/07/claude-code-replace-a-hand-rolled-loop-with-/goal/</link>
      <pubDate>Thu, 16 Jul 2026 13:44:25 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>claude</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/07/claude-code-replace-a-hand-rolled-loop-with-/goal/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/06/claude-code-ship-your-skills-as-a-plugin-marketplace/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: one of my skills, &lt;code&gt;/pr-pass&lt;/code&gt;, was a hand-rolled loop —&#xA;push, poll CI, fix a failure, re-push, repeat, bail after five iterations.&lt;/p&gt;&#xA;&lt;p&gt;It works quite well, and it&amp;rsquo;s one of my most used skills.&lt;/p&gt;&#xA;&lt;p&gt;That said, recently Claude Code shipped&#xA;&lt;a href=&#34;https://code.claude.com/docs/en/goal&#34;&gt;&lt;code&gt;/goal&lt;/code&gt;&lt;/a&gt; (in v2.1.139), which is exactly&#xA;that loop, packaging it as a primitive. So the skill was reimplementing something&#xA;the harness &lt;em&gt;now&lt;/em&gt; does natively.&lt;/p&gt;&#xA;&lt;p&gt;In fact,&#xA;&lt;a href=&#34;https://newsletter.pragmaticengineer.com/p/what-is-loop-engineering&#34;&gt;most&lt;/a&gt;&#xA;harnesses ship &lt;code&gt;/goal&lt;/code&gt; natively these days.&lt;/p&gt;&#xA;&lt;p&gt;We shall&#xA;&lt;a href=&#34;https://github.com/thiagowfx/skills/commit/322d4523f10c8ab1fa3cf8eeb95d776c6debf17c&#34;&gt;refactor&lt;/a&gt;!&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;/goal&lt;/code&gt; sets a completion condition. After each turn a fast model checks whether&#xA;it holds; if not, Claude starts another turn on its own. It auto-clears once the&#xA;condition is met. From the docs:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The &lt;code&gt;/goal&lt;/code&gt; command sets a completion condition and Claude keeps working&#xA;toward it without you prompting each step. After each turn, a small fast model&#xA;checks whether the condition holds. If not, Claude starts another turn instead&#xA;of returning control to you.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;That deletes three things the old skill hand-built: the &lt;code&gt;Monitor&lt;/code&gt; poll waiting&#xA;on CI state, the &amp;ldquo;go back to Step 1&amp;rdquo; loop, and the max-5-iterations guard. The&#xA;guard becomes a clause in the condition itself:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/goal every check on this PR has passed (gh pr checks shows no FAILURE&#xA;and no PENDING), or stop after 5 turns&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;One catch worth knowing before reaching for &lt;code&gt;/goal&lt;/code&gt;: the evaluator (tester?) is&#xA;a fresh model that &lt;strong&gt;only reads the transcript&lt;/strong&gt;. It does not run &lt;code&gt;gh&lt;/code&gt; or read&#xA;files.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;It does not call tools, so it can only judge what Claude has already surfaced&#xA;in the conversation.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;So the condition has to be something Claude&amp;rsquo;s own output demonstrates. The skill&#xA;now says to paste the &lt;code&gt;gh pr checks&lt;/code&gt; result into the reply every turn, so the&#xA;evaluator has a CI state to judge against. Otherwise the goal would have never&#xA;resolved thus it would have looped forever.&lt;/p&gt;&#xA;&lt;p&gt;Another gotcha: &lt;code&gt;/goal&lt;/code&gt; doesn&amp;rsquo;t touch permissions. Each turn&amp;rsquo;s &lt;code&gt;git push&lt;/code&gt;&#xA;still prompts unless paired it with &lt;a href=&#34;https://code.claude.com/docs/en/auto-mode-config&#34;&gt;auto&#xA;mode&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The lesson generalizes past this one skill: whenever the harness grows a&#xA;primitive, the custom code that predates it becomes technical debt. &lt;code&gt;/loop&lt;/code&gt;,&#xA;Stop hooks, and &lt;code&gt;/goal&lt;/code&gt; now cover most of the &amp;ldquo;keep going until X&amp;rdquo; constructs I&#xA;used previously.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: claude code: replace a hand-rolled loop with /goal&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
