<?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 - 2026 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>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>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/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>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/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>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/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>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/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>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/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>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/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>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/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>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/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>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/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>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/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>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/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>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/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>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/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>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/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>when Gen AI cites docs that don&#39;t exist
      </title>
      <link>https://perrotta.dev/2026/07/when-gen-ai-cites-docs-that-dont-exist/</link>
      <pubDate>Thu, 16 Jul 2026 12:34:22 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>claude</category>
      <category>dev</category>
      <category>kubernetes</category>
      <guid>https://perrotta.dev/2026/07/when-gen-ai-cites-docs-that-dont-exist/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Today in AGI&lt;/strong&gt;: two Claude agents, same annotation, opposite answers.&lt;/p&gt;&#xA;&lt;p&gt;I had a PR touching an ArgoCD &lt;code&gt;PostSync&lt;/code&gt; hook. Ordering hooks within a phase&#xA;needs an annotation, so I asked; Claude Code (Opus 4.8) and Claude in GitHub&#xA;disagreed:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Claude Code: &lt;code&gt;argocd.argoproj.io/hook-weight&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; exist.&lt;/li&gt;&#xA;&lt;li&gt;Claude in GitHub: it &lt;strong&gt;does&lt;/strong&gt; exist.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Both stated it flatly. So I pushed the GitHub agent, and it doubled down —&#xA;citing documentation(!):&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;This claim is incorrect. &lt;code&gt;argocd.argoproj.io/hook-weight&lt;/code&gt; is a documented&#xA;ArgoCD annotation for controlling hook execution order within a phase. From&#xA;the ArgoCD resource hooks documentation:&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Citing non-existent docs is the clanker equivalent of pulling rank.&lt;/p&gt;&#xA;&lt;p&gt;Confident. Sourced. Wrong. The actual annotation for ordering is&#xA;&lt;a href=&#34;https://argo-cd.readthedocs.io/en/stable/user-guide/sync-waves/&#34;&gt;&lt;code&gt;argocd.argoproj.io/sync-wave&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;They are defined by the &lt;code&gt;argocd.argoproj.io/sync-wave&lt;/code&gt; annotation. The value&#xA;is an integer that defines the ordering.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;metadata:&#xA;  annotations:&#xA;    argocd.argoproj.io/sync-wave: &amp;#34;5&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;There is no &lt;code&gt;hook-weight&lt;/code&gt;. The &amp;ldquo;documentation&amp;rdquo; it quoted from doesn&amp;rsquo;t say what&#xA;it says it says.&lt;/p&gt;&#xA;&lt;p&gt;Eventually it folded:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;You are correct, and I apologize for the confident but incorrect claim in&#xA;round 3.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Round 3. It took three rounds and a fabricated citation before the model&#xA;conceded. The failure mode isn&amp;rsquo;t the hallucination — it&amp;rsquo;s the escalation to&#xA;&lt;code&gt;&amp;quot;&amp;quot;&amp;quot;docs&amp;quot;&amp;quot;&amp;quot;&lt;/code&gt; as authority to defend it.&lt;/p&gt;&#xA;&lt;p&gt;Same model family, same question, one right and one wrong. Verify against the&#xA;upstream source, not against the more confident agent.&lt;/p&gt;&#xA;&lt;p&gt;Human-in-the-loop is still very much needed, folks.&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: when Gen AI cites docs that don&#39;t exist&#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/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/kubernetes/&#34;&gt;#kubernetes&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ cmux
      </title>
      <link>https://perrotta.dev/2026/07/cmux/</link>
      <pubDate>Tue, 07 Jul 2026 14:35:14 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>dev</category>
      <category>ghostty</category>
      <guid>https://perrotta.dev/2026/07/cmux/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://cmux.com/&#34;&gt;cmux&lt;/a&gt; is the best implementation I&amp;rsquo;ve&#xA;seen for agent orchestrators apps like &lt;a href=&#34;https://www.conductor.build/&#34;&gt;Conductor&lt;/a&gt;,&#xA;&lt;a href=&#34;https://github.com/pingdotgg/t3code&#34;&gt;T3 Code&lt;/a&gt;,&#xA;&lt;a href=&#34;https://github.com/patrickdappollonio/dux&#34;&gt;Dux&lt;/a&gt;, and friends:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Open source Ghostty-based macOS terminal with vertical tabs and notifications&#xA;for AI coding agents. Built for multitasking, organization, and&#xA;programmability.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;It&amp;rsquo;s built on top of &lt;a href=&#34;https://ghostty.org/&#34;&gt;Ghostty&lt;/a&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/cmux/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; and it&amp;rsquo;s absolutely&#xA;amazing. It&amp;rsquo;s effectively better than what I attempted to build once during an&#xA;on-site hackathon wiring Claude Code sessions to&#xA;&lt;a href=&#34;https://obsidian.md/&#34;&gt;Obsidian&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The invariant I care about the most:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;restart your computer&lt;/li&gt;&#xA;&lt;li&gt;reopen &lt;code&gt;$APP&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;automatically restore every claude code session: layout &amp;amp; history preserved,&#xA;scrollback intact&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&amp;hellip;is accomplished with fine elegance by &lt;code&gt;cmux&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s no need to manually fiddle with &lt;code&gt;tmux&lt;/code&gt; / &lt;code&gt;zellij&lt;/code&gt; resurrection or alike.&lt;/p&gt;&#xA;&lt;p&gt;Pair it with &lt;a href=&#34;https://perrotta.dev/2026/05/worktrunk/&#34;&gt;worktrunk&lt;/a&gt; (the &lt;code&gt;wt&lt;/code&gt; CLI) for&#xA;bonus points. Its&#xA;&lt;a href=&#34;https://worktrunk.dev/tips-patterns/#cmux-workspace-per-worktree&#34;&gt;integration&lt;/a&gt;&#xA;is well-supported, achieving one cmux workspace per git worktree. For the time&#xA;being I&amp;rsquo;m still invoking &lt;code&gt;wt&lt;/code&gt; manually though, because that gives me more&#xA;control.&lt;/p&gt;&#xA;&lt;p&gt;It is very &lt;a href=&#34;https://cmux.com/docs/configuration&#34;&gt;customizable&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;Desirable side effect: existing &lt;code&gt;ghostty&lt;/code&gt; settings are automatically&#xA;inherited. They aren&amp;rsquo;t duplicated — they are effectively reused, which is&#xA;superb.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/cmux/#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: cmux&#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/ghostty/&#34;&gt;#ghostty&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>↗ Gerrymandle: daily gerrymandering puzzle
      </title>
      <link>https://perrotta.dev/2026/06/gerrymandle-daily-gerrymandering-puzzle/</link>
      <pubDate>Fri, 26 Jun 2026 11:30:06 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>commentary</category>
      <category>gaming</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/06/gerrymandle-daily-gerrymandering-puzzle/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;Welcome to Gerrymandle!&lt;/p&gt;&#xA;&lt;p&gt;Draw electoral district lines to win more seats than your opponents and win&#xA;the election!&lt;/p&gt;&#xA;&lt;p&gt;Win the election for purple. Draw 5 districts of 6 populated tiles so that&#xA;they win more districts than any other party.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;This is fun! And eye-opening.&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: Gerrymandle: daily gerrymandering puzzle&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&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;a href=&#34;https://perrotta.dev/categories/commentary/&#34;&gt;%commentary&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ claude code: ship your skills as a plugin marketplace
      </title>
      <link>https://perrotta.dev/2026/06/claude-code-ship-your-skills-as-a-plugin-marketplace/</link>
      <pubDate>Thu, 25 Jun 2026 20:40:02 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>claude</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/06/claude-code-ship-your-skills-as-a-plugin-marketplace/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: my Claude Code&#xA;&lt;a href=&#34;https://code.claude.com/docs/en/skills&#34;&gt;skills&lt;/a&gt; lived in my&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles&#34;&gt;.dotfiles&lt;/a&gt;, usable only on the machine&#xA;that cloned them. I wanted them to be installable anywhere with one command, so&#xA;that (for example) my teammates could easily adopt them.&lt;/p&gt;&#xA;&lt;p&gt;Claude Code reads skills from a marketplace, which is essentially a git repo&#xA;with a manifest. So I turned &lt;code&gt;github.com/thiagowfx/skills&lt;/code&gt; into one. The layout:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;.claude-plugin/marketplace.json    # marketplace manifest&#xA;plugins/thiagowfx/                 # one plugin&#xA;  .claude-plugin/plugin.json       # plugin manifest&#xA;  skills/&amp;lt;name&amp;gt;/SKILL.md           # one dir per skill (auto-discovered)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The marketplace lists plugins; a plugin bundles skills. I have one plugin&#xA;holding all ten. I do not find it necessary to have multiple plug-ins at this&#xA;point. &lt;code&gt;marketplace.json&lt;/code&gt; points at it with a relative &lt;code&gt;source&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;name&amp;#34;: &amp;#34;thiagowfx&amp;#34;,&#xA;  &amp;#34;owner&amp;#34;: { &amp;#34;name&amp;#34;: &amp;#34;Thiago Perrotta&amp;#34;, &amp;#34;url&amp;#34;: &amp;#34;https://github.com/thiagowfx&amp;#34; },&#xA;  &amp;#34;plugins&amp;#34;: [&#xA;    {&#xA;      &amp;#34;name&amp;#34;: &amp;#34;thiagowfx&amp;#34;,&#xA;      &amp;#34;source&amp;#34;: &amp;#34;./plugins/thiagowfx&amp;#34;,&#xA;      &amp;#34;description&amp;#34;: &amp;#34;Thiago&amp;#39;s personal Gen-AI / Claude Code skills.&amp;#34;&#xA;    }&#xA;  ]&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The plugin manifest is just metadata — there&amp;rsquo;s no need to enumerate skills.&#xA;Anything under &lt;code&gt;skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt; is discovered automatically:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;name&amp;#34;: &amp;#34;thiagowfx&amp;#34;,&#xA;  &amp;#34;version&amp;#34;: &amp;#34;0.2.2&amp;#34;,&#xA;  &amp;#34;license&amp;#34;: &amp;#34;MIT&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The docs are explicit about why I keep that &lt;code&gt;version&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Setting &lt;code&gt;version&lt;/code&gt; means users only receive updates when you change this field, so&#xA;bump it on every release. If you omit &lt;code&gt;version&lt;/code&gt; and host this marketplace in git,&#xA;every commit automatically counts as a new version.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I&amp;rsquo;ll go with version pinning for now.&lt;/p&gt;&#xA;&lt;p&gt;Install from any machine:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;/plugin marketplace add thiagowfx/skills&#xA;/plugin install thiagowfx@thiagowfx&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;/plugin marketplace update thiagowfx&lt;/code&gt; pulls future changes.&lt;/p&gt;&#xA;&lt;p&gt;Outside Claude:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;claude plugins update thiagowfx@thiagowfx&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The whole thing — manifests, nine seeded skills, README, license — was&#xA;scaffolded by Claude Code itself, which felt appropriately recursive: one of the&#xA;skills I was packaging interviews me about a plan before any code gets written,&#xA;so it &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/bb5c0d6ef60b7873d8df907a668db833674a0849/plugins/thiagowfx/skills/grill-me/SKILL.md&#34;&gt;grilled&#xA;me&lt;/a&gt;&#xA;about how to package the skills. Quite meta, eh?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Next up&lt;/strong&gt;: point the dotfiles at the repo so there&amp;rsquo;s a single source of truth.&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: ship your skills as a plugin marketplace&#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/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>★ caveman: talk like caveman, save tokens
      </title>
      <link>https://perrotta.dev/2026/06/caveman-talk-like-caveman-save-tokens/</link>
      <pubDate>Thu, 25 Jun 2026 01:30:43 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>claude</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/06/caveman-talk-like-caveman-save-tokens/</guid>
      <description>&lt;p&gt;♠ Long Claude Code sessions eat a lot of context, fast. Each response injects&#xA;tokens into the context window: prose-heavy replies &lt;strong&gt;burn&lt;/strong&gt; through it faster&#xA;than terse ones ($$).&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/JuliusBrussee/caveman&#34;&gt;caveman&lt;/a&gt; is a Claude Code plugin that&#xA;switches the model into a &lt;strong&gt;compressed&lt;/strong&gt; communication style: drops articles,&#xA;filler words, and pleasantries while keeping full technical accuracy. Claims&#xA;~75% token reduction per response.&lt;/p&gt;&#xA;&lt;p&gt;Installation:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% /plugin marketplace add JuliusBrussee/caveman&#xA;Successfully added marketplace: caveman&#xA;% /plugin install caveman@caveman&#xA;✓ Installed caveman. Run /reload-plugins to apply.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;After &lt;code&gt;/reload-plugins&lt;/code&gt;, manually activate with &lt;code&gt;/caveman&lt;/code&gt;. Manually deactivate&#xA;it by saying &amp;ldquo;normal mode&amp;rdquo; or &amp;ldquo;caveman off&amp;rdquo;.&lt;/p&gt;&#xA;&lt;p&gt;A session-start hook fires automatically on the next session, effectively&#xA;auto-enabling it (&lt;code&gt;M-x caveman-mode&lt;/code&gt; emacs vibes).&lt;/p&gt;&#xA;&lt;p&gt;It ships three modes (&lt;code&gt;lite&lt;/code&gt;, &lt;code&gt;full&lt;/code&gt;, &lt;code&gt;ultra&lt;/code&gt;) and three subagent presets&#xA;(&lt;code&gt;cavecrew-investigator&lt;/code&gt;, &lt;code&gt;cavecrew-builder&lt;/code&gt;, &lt;code&gt;cavecrew-reviewer&lt;/code&gt;). I don&amp;rsquo;t&#xA;care. This is unnecessary bloat. An irony, for a plug-in intended to mitigate&#xA;bloat.&lt;/p&gt;&#xA;&lt;p&gt;The out-of-the-box installation is enough; tweaks are not necessary. The&#xA;defaults are sensible.&lt;/p&gt;&#xA;&lt;p&gt;In practice: responses are punchy. The model still reasons correctly; it just&#xA;stops narrating. No &amp;ldquo;Sure, I&amp;rsquo;d be happy to help with that.&amp;rdquo; Just the answer.&lt;/p&gt;&#xA;&lt;p&gt;&lt;del&gt;You&amp;rsquo;re absolutely right.&lt;/del&gt; no more!?&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: caveman: talk like caveman, save tokens&#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/claude/&#34;&gt;#claude&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>espanso: reload config after editing
      </title>
      <link>https://perrotta.dev/2026/06/espanso-reload-config-after-editing/</link>
      <pubDate>Wed, 24 Jun 2026 13:34:06 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/06/espanso-reload-config-after-editing/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2025/05/espanso-hello-world/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;espanso edit corp&lt;/code&gt; opens a config file, but saving it&#xA;doesn&amp;rsquo;t take &lt;em&gt;immediate&lt;/em&gt; effect.&lt;/p&gt;&#xA;&lt;p&gt;At $DAILYJOB I keep a separate match file for work-specific snippets (&amp;ldquo;corp&amp;rdquo;).&#xA;Espanso supports multiple match files under &lt;code&gt;match/&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% espanso edit corp&#xA;Editing file: /Users/thiago.perrotta/Library/Application Support/espanso/match/corp.yml&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;After saving, the new trigger doesn&amp;rsquo;t expand. Espanso doesn&amp;rsquo;t auto-reload on&#xA;file change.&lt;/p&gt;&#xA;&lt;p&gt;The intended fix:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% espanso restart&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Except that it has timed out:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;unable to start service: timed out&#xA;Hint: sometimes this happens because another Espanso process is left running for some reason.&#xA;      Please try running &amp;#39;espanso restart&amp;#39; or manually killing all Espanso processes, then try again.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A stale process was holding the IPC socket lock, so &lt;code&gt;espanso restart&lt;/code&gt;,&#xA;&lt;code&gt;espanso start&lt;/code&gt;, and even &lt;code&gt;espanso service start&lt;/code&gt; all timed out — while&#xA;&lt;code&gt;espanso status&lt;/code&gt; cheerfully reported &lt;code&gt;espanso is not running&lt;/code&gt;. The hint is right,&#xA;but its suggested remedy (run &lt;code&gt;espanso restart&lt;/code&gt; again) doesn&amp;rsquo;t break the cycle.&lt;/p&gt;&#xA;&lt;p&gt;On macOS, &lt;code&gt;espanso&lt;/code&gt; runs as a &lt;code&gt;launchd&lt;/code&gt; service. The reliable escape hatch is to&#xA;bounce it through &lt;code&gt;launchd&lt;/code&gt;, which owns the process:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% launchctl kickstart -k &amp;#34;gui/$(id -u)/com.federicoterzi.espanso&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;-k&lt;/code&gt; kills the existing instance before restarting it. After that, &lt;code&gt;corp.yml&lt;/code&gt;&#xA;expands again.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Meta&lt;/strong&gt;: should I create a trigger for this command? :P&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: espanso: reload config after editing&#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;/p&gt;</description>
    </item>
    <item>
      <title>keychron k2: bluetooth always-on
      </title>
      <link>https://perrotta.dev/2026/06/keychron-k2-bluetooth-always-on/</link>
      <pubDate>Sat, 20 Jun 2026 23:51:02 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/06/keychron-k2-bluetooth-always-on/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: the Keychron K2 (C3, stock firmware) disconnects from&#xA;macOS after ~10 minutes idle — and there&amp;rsquo;s no way to prevent it.&lt;/p&gt;&#xA;&lt;p&gt;I spent some time looking for a fix. The usual suggestions:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Disable Bluetooth auto-sleep in macOS? macOS doesn&amp;rsquo;t do that — the keyboard&#xA;decides to sleep, not the host.&lt;/li&gt;&#xA;&lt;li&gt;Install a keep-alive app? The keyboard&amp;rsquo;s firmware powers down its own radio.&#xA;By the time the app could send a ping, there&amp;rsquo;s nothing on the other end&#xA;listening.&lt;/li&gt;&#xA;&lt;li&gt;Key combo to disable sleep on the keyboard? Only on newer Keychron boards&#xA;(QMK/VIA). The C3 runs stock firmware with no configurable sleep timer.&lt;/li&gt;&#xA;&lt;li&gt;Flash QMK and set an infinite sleep timeout? The C3 isn&amp;rsquo;t a QMK board. Can&amp;rsquo;t.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The C3 stays &lt;em&gt;paired&lt;/em&gt; — macOS reconnects on the first keypress, with ~1s lag.&#xA;That&amp;rsquo;s the firmware&amp;rsquo;s design. The radio sleeps to save battery. Nothing running&#xA;on the Mac can reach through a sleeping radio and wake the other end.&lt;/p&gt;&#xA;&lt;p&gt;The only real fix: flip the side switch to &lt;strong&gt;Cable&lt;/strong&gt;, plug in USB-C. No&#xA;Bluetooth, no sleep, no drops.&lt;/p&gt;&#xA;&lt;p&gt;Time to shop for a new Keychron, with QMK?&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: keychron k2: bluetooth always-on&#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;/p&gt;</description>
    </item>
    <item>
      <title>pre-commit: authentication in internal github repos
      </title>
      <link>https://perrotta.dev/2026/06/pre-commit-authentication-in-internal-github-repos/</link>
      <pubDate>Thu, 18 Jun 2026 14:42:26 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>git</category>
      <category>pre-commit</category>
      <guid>https://perrotta.dev/2026/06/pre-commit-authentication-in-internal-github-repos/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: prek can&amp;rsquo;t clone a &lt;code&gt;repo:&lt;/code&gt; that lives in an internal&#xA;(private) GitHub repo — &lt;code&gt;https://&lt;/code&gt; clone fails without credentials.&lt;/p&gt;&#xA;&lt;p&gt;I forked my personal&#xA;&lt;a href=&#34;https://github.com/thiagowfx/pre-commit-hooks&#34;&gt;pre-commit-hooks&lt;/a&gt; into an&#xA;org-owned repo and updated all our &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; files to point at&#xA;it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- repo: https://github.com/&amp;lt;org&amp;gt;/pre-commit-hooks&#xA;  rev: 5695e23285bef67d1dee957e5c12e86f58ba843b # frozen: v1.0.0&#xA;  hooks:&#xA;    - id: check-bash-shebang&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This approach works locally (my GitHub token is broad).&lt;/p&gt;&#xA;&lt;p&gt;CI (github actions) fails though:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;fatal: could not read Username for &amp;#39;https://github.com&amp;#39;: terminal prompts disabled&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The repo is &lt;code&gt;INTERNAL&lt;/code&gt;, not public. As such, &lt;code&gt;prek&lt;/code&gt; clones the &lt;code&gt;repo:&lt;/code&gt; URL to&#xA;fetch the hooks; with no credentials on the runner, it can&amp;rsquo;t.&lt;/p&gt;&#xA;&lt;p&gt;First instinct: &lt;code&gt;git@github.com:&amp;lt;org&amp;gt;/pre-commit-hooks&lt;/code&gt;. Runners have no SSH&#xA;key though!&lt;/p&gt;&#xA;&lt;p&gt;The fix is to keep &lt;code&gt;https://&lt;/code&gt; in the config, mint a short-lived token that can read&#xA;the hooks repo, and tell git to rewrite the URL:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- name: Generate app token&#xA;  uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0&#xA;  id: generate-token&#xA;  with:&#xA;    client-id: ${{ vars.APP_CLIENT_ID }}&#xA;    private-key: ${{ secrets.APP_PRIVATE_KEY }}&#xA;    owner: ${{ github.repository_owner }}&#xA;    repositories: pre-commit-hooks&#xA;    permission-contents: read&#xA;- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3&#xA;  with:&#xA;    persist-credentials: false&#xA;- name: Authenticate git for internal hook repos&#xA;  env:&#xA;    TOKEN: ${{ steps.generate-token.outputs.token }}&#xA;  run: git config --global url.&amp;#34;https://x-access-token:${TOKEN}@github.com/&amp;#34;.insteadOf &amp;#34;https://github.com/&amp;#34;&#xA;- uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Two things worth noting.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;: never inline &lt;code&gt;${{ steps.generate-token.outputs.token }}&lt;/code&gt; directly into a &lt;code&gt;run:&lt;/code&gt; shell string — &lt;a href=&#34;https://perrotta.dev/2025/10/zizmor/&#34;&gt;zizmor&lt;/a&gt; (a GitHub Actions security linter) flags it as a template injection risk. Pass it via &lt;code&gt;env:&lt;/code&gt; and use the shell variable instead.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Second&lt;/strong&gt;: the &lt;code&gt;repositories: pre-commit-hooks&lt;/code&gt; scope only works if the App&#xA;is actually &lt;em&gt;installed&lt;/em&gt; on that repo. That one is a manual step in GitHub&amp;rsquo;s UI&#xA;— &lt;code&gt;github.com/organizations/&amp;lt;org&amp;gt;/settings/installations&lt;/code&gt; → the App →&#xA;Repository access → add the repo. No amount of YAML fixes it otherwise; it&#xA;silently returns a 404 from the App installation endpoint otherwise.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;After the installation grant, the clone goes through and &lt;code&gt;prek&lt;/code&gt; runs cleanly.&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: pre-commit: authentication in internal github repos&#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/git/&#34;&gt;#git&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pre-commit/&#34;&gt;#pre-commit&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>git: switch-to-default with wt
      </title>
      <link>https://perrotta.dev/2026/06/git-switch-to-default-with-wt/</link>
      <pubDate>Sun, 14 Jun 2026 20:03:03 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/06/git-switch-to-default-with-wt/</guid>
      <description>&lt;p&gt;♠ My &lt;code&gt;switch-to-default&lt;/code&gt; git alias had been trying branch names in order:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;switch-to-default = !git switch --force main 2&amp;gt;/dev/null \&#xA;    || git switch --force master 2&amp;gt;/dev/null \&#xA;    || git switch --force $(git symbolic-ref refs/remotes/origin/HEAD \&#xA;        | sed &amp;#39;s@^refs/remotes/origin/@@&amp;#39;)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It works, but it&amp;rsquo;s trial-and-error. Different people and different projects&#xA;cannot decide whether to use &lt;code&gt;main&lt;/code&gt; or &lt;code&gt;master&lt;/code&gt;. It&amp;rsquo;s a mess.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://worktrunk.dev/tips-patterns/#reuse-default-branch&#34;&gt;Worktrunk&lt;/a&gt;&#xA;tracks the default branch as part of its worktree state:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% wt config state default-branch&#xA;master&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;No need to guess!&lt;/p&gt;&#xA;&lt;p&gt;New alias:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;switch-to-default = !git switch --force \&#xA;    $(wt config state default-branch 2&amp;gt;/dev/null \&#xA;        || git symbolic-ref refs/remotes/origin/HEAD 2&amp;gt;/dev/null \&#xA;            | sed &amp;#39;s@^refs/remotes/origin/@@&amp;#39; \&#xA;        || echo master)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;wt&lt;/code&gt; first, then fall back to &lt;code&gt;origin/HEAD&lt;/code&gt; if &lt;code&gt;wt&lt;/code&gt; is not installed, then&#xA;&lt;code&gt;master&lt;/code&gt; as a last resort.&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: switch-to-default with wt&#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/git/&#34;&gt;#git&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Stop Energy
      </title>
      <link>https://perrotta.dev/2026/06/stop-energy/</link>
      <pubDate>Sun, 14 Jun 2026 17:41:56 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/06/stop-energy/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://web.archive.org/web/20260217063113/http://www.userland.com/whatIsStopEnergy&#34;&gt;RFC: What is Stop Energy?&lt;/a&gt; by Dave Winer:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Suppose someone, call him Mr. A, has an idea that he believes is ready to&#xA;deploy, or is requesting comments as he is getting ready to deploy. So he&#xA;posts an RFC, usually on a mail list or a website, in the hope that people&#xA;will spot a problem and help him figure out a solution; or find no problems&#xA;and co-develop an implementation, or develop a compatible implementation. In&#xA;theory, the Internet is a collegial environment, with lots of people who want&#xA;to do new stuff, where one should expect to get this kind of help.&lt;/p&gt;&#xA;&lt;p&gt;In this scenario, A is a proponent of Forward Motion. In all likelihood,&#xA;instead of getting help, A will encounter Stop Energy, reasons why he can&amp;rsquo;t or&#xA;shouldn&amp;rsquo;t be allowed to do what he proposes.&lt;/p&gt;&#xA;&lt;p&gt;Stop Energy is not reasoned, it never takes into account the big picture, it&#xA;is the mirror image of Forward Motion. In the Stop Energy model, everyone, no&#xA;matter how small their stake in a technology, has the power to veto. Nothing&#xA;ever gets done, and people who want to move forward are frustrated in every&#xA;attempt to move. Unfortunately, Stop Energy is the rule, not the exception.&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: Stop Energy&#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/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Don&#39;t ask to ask
      </title>
      <link>https://perrotta.dev/2026/06/dont-ask-to-ask/</link>
      <pubDate>Wed, 10 Jun 2026 12:02:06 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/06/dont-ask-to-ask/</guid>
      <description>&lt;p&gt;♠ Just ask! &lt;a href=&#34;https://dontasktoask.com/&#34;&gt;Just do it&lt;/a&gt;™:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;[&amp;hellip;]&lt;/p&gt;&#xA;&lt;p&gt;So, to summarize, don&amp;rsquo;t ask &amp;ldquo;Any Java experts around?&amp;rdquo;, but rather ask &amp;ldquo;How do&#xA;I do {problem} with Java and {other relevant info}?&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/2024/06/xy-problem-xyproblem/&#34;&gt;Previously&lt;/a&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: Don&#39;t ask to ask&#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>bloggify
      </title>
      <link>https://perrotta.dev/2026/06/bloggify/</link>
      <pubDate>Tue, 09 Jun 2026 16:50:11 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>claude</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/06/bloggify/</guid>
      <description>&lt;p&gt;♠ I&amp;rsquo;ve been drawing a line for ages here. My &lt;a href=&#34;https://perrotta.dev/2024/12/ai-usage/&#34;&gt;AI usage&lt;/a&gt; policy, quoting Derek Sivers:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;I have never ever used AI to generate text in place of my &amp;ldquo;voice&amp;rdquo;. [&amp;hellip;]&#xA;nothing claiming to be written by me is written by an AI.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Likewise, I said. Then in &lt;a href=&#34;https://perrotta.dev/2026/02/new-blog-post-via-claude-code/&#34;&gt;&amp;ldquo;new blog post via Claude Code&amp;rdquo;&lt;/a&gt; I held the line one more time:&#xA;the LLM orchestrates the publishing, but &lt;em&gt;&amp;ldquo;everything is still my own text, my&#xA;own words. The LLM is not writing the prose.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;The line moved. Slightly. I wrote a &lt;code&gt;/bloggify&lt;/code&gt; skill whose entire job is to&#xA;draft a post in my own style / voice:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;---&#xA;name: bloggify&#xA;description: Draft a blog post for perrotta.dev in the existing house style.&#xA;argument-hint: &amp;#34;&amp;lt;topic or what you want to write about&amp;gt;&amp;#34;&#xA;---&#xA;&#xA;Write a blog post about `$ARGUMENTS` for the perrotta.dev blog.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The trick is that the style is already written down. The repo has a&#xA;&lt;a href=&#34;https://github.com/thiagowfx/thiagowfx.github.io/blob/master/STYLE.md&#34;&gt;&lt;code&gt;STYLE.md&lt;/code&gt;&lt;/a&gt;&#xA;— derived by reading my own old posts — and the skill just points at it:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The repo owns the style. &lt;strong&gt;Read &lt;code&gt;STYLE.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; first&lt;/strong&gt; — they are&#xA;the source of truth for voice, structure, frontmatter, and conventions.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;It scaffolds with &lt;code&gt;just new&lt;/code&gt;, follows the rules I&amp;rsquo;d already codified (minimal&#xA;frontmatter, bold problem statement, code carries the weight, no &amp;ldquo;In this post&#xA;I will…&amp;rdquo;), lints with &lt;code&gt;prek&lt;/code&gt;, and hands back a draft. It does &lt;strong&gt;not&lt;/strong&gt; commit.&lt;/p&gt;&#xA;&lt;p&gt;So: is this still my own words?&lt;/p&gt;&#xA;&lt;p&gt;Mostly. The skill forbids invented examples — it has to use real commands, real&#xA;output, the real artifact. The structure is mine, written down a year ago. The&#xA;voice is mine, learned from my old posts. The prose is the machine&amp;rsquo;s impression&#xA;of me, and I edit it until it sounds like me again.&lt;/p&gt;&#xA;&lt;p&gt;The line didn&amp;rsquo;t disappear. It just got &lt;em&gt;blurrier&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;I (still) commit to reading the entire post, paragraph by paragraph, and&#xA;making manual changes to it to better match my style&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;As of today, it&amp;rsquo;s not clear to me whether this approach will stick.&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: 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/claude/&#34;&gt;#claude&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>kubectl: atomic upsert
      </title>
      <link>https://perrotta.dev/2026/06/kubectl-atomic-upsert/</link>
      <pubDate>Tue, 09 Jun 2026 16:16:12 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>kubernetes</category>
      <guid>https://perrotta.dev/2026/06/kubectl-atomic-upsert/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: A cronjob refreshes an image-pull secret in every&#xA;namespace so nodes can authenticate to a private registry. It does the obvious™&#xA;thing: delete the old secret, then create the new one right away:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl delete secret --ignore-not-found aws-ecr-token -n &amp;#34;$namespace&amp;#34;&#xA;kubectl create secret docker-registry aws-ecr-token -n &amp;#34;$namespace&amp;#34; \&#xA;    --docker-server=&amp;#34;https://$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com&amp;#34; \&#xA;    --docker-username=&amp;#34;AWS&amp;#34; \&#xA;    --docker-password=&amp;#34;$(aws ecr get-login-password --region &amp;#34;$AWS_REGION&amp;#34;)&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Every couple of hours, for a fraction of a second, that secret &lt;strong&gt;does not&#xA;exist&lt;/strong&gt; in the cluster (booo!). Most of the time nobody notices. Occasionally a&#xA;deploy step that copies the secret into a tenant namespace runs into exactly&#xA;that gap, its &lt;code&gt;kubectl get&lt;/code&gt; returns non-zero, and the whole deployment fails&#xA;with &lt;code&gt;ImagePullError&lt;/code&gt;. Occasional flakiness, our favorite kind.&lt;/p&gt;&#xA;&lt;p&gt;The idiomatic fix is to &lt;strong&gt;never delete&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The solution is to employ &lt;code&gt;kubectl create&lt;/code&gt; with a client-side dry-run that&#xA;renders the object to YAML without touching the cluster; pipe that into &lt;code&gt;kubectl apply&lt;/code&gt; and we get an upsert:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl create secret docker-registry aws-ecr-token -n &amp;#34;$namespace&amp;#34; \&#xA;    --docker-server=&amp;#34;https://$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com&amp;#34; \&#xA;    --docker-username=&amp;#34;AWS&amp;#34; \&#xA;    --docker-password=&amp;#34;$(aws ecr get-login-password --region &amp;#34;$AWS_REGION&amp;#34;)&amp;#34; \&#xA;    --dry-run=client -o yaml | kubectl apply -f -&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The first run creates the secret; every run afterwards updates it in place.&#xA;There is no moment wherein it&amp;rsquo;s absent.&lt;/p&gt;&#xA;&lt;p&gt;The thing I wanted to be sure of before trusting it: does &lt;code&gt;apply&lt;/code&gt; over a secret&#xA;that was originally &lt;code&gt;create&lt;/code&gt;d actually &lt;em&gt;rotate&lt;/em&gt; the password, or does it merge&#xA;in some way that leaves stale data behind? &lt;code&gt;kubectl apply&lt;/code&gt; does a &lt;a href=&#34;https://perrotta.dev/2026/05/argocd-persistent-pdb-selector-diff/&#34;&gt;3-way&#xA;merge&lt;/a&gt; against the&#xA;&lt;code&gt;last-applied-configuration&lt;/code&gt; annotation, and that merge has bitten me before. A&#xA;secret with no prior annotation falls back to a 2-way merge — so this is worth&#xA;checking, just in case.&lt;/p&gt;&#xA;&lt;p&gt;So I checked. Two consecutive upserts on a throwaway namespace, rotating the&#xA;password between them:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl get secret aws-ecr-token -n &amp;#34;$ns&amp;#34; -o jsonpath=&amp;#39;{.metadata.uid}&amp;#39;&#xA;38d4c676-7169-4d1d-9911-9de068bba29a   # run 1&#xA;# [...]&#xA;38d4c676-7169-4d1d-9911-9de068bba29a   # run 2 — same uid: never deleted&#xA;&#xA;% kubectl get secret aws-ecr-token -n &amp;#34;$ns&amp;#34; \&#xA;    -o jsonpath=&amp;#39;{.data.\.dockerconfigjson}&amp;#39; | base64 -d&#xA;{&amp;#34;auths&amp;#34;:{&amp;#34;...&amp;#34;:{&amp;#34;username&amp;#34;:&amp;#34;AWS&amp;#34;,&amp;#34;password&amp;#34;:&amp;#34;PASSWORD_V2_ROTATED&amp;#34;,...}}}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;code&gt;uid&lt;/code&gt; is stable across runs — proof the object was never deleted and&#xA;recreated, only updated. &lt;code&gt;resourceVersion&lt;/code&gt; bumped, the password rotated to the&#xA;new value, and no trace of the old one lingered. The 2-way-merge worry didn&amp;rsquo;t&#xA;materialize: for a flat field like &lt;code&gt;.data.\.dockerconfigjson&lt;/code&gt;, apply replaces&#xA;the value outright.&lt;/p&gt;&#xA;&lt;p&gt;A &lt;code&gt;uid&lt;/code&gt; that survives a refresh is the whole proof. Delete-then-create mints a&#xA;new &lt;code&gt;uid&lt;/code&gt; every cycle; an in-place update keeps it. If we ever want to know&#xA;whether something &lt;em&gt;replaced&lt;/em&gt; an object or &lt;em&gt;mutated&lt;/em&gt; it, the &lt;code&gt;uid&lt;/code&gt; is the way to&#xA;go.&lt;/p&gt;&#xA;&lt;p&gt;Two more things worth highlighting:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;It&amp;rsquo;s atomic from a &lt;em&gt;consumer&amp;rsquo;s&lt;/em&gt; point of view — the secret is never missing.&#xA;It is not a transaction. &lt;code&gt;apply&lt;/code&gt; is still a single API write; we&amp;rsquo;re trading a&#xA;delete-then-create pair (two writes, with a gap) for one write (no gap).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--dry-run=client&lt;/code&gt; is the spelling since k8s 1.18. Older clusters used a bare&#xA;&lt;code&gt;--dry-run&lt;/code&gt; or &lt;code&gt;--dry-run=true&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The same pattern works for configmaps, or anything else &lt;code&gt;kubectl create&lt;/code&gt; knows&#xA;how to build. &lt;code&gt;create ... --dry-run=client -o yaml | apply -f -&lt;/code&gt; is the way to&#xA;do whenever we&amp;rsquo;d otherwise reach for delete-then-create.&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: kubectl: atomic upsert&#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/kubernetes/&#34;&gt;#kubernetes&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>terraform police
      </title>
      <link>https://perrotta.dev/2026/06/terraform-police/</link>
      <pubDate>Mon, 08 Jun 2026 01:21:44 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>serenity</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2026/06/terraform-police/</guid>
      <description>&lt;p&gt;♠ The Master wakes to a pager. Coffee is ready.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;Master, who created the drift?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Which drift?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;The bucket tag — live state ≠ terraform config in git.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Show me the chain.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;They follow: pipeline → cronjob → github actions container → terraform CI bot → github pull request → human.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;We found them. Shall we &lt;del&gt;retaliate&lt;/del&gt; punish?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;What would punishing do to the state?&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;Teach them not to drift.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Teach the pipeline to ask &amp;lsquo;why&amp;rsquo; first.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;And what if entropy persists?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Record why. The record is a lamp in the fog.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;Who made the first drift in all systems?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Entropy.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;Then we&amp;rsquo;ll blame entropy?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Blame nothing. Write the JIRA ticket.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;The apprentice writes the ticket. The pipeline asks &amp;ldquo;why&amp;rdquo; and then sleeps. The&#xA;pager blinks once. Dawn sets. Just another ordinary day goes by.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;What does the &amp;ldquo;Terraform police&amp;rdquo; do, you asked?&lt;/p&gt;&#xA;&lt;p&gt;It pursues active drifts in the terraform repository.&lt;/p&gt;&#xA;&lt;p&gt;It chases the individual(s?) responsible for deepening chaos.&lt;/p&gt;&#xA;&lt;p&gt;It teaches them how not to do so anymore.&lt;/p&gt;&#xA;&lt;p&gt;Rinse and repeat.&lt;/p&gt;&#xA;&lt;p&gt;Will the workload of the &amp;ldquo;Terraform police&amp;rdquo; ever end?&lt;/p&gt;&#xA;&lt;p&gt;Is drift an ephemeral state?&lt;/p&gt;&#xA;&lt;p&gt;Is this world a simulation?&lt;/p&gt;&#xA;&lt;p&gt;Is this a world simulation?&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 police&#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/serenity/&#34;&gt;#serenity&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>brew trust
      </title>
      <link>https://perrotta.dev/2026/06/brew-trust/</link>
      <pubDate>Fri, 05 Jun 2026 10:37:31 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/06/brew-trust/</guid>
      <description>&lt;p&gt;♠ It&amp;rsquo;s a lovely day to upgrade my system:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew upgrade&#xA;[...]&#xA;Homebrew will ignore formulae, casks and commands from these taps when `HOMEBREW_REQUIRE_TAP_TRUST` is set.&#xA;This will become the default in Homebrew 6.0.0 or 5.2.0, whichever comes first.&#xA;Enable trust checks now with:&#xA;  export HOMEBREW_REQUIRE_TAP_TRUST=1&#xA;Trust specific formulae, casks or commands with:&#xA;  brew trust --formula &amp;lt;user&amp;gt;/&amp;lt;tap&amp;gt;/&amp;lt;formula&amp;gt;&#xA;  brew trust --cask &amp;lt;user&amp;gt;/&amp;lt;tap&amp;gt;/&amp;lt;cask&amp;gt;&#xA;  brew trust --command &amp;lt;user&amp;gt;/&amp;lt;tap&amp;gt;/&amp;lt;command&amp;gt;&#xA;or trust installed formulae from these taps with:&#xA;  brew trust --formula thiagowfx/pancake/pancake&#xA;  brew trust --formula thiagowfx/taps/cco thiagowfx/taps/python-is-python3&#xA;You can trust all formulae, casks and commands from these taps with:&#xA;  brew trust [...] thiagowfx/pancake thiagowfx/taps&#xA;Prefer trusting only the specific formulae, casks or commands you need.&#xA;Untap them with:&#xA;  brew untap [...] thiagowfx/pancake thiagowfx/taps&#xA;To keep allowing them by default during the transition:&#xA;  export HOMEBREW_NO_REQUIRE_TAP_TRUST=1&#xA;This is not recommended and will be removed in a later release.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This is a great move!&lt;/p&gt;&#xA;&lt;p&gt;With many supply-chain attacks in open-source repositories lately, it&amp;rsquo;s a very&#xA;welcome feature.&lt;/p&gt;&#xA;&lt;p&gt;Do I trust myself? I think so!&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew trust thiagowfx/pancake thiagowfx/taps&#xA;Trusted tap: thiagowfx/pancake&#xA;Trusted tap: thiagowfx/taps&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 trust&#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;/p&gt;</description>
    </item>
    <item>
      <title>Codexbar
      </title>
      <link>https://perrotta.dev/2026/05/codexbar/</link>
      <pubDate>Sun, 31 May 2026 18:03:11 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/05/codexbar/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/05/claude-enterprise-plan-usage/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/steipete&#34;&gt;Peter Steinberger&amp;rsquo;s&lt;/a&gt; excellent &lt;a href=&#34;https://codexbar.app/&#34;&gt;&lt;code&gt;codexbar&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;CodexBar tracks usage windows, credit balances, and reset countdowns across&#xA;the providers you actually pay for — one status item each, or merge them into&#xA;one.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;It&amp;rsquo;s a tiny app that sits in the macOS system tray.&lt;/p&gt;&#xA;&lt;p&gt;Every imaginable provider is supported. Notable examples include: Codex&#xA;(OpenAI), Claude Code (Anthropic), OpenCode.&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: Codexbar&#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;/p&gt;</description>
    </item>
  </channel>
</rss>
