<?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>Bloggify on ¬ just serendipity 🍀</title>
    <link>https://perrotta.dev/</link>
    <description>Recent content in Bloggify on ¬ just serendipity 🍀</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <managingEditor>serendipity@perrotta.dev (Thiago Perrotta)</managingEditor>
    <webMaster>serendipity@perrotta.dev (Thiago Perrotta)</webMaster>
    <copyright>© 2013 - present Thiago Perrotta ·
  a fork of [hugo ʕ•ᴥ•ʔ bear](https://github.com/janraasch/hugo-bearblog/)
</copyright>
    <lastBuildDate>Sun, 16 Aug 2026 14:12:17 +0200</lastBuildDate>
    <atom:link href="https://perrotta.dev/tags/bloggify/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>homebrew: cask drift after an in-app update
      </title>
      <link>https://perrotta.dev/2026/08/homebrew-cask-drift-after-an-in-app-update/</link>
      <pubDate>Sun, 16 Aug 2026 14:11:17 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/08/homebrew-cask-drift-after-an-in-app-update/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: every &lt;code&gt;brew upgrade&lt;/code&gt; died on a cask whose app had&#xA;upgraded itself years ago.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew upgrade&#xA;==&amp;gt; Upgrading 1 outdated package:&#xA;1password 7.9.2 -&amp;gt; 8.12.33&#xA;==&amp;gt; Upgrading 1password&#xA;  7.9.2 -&amp;gt; 8.12.33&#xA;Error: 1password: It seems the App source &amp;#39;/Applications/1Password 7.app&amp;#39; is not there.&#xA;==&amp;gt; Purging files for version 8.12.33 of Cask 1password&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That path stopped existing when 1Password 8 renamed its bundle from&#xA;&lt;code&gt;1Password 7.app&lt;/code&gt; to &lt;code&gt;1Password.app&lt;/code&gt;. The app had updated itself in place,&#xA;behind Homebrew&amp;rsquo;s back:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% defaults read /Applications/1Password.app/Contents/Info.plist CFBundleShortVersionString&#xA;8.12.30&#xA;% brew list --cask --versions 1password&#xA;1password 7.9.2&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Two disagreeing sources of truth. &lt;code&gt;--adopt&lt;/code&gt; looked like the obvious cure, and&#xA;wasn&amp;rsquo;t — the stale receipt still routes into the upgrade code path, which still&#xA;looks for the v7 bundle:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew install --cask --adopt 1password&#xA;==&amp;gt; Upgrading 1password&#xA;  7.9.2 -&amp;gt; 8.12.33&#xA;Error: 1password: It seems the App source &amp;#39;/Applications/1Password 7.app&amp;#39; is not there.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The receipt is the whole problem, so look at what it actually holds:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% find /opt/homebrew/Caskroom/1password -maxdepth 2&#xA;/opt/homebrew/Caskroom/1password&#xA;/opt/homebrew/Caskroom/1password/.metadata&#xA;/opt/homebrew/Caskroom/1password/.metadata/config.json&#xA;/opt/homebrew/Caskroom/1password/.metadata/7.9.2&#xA;/opt/homebrew/Caskroom/1password/7.9.2&#xA;% ls -A /opt/homebrew/Caskroom/1password/7.9.2/&#xA;% du -sh /opt/homebrew/Caskroom/1password&#xA;8.0K&#x9;/opt/homebrew/Caskroom/1password&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Eight kilobytes of bookkeeping and an empty version directory. Nothing in there&#xA;is the application. Delete it, then reinstall over the app that is really on&#xA;disk:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% rm -rf /opt/homebrew/Caskroom/1password&#xA;% brew install --cask --force 1password&#xA;==&amp;gt; Fetching downloads for: 1password&#xA;✔︎ Cask 1password (8.12.33)&#xA;Warning: It seems there is already an App at &amp;#39;/Applications/1Password.app&amp;#39;; overwriting.&#xA;==&amp;gt; Installing Cask 1password&#xA;==&amp;gt; Removing App &amp;#39;/Applications/1Password.app&amp;#39;&#xA;==&amp;gt; Moving App &amp;#39;1Password.app&amp;#39; to &amp;#39;/Applications/1Password.app&amp;#39;&#xA;🍺  1password was successfully installed!&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Both sides agree again:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% defaults read /Applications/1Password.app/Contents/Info.plist CFBundleShortVersionString&#xA;8.12.33&#xA;% brew list --cask --versions 1password&#xA;1password 8.12.33&#xA;% brew outdated --cask | grep 1password&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Quitting the app first is mandatory, and this one ignored two polite&#xA;&lt;code&gt;osascript&lt;/code&gt; requests before a plain &lt;code&gt;kill&lt;/code&gt; took:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% osascript -e &amp;#39;quit app &amp;#34;1Password&amp;#34;&amp;#39;&#xA;% pgrep -x 1Password&#xA;740&#xA;% kill 740&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The general shape: a cask whose app ships its own updater will drift, and&#xA;Homebrew only notices when the vendor also renames the bundle. &lt;code&gt;--adopt&lt;/code&gt; is for&#xA;an app Homebrew never tracked; for an app it tracked &lt;em&gt;wrongly&lt;/em&gt;, the receipt has&#xA;to go first.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: homebrew: cask drift after an in-app update&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>new tag: bloggify
      </title>
      <link>https://perrotta.dev/2026/08/new-tag-bloggify/</link>
      <pubDate>Sun, 16 Aug 2026 13:50:00 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/08/new-tag-bloggify/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/06/bloggify/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: some readers may not want AI-assisted posts in their feed&#xA;reader, even with my edits. I support making it easier for people to opt out&#xA;from such posts. They should be able to filter them out without unsubscribing&#xA;from the whole blog.&lt;/p&gt;&#xA;&lt;p&gt;Every post the &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;&#xA;skill drafts already carries a footer disclosing it. What was missing was a&#xA;machine-readable signal, not just a line at the bottom a feed reader can&amp;rsquo;t act&#xA;on. So I grepped for the marker and tagged every post that has it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% grep -rl &amp;#34;Drafted with \[\`/bloggify\`\]&amp;#34; content/posts/ | wc -l&#xA;27&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git show 79018e8cf1 -- content/posts/2026-06-09-bloggify.md&#xA;diff --git content/posts/2026-06-09-bloggify.md content/posts/2026-06-09-bloggify.md&#xA;index b0a5a8ee86..9d15d656fd 100644&#xA;--- content/posts/2026-06-09-bloggify.md&#xA;&amp;#43;&amp;#43;&amp;#43; content/posts/2026-06-09-bloggify.md&#xA;@@ -3,6 &amp;#43;3,7 @@ title: &amp;#34;bloggify&amp;#34;&#xA; date: 2026-06-09T16:50:11&amp;#43;02:00&#xA; tags:&#xA;   - ai&#xA;&amp;#43;  - bloggify&#xA;   - claude&#xA;   - dev&#xA;   - meta&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Every post&amp;rsquo;s &lt;code&gt;tags:&lt;/code&gt; list ends up in the RSS &lt;code&gt;&amp;lt;item&amp;gt;&lt;/code&gt; as a &lt;code&gt;&amp;lt;category&amp;gt;&lt;/code&gt;&#xA;element, one per tag (see &lt;code&gt;layouts/_default/rss.xml&lt;/code&gt;). So &lt;code&gt;bloggify&lt;/code&gt; is now a&#xA;regular, filterable category in the feed, same as any other tag.&lt;/p&gt;&#xA;&lt;p&gt;For those of you who use &lt;a href=&#34;https://miniflux.app/&#34;&gt;Miniflux&lt;/a&gt;, filtering it out is the same trick from&#xA;&lt;a href=&#34;https://perrotta.dev/2026/02/miniflux-block-entry-matching-url/&#34;&gt;blocking an entry matching a URL&lt;/a&gt; and the &lt;a href=&#34;https://perrotta.dev/2024/12/miniflux-global-blocklist/&#34;&gt;global&#xA;blocklist&lt;/a&gt;, except on the&#xA;&lt;code&gt;EntryTag&lt;/code&gt; field instead of &lt;code&gt;EntryURL&lt;/code&gt; or &lt;code&gt;EntryTitle&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;EntryTag=(?i)^bloggify$&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Added as a global block rule in miniflux, and every future &lt;code&gt;/bloggify&lt;/code&gt;-drafted&#xA;post never reaches the inbox. &lt;code&gt;/tags/bloggify/&lt;/code&gt; on the blog itself works too,&#xA;for anyone who&amp;rsquo;d rather subscribe to the opposite: only the AI-assisted posts.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: new tag: bloggify&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/meta/&#34;&gt;#meta&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: prompt before dangerous commands
      </title>
      <link>https://perrotta.dev/2026/08/pi-prompt-before-dangerous-commands/</link>
      <pubDate>Sun, 16 Aug 2026 13:28:44 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/08/pi-prompt-before-dangerous-commands/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-block-dangerous-commands-syntax-aware/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: my Pi extension blocked dangerous shell commands outright,&#xA;even when I had already preserved the state they could destroy.&lt;/p&gt;&#xA;&lt;p&gt;The guard parses every &lt;code&gt;bash&lt;/code&gt; tool call with tree-sitter, catching commands such&#xA;as &lt;code&gt;rm -rf&lt;/code&gt;, &lt;code&gt;terraform destroy&lt;/code&gt;, and destructive Git operations. Today it did&#xA;exactly what I had asked:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git -C &amp;#34;$(brew --repo thiagowfx/pancake)&amp;#34; reset --hard origin/master&#xA;git reset --hard is blocked - discards changes irreversibly&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Blocking remains the right default, but an interactive session can ask me. Pi&#xA;extensions can intercept the&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/extensions.md#tool_call&#34;&gt;&lt;code&gt;tool_call&lt;/code&gt; event&lt;/a&gt;&#xA;and open a confirmation dialog through &lt;code&gt;ctx.ui&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The change in&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/commit/b5cb42f8deafae176ecddc12fccbd7c04ca264dd&#34;&gt;&lt;code&gt;dangerous-command-guard/guard.ts&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;-  pi.on(&amp;#34;tool_call&amp;#34;, async (event) =&amp;gt; {&#xA;&amp;#43;  pi.on(&amp;#34;tool_call&amp;#34;, async (event, ctx) =&amp;gt; {&#xA;     if (event.toolName !== &amp;#34;bash&amp;#34;) return;&#xA;&#xA;     const command = event.input.command;&#xA;     if (typeof command !== &amp;#34;string&amp;#34;) return;&#xA;&#xA;     const blocked = await findBlockedCommand(command);&#xA;-    if (blocked) return { block: true, reason: blocked.reason };&#xA;&amp;#43;    if (!blocked) return;&#xA;&amp;#43;    if (!ctx.hasUI) return { block: true, reason: blocked.reason };&#xA;&amp;#43;&#xA;&amp;#43;    const allowed = await ctx.ui.confirm(&#xA;&amp;#43;      &amp;#34;Allow dangerous command?&amp;#34;,&#xA;&amp;#43;      `${blocked.command}\n\n${blocked.reason}`,&#xA;&amp;#43;    );&#xA;&amp;#43;    if (!allowed) return { block: true, reason: &amp;#34;Blocked by user&amp;#34; };&#xA;   });&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Interactive Pi now pauses for approval. Print and JSON modes have no UI, so they&#xA;still fail closed. Safe commands never prompt.&lt;/p&gt;&#xA;&lt;p&gt;The tests exercise all three decisions:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;✔ extension prompts before dangerous bash tool calls (0.66225ms)&#xA;ℹ tests 48&#xA;ℹ pass 48&#xA;ℹ fail 0&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Same guardrails, with an escape hatch operated by a human rather than a model.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: prompt before dangerous commands&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: am I sandboxed?
      </title>
      <link>https://perrotta.dev/2026/08/pi-am-i-sandboxed/</link>
      <pubDate>Sat, 15 Aug 2026 00:56:29 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/08/pi-am-i-sandboxed/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: asking a coding agent whether it runs in a sandbox&#xA;returns a confident answer derived from its system prompt, not from the machine&#xA;it is actually running on.&lt;/p&gt;&#xA;&lt;p&gt;I asked &lt;a href=&#34;https://github.com/earendil-works/pi-mono&#34;&gt;Pi&lt;/a&gt; exactly that. The reply&#xA;was assured:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;No — pi itself doesn&amp;rsquo;t sandbox tool execution. When I run &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, or&#xA;&lt;code&gt;write&lt;/code&gt;, they execute directly on your machine with your user&amp;rsquo;s permissions,&#xA;in the current working directory (&lt;code&gt;/Users/thiago.perrotta/.pi&lt;/code&gt;). [&amp;hellip;] But&#xA;there&amp;rsquo;s no OS-level sandbox (no container, no seccomp, no filesystem jail).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;It&amp;rsquo;s plausible, well-formatted, and&amp;hellip;&lt;strong&gt;wrong&lt;/strong&gt;!. One typo-ridden command was&#xA;enough to break the illusion:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ls ~/worksppace&#xA;ls: cannot access &amp;#39;/home/hostuser/worksppace&amp;#39;: No such file or directory&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;~&lt;/code&gt; is not &lt;code&gt;/Users/thiago.perrotta&lt;/code&gt;. The follow-up probe:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pwd; uname -a; whoami; cat /etc/os-release&#xA;/Users/thiago.perrotta/.pi&#xA;Linux lima-rancher-desktop 6.18.37-0-virt #1-Alpine SMP PREEMPT_DYNAMIC 2026-06-29 10:52:25 aarch64 GNU/Linux&#xA;hostuser&#xA;PRETTY_NAME=&amp;#34;Debian GNU/Linux 12 (bookworm)&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A macOS-shaped &lt;code&gt;pwd&lt;/code&gt; inside an aarch64 Debian container, on an Alpine&#xA;&lt;a href=&#34;https://lima-vm.io/&#34;&gt;Lima&lt;/a&gt; VM, under Rancher Desktop. &lt;em&gt;Containerception&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;And the giveaway at the filesystem root:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ls -la / | head -5&#xA;total 68&#xA;drwxr-xr-x   1 root root 4096 Aug 15 00:54 .&#xA;drwxr-xr-x   1 root root 4096 Aug 15 00:54 ..&#xA;-rwxr-xr-x   1 root root    0 Aug 15 00:54 .dockerenv&#xA;drwxr-xr-x   3 root root 4096 Aug 15 00:54 Users&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;/.dockerenv&lt;/code&gt;, plus a &lt;code&gt;/Users&lt;/code&gt; bind mount recreating just enough of the host&#xA;path for the system prompt&amp;rsquo;s &lt;code&gt;cwd&lt;/code&gt; to resolve.&lt;/p&gt;&#xA;&lt;p&gt;The model was not lying; it had no way to know. Its self-description comes from&#xA;tokens someone else wrote, while the ground truth sits one &lt;code&gt;uname&lt;/code&gt; away. The&#xA;useful lesson is that agent introspection is a documentation lookup, not a&#xA;measurement — for anything security-relevant, make it run the command.&lt;/p&gt;&#xA;&lt;p&gt;The sandbox was &lt;a href=&#34;https://perrotta.dev/2026/02/cco-claude-condom-sandbox/&#34;&gt;cco&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: am I sandboxed?&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>homebrew: replace deprecated and orphaned packages
      </title>
      <link>https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/</link>
      <pubDate>Mon, 10 Aug 2026 15:10:42 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;brew doctor&lt;/code&gt; found one deprecated cask and one installed&#xA;keg whose formula had disappeared.&lt;/p&gt;&#xA;&lt;h2 id=&#34;tflint&#34;&gt;&#xA;  tflint&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/#tflint&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Warning: Some installed casks are deprecated or disabled.&#xA;You should find replacements for the following casks:&#xA;flameshot&#xA;&#xA;Warning: Some installed kegs have no formulae!&#xA;You should find replacements for the following formulae:&#xA;  tflint&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Homebrew/core &lt;a href=&#34;https://github.com/Homebrew/homebrew-core/commit/6cefefcbba24e4a1093dfd2e189f2cb01170e45f&#34;&gt;removed TFLint&lt;/a&gt;&#xA;after its executable became subject to both MPL 2.0 and BUSL 1.1. Upstream now&#xA;maintains &lt;a href=&#34;https://github.com/terraform-linters/homebrew-tap&#34;&gt;&lt;code&gt;terraform-linters/tap&lt;/code&gt;&lt;/a&gt;&#xA;as a cask, so migrate the old formula instead of merely reinstalling it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew uninstall tflint&#xA;Uninstalling /opt/homebrew/Cellar/tflint/0.61.0... (8 files, 51.8MB)&#xA;&#xA;% brew install terraform-linters/tap/tflint&#xA;==&amp;gt; Tapping terraform-linters/tap&#xA;Tapped 1 cask (15 files, 14.7KB).&#xA;==&amp;gt; Installing Cask tflint&#xA;==&amp;gt; Linking Binary &amp;#39;tflint&amp;#39; to &amp;#39;/opt/homebrew/bin/tflint&amp;#39;&#xA;🍺  tflint was successfully installed!&#xA;&#xA;% tflint --version&#xA;TFLint version 0.64.0&#xA;&amp;#43; ruleset.terraform (0.15.0-bundled)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;flameshot--shottr&#34;&gt;&#xA;  Flameshot → Shottr&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/#flameshot--shottr&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://formulae.brew.sh/cask/flameshot&#34;&gt;Flameshot cask&lt;/a&gt; was deprecated&#xA;because it does not pass macOS Gatekeeper checks. I replaced it with&#xA;&lt;a href=&#34;https://shottr.cc/&#34;&gt;Shottr&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew uninstall --cask flameshot&#xA;==&amp;gt; Uninstalling Cask flameshot&#xA;==&amp;gt; Removing App &amp;#39;/Applications/flameshot.app&amp;#39;&#xA;==&amp;gt; Purging files for version 14.0.0,14.0 of Cask flameshot&#xA;&#xA;% brew install --cask shottr&#xA;==&amp;gt; Installing Cask shottr&#xA;==&amp;gt; Moving App &amp;#39;Shottr.app&amp;#39; to &amp;#39;/Applications/Shottr.app&amp;#39;&#xA;🍺  shottr was successfully installed!&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The corresponding &lt;code&gt;Brewfile&lt;/code&gt; &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/commit/79cd3dd4eb448d4a3bc5f66a124fd60d3f131d58&#34;&gt;change&lt;/a&gt; keeps the migration reproducible:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;&amp;#43;tap &amp;#34;terraform-linters/tap&amp;#34;&#xA;&#xA;-brew &amp;#34;tflint&amp;#34;&#xA;&#xA;-cask &amp;#34;flameshot&amp;#34;&#xA;&amp;#43;cask &amp;#34;shottr&amp;#34;&#xA;&amp;#43;cask &amp;#34;tflint&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Both offending entries are now gone:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew doctor 2&amp;gt;&amp;amp;1 | grep -E &amp;#39;flameshot|tflint|deprecated|no formulae&amp;#39;&#xA;&#xA;% brew list --cask | grep -E &amp;#39;^(flameshot|shottr|tflint)$&amp;#39;&#xA;shottr&#xA;tflint&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: homebrew: replace deprecated and orphaned packages&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>terraform: fileset does not respect gitignore
      </title>
      <link>https://perrotta.dev/2026/08/terraform-fileset-does-not-respect-gitignore/</link>
      <pubDate>Fri, 07 Aug 2026 11:27:13 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>aws</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2026/08/terraform-fileset-does-not-respect-gitignore/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: a terraform project planned clean locally (&lt;code&gt;terraform plan&lt;/code&gt;), but CI insisted on a diff. Every apply fixed it for one side and broke&#xA;it for the other, back-and-forth.&lt;/p&gt;&#xA;&lt;p&gt;The culprit was a &lt;code&gt;null_resource&lt;/code&gt; that rebuilds an AWS Lambda for a NodeJS app&#xA;bundle whenever its sources change, keyed on a hash of the whole directory:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;resource &amp;#34;null_resource&amp;#34; &amp;#34;lambda_build&amp;#34; {&#xA;  triggers = {&#xA;    dir_sha = sha256(join(&amp;#34;&amp;#34;, [for f in sort(fileset(&amp;#34;${path.module}/lambda&amp;#34;, &amp;#34;**&amp;#34;)) : &amp;#34;${f}:${filesha256(&amp;#34;${path.module}/lambda/${f}&amp;#34;)}&amp;#34;]))&#xA;  }&#xA;&#xA;  provisioner &amp;#34;local-exec&amp;#34; {&#xA;    command = &amp;#34;cd ${path.module}/lambda &amp;amp;&amp;amp; npm ci &amp;amp;&amp;amp; npm run build&amp;#34;&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/functions/fileset&#34;&gt;&lt;code&gt;fileset()&lt;/code&gt;&lt;/a&gt;&#xA;walks the filesystem. It has no idea &lt;code&gt;.gitignore&lt;/code&gt; exists. So &lt;code&gt;node_modules/&lt;/code&gt;&#xA;and &lt;code&gt;dist/&lt;/code&gt;, present locally after a build, but absent in a fresh CI checkout,&#xA;creates the discrepancy.&lt;/p&gt;&#xA;&lt;p&gt;A minimal reproduction test case, with &lt;code&gt;node_modules/&lt;/code&gt; and &lt;code&gt;dist/&lt;/code&gt; gitignored:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git ls-files lambda | wc -l&#xA;2&#xA;% echo &amp;#39;sha256(join(&amp;#34;&amp;#34;, [for f in sort(fileset(&amp;#34;./lambda&amp;#34;, &amp;#34;**&amp;#34;)) : &amp;#34;${f}:${filesha256(&amp;#34;./lambda/${f}&amp;#34;)}&amp;#34;]))&amp;#39; | terraform console&#xA;&amp;#34;a0af67e204ae2b73a7452d0c2755be1f665b377b5d49e7cc520bd31607adb832&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then with &lt;code&gt;npm ci &amp;amp;&amp;amp; npm run build&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% find lambda -type f | wc -l&#xA;4&#xA;% git status --porcelain | wc -l&#xA;0&#xA;% echo &amp;#39;sha256(join(&amp;#34;&amp;#34;, [for f in sort(fileset(&amp;#34;./lambda&amp;#34;, &amp;#34;**&amp;#34;)) : &amp;#34;${f}:${filesha256(&amp;#34;./lambda/${f}&amp;#34;)}&amp;#34;]))&amp;#39; | terraform console&#xA;&amp;#34;728a40d04abc2b8292d57465d1f037f833a67d4cc0607a6c64cef487292d9be6&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Git says nothing changed; Terraform says everything did (ugh!).&lt;/p&gt;&#xA;&lt;p&gt;That&amp;rsquo;s the issue. CI plans one value, my laptop plans the other, and each&#xA;apply sets up the next one to &amp;ldquo;detect drift&amp;rdquo; again.&lt;/p&gt;&#xA;&lt;p&gt;Nothing in AWS had changed. The deployed artifact was byte-identical the whole&#xA;time; only the build trigger churned.&lt;/p&gt;&#xA;&lt;p&gt;The fix is to hash the build &lt;em&gt;inputs&lt;/em&gt;, not the build &lt;em&gt;directory&lt;/em&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;dir_sha = sha256(join(&amp;#34;&amp;#34;, [for f in sort(setunion(&#xA;  fileset(&amp;#34;${path.module}/lambda&amp;#34;, &amp;#34;src/**&amp;#34;),&#xA;  [&amp;#34;package.json&amp;#34;, &amp;#34;package-lock.json&amp;#34;],&#xA;)) : &amp;#34;${f}:${filesha256(&amp;#34;${path.module}/lambda/${f}&amp;#34;)}&amp;#34;]))&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Same value on both sides, and still sensitive to anything that actually matters:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% # dirty tree, after npm ci&#xA;&amp;#34;a0af67e204ae2b73a7452d0c2755be1f665b377b5d49e7cc520bd31607adb832&amp;#34;&#xA;% # pristine `git archive` checkout&#xA;&amp;#34;a0af67e204ae2b73a7452d0c2755be1f665b377b5d49e7cc520bd31607adb832&amp;#34;&#xA;% # after editing lambda/src/handler.ts (hypotetically)&#xA;&amp;#34;53ef69e287a65f9785cf05985942a5c62a8175348c91c7d9c83039bfc31b8714&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;terraform console&lt;/code&gt; is the right tool here — it evaluates an expression against&#xA;the real filesystem without an apply, so both variants can be diffed against a&#xA;pristine &lt;code&gt;git archive&lt;/code&gt; tree and a working one before committing anything.&lt;/p&gt;&#xA;&lt;p&gt;The general shape: any &lt;code&gt;fileset()&lt;/code&gt; over a directory that also holds build&#xA;artifacts, virtualenvs, or vendored dependencies is environment-dependent, and&#xA;will read as permanent drift for exactly as long as nobody looks closely.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: terraform: fileset does not respect gitignore&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/aws/&#34;&gt;#aws&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>dual-review: address review findings automatically
      </title>
      <link>https://perrotta.dev/2026/08/dual-review-address-review-findings-automatically/</link>
      <pubDate>Wed, 05 Aug 2026 14:01:30 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>claude</category>
      <category>coding</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/08/dual-review-address-review-findings-automatically/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/06/claude-code-ship-your-skills-as-a-plugin-marketplace/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;My&#xA;&lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/dual-review/SKILL.md&#34;&gt;&lt;code&gt;dual-review&lt;/code&gt;&lt;/a&gt;&#xA;skill runs two independent LLM reviewers over a commit / diff / PR, validates&#xA;every finding against the code, and hands back one action plan — Blockers,&#xA;Important, Suggestions.&lt;/p&gt;&#xA;&lt;p&gt;Recently I added a flag argument that closes the loop:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt; argument-hint: &amp;#34;[branch|staged|all] [--pr NUMBER] [--post]&amp;#34;&#xA;&amp;#43;argument-hint: &amp;#34;[branch|staged|all] [--pr NUMBER] [--post] [--address]&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt; - `--post`: post final action plan to target pull request after showing it; never post otherwise&#xA;&amp;#43;- `--address`: after showing (and, if requested, posting) the action plan, fix Blockers and Important findings directly in the reviewed tree; never fix Suggestions without explicit confirmation&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;&amp;#43;## 7. Address Findings (only with `--address`)&#xA;&amp;#43;&#xA;&amp;#43;Skip this section entirely unless `--address` was requested.&#xA;&amp;#43;&#xA;&amp;#43;1. Work in the same tree that was reviewed (existing worktree or user&amp;#39;s checkout for `staged`/`all`); never create a second copy of the changes.&#xA;&amp;#43;2. Address every Blocker and every Important finding. For Suggestions, ask the user which (if any) to apply; default to none.&#xA;&amp;#43;3. For each finding: re-read the exact `file:line` locations, apply the `Fix:` recommendation, and fix every sibling location listed under that finding, not just the first instance.&#xA;&amp;#43;4. If a finding is ambiguous, requires a design decision, or the recommended fix turns out to be wrong once you&amp;#39;re in the code, stop and ask the user instead of guessing.&#xA;&amp;#43;5. After edits, rerun any locally available checks that cover the change (tests, linters, type-checks) before treating a finding as resolved.&#xA;&amp;#43;6. Stage only the files touched while addressing findings. Commit with a message describing which findings were fixed (e.g. `address dual-review findings`), listing them briefly in the body.&#xA;&amp;#43;7. If scope was `branch`/`--pr`, push to the branch. If the action plan was posted (`--post`), leave a short follow-up comment noting the findings were addressed and pushed; do not repost the full plan.&#xA;&amp;#43;8. Report: which findings were fixed, which were skipped and why, and which need user input.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The important line is the second one: Suggestions never get auto-applied,&#xA;only Blockers and Important.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;/dual-review --post --address&lt;/code&gt; now reviews, posts the plan, fixes&#xA;what&amp;rsquo;s fixable, and pushes.&lt;/p&gt;&#xA;&lt;p&gt;Why two review(er)s instead of one? Because LLMs are non-deterministic.&lt;/p&gt;&#xA;&lt;p&gt;Why not more than two review(er)s then? Tokens ($$$) and time. It&amp;rsquo;s a balance.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: dual-review: address review findings automatically&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/meta/&#34;&gt;#meta&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: remove redundant github MCP
      </title>
      <link>https://perrotta.dev/2026/08/pi-remove-redundant-github-mcp/</link>
      <pubDate>Mon, 03 Aug 2026 18:47:51 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/08/pi-remove-redundant-github-mcp/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2025/10/mcp-model-context-protocol-considered-harmful/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;ve finally removed the GitHub MCP server from my &lt;a href=&#34;https://pi.dev/&#34;&gt;Pi&lt;/a&gt;&#xA;configuration, after piloting it for a while with &lt;a href=&#34;https://github.com/nicobailon/pi-mcp-adapter&#34;&gt;&lt;code&gt;pi-mcp-adapter&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;GitHub already has a mature CLI (&lt;code&gt;gh&lt;/code&gt;) installed and authenticated:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% gh --version&#xA;gh version 2.97.0 (2026-07-31)&#xA;https://github.com/cli/cli/releases/tag/v2.97.0&#xA;&#xA;% gh auth status&#xA;github.com&#xA;  ✓ Logged in to github.com account thiagowfx (keyring)&#xA;  - Active account: true&#xA;  - Git operations protocol: ssh&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;gh&lt;/code&gt; handles pull requests, issues, workflows, and releases directly. Anything&#xA;else remains available through &lt;code&gt;gh api&lt;/code&gt;, including GraphQL. The MCP server added&#xA;another interface without adding capability.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt; {&#xA;   &amp;#34;mcpServers&amp;#34;: {&#xA;-    &amp;#34;github&amp;#34;: {&#xA;-      &amp;#34;auth&amp;#34;: &amp;#34;bearer&amp;#34;,&#xA;-      &amp;#34;bearerToken&amp;#34;: &amp;#34;!gh auth token&amp;#34;,&#xA;-      &amp;#34;url&amp;#34;: &amp;#34;https://api.githubcopilot.com/mcp&amp;#34;&#xA;-    },&#xA;     &amp;#34;slack-mcp&amp;#34;: {&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;MCP remains useful for APIs without a better (or any) CLI, but it should be&#xA;avoided wherever possible.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: remove redundant github MCP&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>DNS: negative cache behind a local resolver
      </title>
      <link>https://perrotta.dev/2026/07/dns-negative-cache-behind-a-local-resolver/</link>
      <pubDate>Fri, 31 Jul 2026 14:02:20 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/07/dns-negative-cache-behind-a-local-resolver/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: a newly created Route 53 record resolved through public DNS,&#xA;but &lt;del&gt;Chrome&lt;/del&gt; Brave and macOS still returned &lt;code&gt;DNS_PROBE_POSSIBLE&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;It smells like a DNS caching issue.&lt;/p&gt;&#xA;&lt;p&gt;Flushing &lt;code&gt;mDNSResponder&lt;/code&gt; did nothing though.&lt;/p&gt;&#xA;&lt;p&gt;The system resolver was &lt;strong&gt;Tailscale&lt;/strong&gt;&amp;rsquo;s local DNS&#xA;proxy, so I asked that resolver rather than trusting &lt;code&gt;dig&lt;/code&gt;&amp;rsquo;s default path:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% dig &amp;#43;time=3 &amp;#43;tries=1 @100.100.100.100 kargo.tools.example-corp.net A &amp;#43;noall &amp;#43;answer &amp;#43;authority &amp;#43;comments&#xA;; &amp;lt;&amp;lt;&amp;gt;&amp;gt; DiG 9.10.6 &amp;lt;&amp;lt;&amp;gt;&amp;gt; &amp;#43;time=3 &amp;#43;tries=1 @100.100.100.100 kargo.tools.example-corp.net A &amp;#43;noall &amp;#43;answer &amp;#43;authority &amp;#43;comments&#xA;; (1 server found)&#xA;;; -&amp;gt;&amp;gt;HEADER&amp;lt;&amp;lt;- opcode: QUERY, status: NXDOMAIN, id: 13683&#xA;;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1&#xA;&#xA;;; AUTHORITY SECTION:&#xA;tools.example-corp.net. 225 IN SOA ns-1609.awsdns-09.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Route 53&amp;rsquo;s authoritative nameserver and several public resolvers already had it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% for resolver in 1.1.1.1 1.1.1.2 8.8.8.8; do&#xA;    printf &amp;#39;%-16s &amp;#39; &amp;#34;$resolver&amp;#34;&#xA;    dig &amp;#43;time=2 &amp;#43;tries=1 &amp;#43;short @&amp;#34;$resolver&amp;#34; kargo.tools.example-corp.net A | paste -sd, -&#xA;  done&#xA;1.1.1.1          192.0.2.11,198.51.100.50,203.0.113.95&#xA;1.1.1.2          192.0.2.11,203.0.113.95,198.51.100.50&#xA;8.8.8.8          203.0.113.95,198.51.100.50,192.0.2.11&#xA;&#xA;% dig &amp;#43;short @ns-1609.awsdns-09.co.uk kargo.tools.example-corp.net A&#xA;192.0.2.11&#xA;203.0.113.95&#xA;198.51.100.50&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Tailscale had cached the previous &lt;code&gt;NXDOMAIN&lt;/code&gt; response for a couple of minutes.&#xA;Its cache is not macOS&amp;rsquo;s cache; restarting &lt;code&gt;mDNSResponder&lt;/code&gt; cannot clear it.&#xA;Waiting out that TTL fixed the web browser lookup.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: DNS: negative cache behind a local resolver&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>git: repair unreachable object corruption
      </title>
      <link>https://perrotta.dev/2026/07/git-repair-unreachable-object-corruption/</link>
      <pubDate>Wed, 29 Jul 2026 22:16:46 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/07/git-repair-unreachable-object-corruption/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;git fsck&lt;/code&gt; failed in a linked worktree, even though every&#xA;branch and reflog remained intact.&lt;/p&gt;&#xA;&lt;p&gt;The initial diagnosis was noisy: missing blobs, hundreds of dangling objects, and&#xA;an invalid commit graph:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;missing blob 97f9a2ed29f84e47848244c2d2626b2536a33c55&#xA;error: Could not read 003fb43a3aa361b52f2ab3f27bdabcb9b5137103&#xA;failed to parse commit 003fb43a3aa361b52f2ab3f27bdabcb9b5137103 from object database for commit-graph&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Worktrees have their own administrative directory but share an object database:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git rev-parse --git-dir&#xA;.git/worktrees/thiagowfx-prm-revenue-tagging&#xA;% git rev-parse --git-common-dir&#xA;.git&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I checked every missing blob against objects reachable from refs and reflogs. None&#xA;were reachable. Before removing them, I backed up objects, commit graphs, and index:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% common=$(git rev-parse --git-common-dir)&#xA;% backup=&amp;#34;$common/repair-backup-20260729221124&amp;#34;&#xA;% mkdir -p &amp;#34;$backup&amp;#34;&#xA;% cp &amp;#34;$(git rev-parse --git-path index)&amp;#34; &amp;#34;$backup/worktree-index&amp;#34;&#xA;% ditto &amp;#34;$common/objects&amp;#34; &amp;#34;$backup/objects-before-gc&amp;#34;&#xA;% mv &amp;#34;$common/objects/info/commit-graphs&amp;#34; &amp;#34;$backup/commit-graphs&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The index also had an invalid cache-tree entry. Rebuilding it from &lt;code&gt;HEAD&lt;/code&gt; preserves&#xA;working-tree files; comparing its old entries found no staged changes:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git read-tree HEAD&#xA;% GIT_INDEX_FILE=&amp;#34;$backup/worktree-index&amp;#34; git ls-files --stage &amp;gt; /tmp/index-before-repair&#xA;% join -t $&amp;#39;\t&amp;#39; -v 1 \&#xA;    &amp;lt;(awk &amp;#39;{print $4 &amp;#34;\t&amp;#34; $1}&amp;#39; /tmp/index-before-repair | sort) \&#xA;    &amp;lt;(git ls-files --stage | awk &amp;#39;{print $4 &amp;#34;\t&amp;#34; $1}&amp;#39; | sort)&#xA;% # no output&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then Git could discard unreachable corrupt objects and rebuild its graph:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git gc --prune=now&#xA;% git fsck --full --strict&#xA;% echo $?&#xA;0&#xA;% git fsck --full --no-reflogs --strict &amp;gt;/tmp/fsck&#xA;% echo $?&#xA;0&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The backup remains under &lt;code&gt;.git/repair-backup-20260729221124&lt;/code&gt;. &lt;code&gt;fsck&lt;/code&gt; is clean;&#xA;recovery material remains available.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s very weird to experience this kind of error. We&amp;rsquo;re not used to it.&lt;/p&gt;&#xA;&lt;p&gt;The one trick to remember from this post is &lt;code&gt;git fsck&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: git: repair unreachable object corruption&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>obsidian: logseq-compatible tasks
      </title>
      <link>https://perrotta.dev/2026/07/obsidian-logseq-compatible-tasks/</link>
      <pubDate>Wed, 29 Jul 2026 14:34:39 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>dev</category>
      <category>pkm</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/07/obsidian-logseq-compatible-tasks/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/logseq-obsidian/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: preserve Logseq&amp;rsquo;s task syntax while opening the same markdown&#xA;files in Obsidian.&lt;/p&gt;&#xA;&lt;p&gt;My migration script originally converted every task into Obsidian&amp;rsquo;s checkbox&#xA;syntax:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;- TODO buy milk     -&amp;gt;  - [ ] buy milk&#xA;- DOING ...         -&amp;gt;  - [/] ...&#xA;- DONE ...          -&amp;gt;  - [x] ...&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Obsidian itself does not recognize the left-hand side as tasks. The&#xA;&lt;a href=&#34;https://github.com/scross01/obsidian-todoseq&#34;&gt;TODOseq&lt;/a&gt; community plug-in does:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;- TODO buy milk&#xA;- DOING ...&#xA;- DONE ...&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It keeps the file unchanged, adds clickable task states and a task list to&#xA;Obsidian, and remains compatible with Logseq.&lt;/p&gt;&#xA;&lt;p&gt;Before granting a community plug-in write access to my vault, I inspected the&#xA;current release with the LLM™:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git clone --depth=1 https://github.com/scross01/obsidian-todoseq.git /tmp/obsidian-todoseq-audit&#xA;Cloning into &amp;#39;/tmp/obsidian-todoseq-audit&amp;#39;...&#xA;% cd /tmp/obsidian-todoseq-audit&#xA;% git rev-parse --short HEAD&#xA;4ba5e81&#xA;% jq &amp;#39;{id, version, minAppVersion, description}&amp;#39; manifest.json&#xA;{&#xA;  &amp;#34;id&amp;#34;: &amp;#34;todoseq&amp;#34;,&#xA;  &amp;#34;version&amp;#34;: &amp;#34;0.18.1&amp;#34;,&#xA;  &amp;#34;minAppVersion&amp;#34;: &amp;#34;1.11.0&amp;#34;,&#xA;  &amp;#34;description&amp;#34;: &amp;#34;Lightweight keyword-based task tracker using Logseq style keywords.&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A source scan found no network requests, telemetry, process execution, or&#xA;dynamic code evaluation&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/obsidian-logseq-compatible-tasks/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;It does read and modify notes through Obsidian&amp;rsquo;s vault API, as expected:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% rg -n &amp;#39;vault\.(read|modify)&amp;#39; src/services/editor-controller.ts | head -4&#xA;1130:        const currentContent = await this.plugin.app.vault.read(todayNote);&#xA;1135:        await this.plugin.app.vault.modify(todayNote, newContent);&#xA;1283:        const todayContent = await this.plugin.app.vault.read(todayNote);&#xA;1286:        await this.plugin.app.vault.modify(todayNote, newTodayContent);&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Low risk, and no task-format conversion needed.&lt;/p&gt;&#xA;&lt;p&gt;This should make it easier for me to adapt my LogSeq muscle memory to Obsidian,&#xA;even if at the cost of enabling community plug-ins.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;&lt;em&gt;Hope is not a strategy&lt;/em&gt;.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/obsidian-logseq-compatible-tasks/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: obsidian: logseq-compatible tasks&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: change directories mid-session
      </title>
      <link>https://perrotta.dev/2026/07/pi-change-directories-mid-session/</link>
      <pubDate>Wed, 29 Jul 2026 14:13:03 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-change-directories-mid-session/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Today I learned&lt;/strong&gt;: &lt;a href=&#34;https://pi.dev/&#34;&gt;pi&lt;/a&gt; 0.82.1 cannot change its working&#xA;directory mid-session.&lt;/p&gt;&#xA;&lt;p&gt;The interactive shell syntax looks tempting, but &lt;code&gt;!&lt;/code&gt; and &lt;code&gt;!!&lt;/code&gt; run commands in a&#xA;child process. &lt;code&gt;cd&lt;/code&gt; ends with that process; Pi keeps the session&amp;rsquo;s original cwd.&#xA;There is no built-in &lt;code&gt;/cd&lt;/code&gt; either:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi --version&#xA;0.82.1&#xA;% docs=&amp;#34;$(brew --prefix pi-coding-agent)/libexec/lib/node_modules/@earendil-works/pi-coding-agent/docs&amp;#34;&#xA;% rg -n &amp;#39;ctx\.(newSession|switchSession|changeCwd|switchCwd)&amp;#39; &amp;#34;$docs/extensions.md&amp;#34;&#xA;1109:### ctx.newSession(options?)&#xA;1117:const result = await ctx.newSession({&#xA;1187:### ctx.switchSession(sessionPath, options?)&#xA;1192:const result = await ctx.switchSession(&amp;#34;/path/to/session.jsonl&amp;#34;, {&#xA;1220:      await ctx.switchSession(choice, {&#xA;1247:    await ctx.newSession({&#xA;1262:    await ctx.newSession({&#xA;% rg -n &amp;#39;^\| `/[^|]&amp;#43;`&amp;#39; &amp;#34;$docs/usage.md&amp;#34; | tail -5&#xA;56:| `/share` | Upload as private GitHub gist with shareable HTML link |&#xA;57:| `/reload` | Reload keybindings, extensions, skills, prompts, themes, and context files |&#xA;58:| `/hotkeys` | Show all keyboard shortcuts |&#xA;59:| `/changelog` | Display version history |&#xA;60:| `/quit` | Quit pi |&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This has been requested several times upstream:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% for n in 2102 2992 3921 4423 6997; do&#xA;    gh issue view &amp;#34;$n&amp;#34; --repo earendil-works/pi \&#xA;      --json number,title,state,stateReason \&#xA;      --jq &amp;#39;[.number, .title, .state, .stateReason] | @tsv&amp;#39;&#xA;  done&#xA;2102&#x9;Allow extensions to change cwd at runtime&#x9;CLOSED&#x9;COMPLETED&#xA;2992&#x9;Allow session CWD to be changed&#x9;CLOSED&#x9;COMPLETED&#xA;3921&#x9;Feature request: ExtensionCommandContext.changeCwd() for lightweight cwd switching&#x9;CLOSED&#x9;NOT_PLANNED&#xA;4423&#x9;Add command API to switch cwd&#x9;CLOSED&#x9;COMPLETED&#xA;6997&#x9;Windows Bash inside Pi not working properly&#x9;CLOSED&#x9;NOT_PLANNED&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The exact &lt;code&gt;/cd&lt;/code&gt; proposal lives in&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/issues/3921&#34;&gt;#3921&lt;/a&gt;. The maintainer&#xA;responses on &lt;a href=&#34;https://github.com/earendil-works/pi/issues/2102&#34;&gt;#2102&lt;/a&gt;,&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/issues/2992&#34;&gt;#2992&lt;/a&gt;, and&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/issues/4423&#34;&gt;#4423&lt;/a&gt; capture the trade-off:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;#2102 @badlogic: use case makes sense, however, it can be done by overriding the built-in tools to support cwd changes.&#xA;#2992 @badlogic: setting a new cwd in a session is not great, as a bunch of things need to be rewired. currently unsupported.&#xA;#4423 @badlogic: this is part of the big refactor work and will be available in the new extension api.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That last API still is not exposed in 0.82.1. Changing cwd means rebuilding tools,&#xA;project settings, extensions, skills, context files, autocomplete, footer state,&#xA;and session storage—not merely calling &lt;code&gt;process.chdir()&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;For now, the boring path is the supported one:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;/quit&#xA;% cd ~/Workspace/perrotta.dev&#xA;% pi&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Session cwd is a runtime boundary. Oh well.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: change directories mid-session&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>drskill: lint claude skills
      </title>
      <link>https://perrotta.dev/2026/07/drskill-lint-claude-skills/</link>
      <pubDate>Tue, 28 Jul 2026 19:09:34 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>claude</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/07/drskill-lint-claude-skills/</guid>
      <description>&lt;p&gt;♠ I found drskill through Drew Breunig&amp;rsquo;s post about&#xA;&lt;a href=&#34;https://www.dbreunig.com/2026/07/24/manage-your-agent-s-loadout-with-dr-skill.html&#34;&gt;managing an agent&amp;rsquo;s loadout&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: lint skills bundled in a Claude Code plugin with&#xA;&lt;a href=&#34;https://github.com/dbreunig/drskill&#34;&gt;&lt;code&gt;drskill&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Running it through &lt;code&gt;uvx&lt;/code&gt; is easy enough:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% uvx drskill scan --detailed&#xA;drskill scan — 3 harnesses (10 more empty), 9 skills&#xA;...&#xA;0 errors, 5 warnings (5 new) · token counts are approximate&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;But none of this repository&amp;rsquo;s 11 skills appeared. The&#xA;&lt;a href=&#34;https://github.com/dbreunig/drskill#known-limitations&#34;&gt;known limitation&lt;/a&gt; explains&#xA;why:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Claude Code skills bundled inside plugins are not scanned yet.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I projected the plugin&amp;rsquo;s skill directory into the plain &lt;code&gt;.claude/skills&lt;/code&gt; layout&#xA;that drskill understands via &lt;code&gt;Justfile&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-just&#34;&gt;# Check skills with drskill&#xA;doctor:&#xA;    #!/usr/bin/env bash&#xA;    set -euo pipefail&#xA;    tmp=$(mktemp -d)&#xA;    trap &amp;#39;trash &amp;#34;$tmp&amp;#34;&amp;#39; EXIT&#xA;    mkdir -p &amp;#34;$tmp/project/.claude/skills&amp;#34; &amp;#34;$tmp/home&amp;#34;&#xA;    for skill in &amp;#34;$PWD&amp;#34;/plugins/thiagowfx/skills/*; do&#xA;      ln -s &amp;#34;$skill&amp;#34; &amp;#34;$tmp/project/.claude/skills/$(basename &amp;#34;$skill&amp;#34;)&amp;#34;&#xA;    done&#xA;    uv_cache=$(uv cache dir)&#xA;    cd &amp;#34;$tmp/project&amp;#34;&#xA;    HOME=&amp;#34;$tmp/home&amp;#34; UV_CACHE_DIR=&amp;#34;$uv_cache&amp;#34; uvx drskill scan --ci --harness claude-code&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The temporary &lt;code&gt;HOME&lt;/code&gt; excludes unrelated global skills. Reusing uv&amp;rsquo;s cache avoids&#xA;redownloading drskill there. &lt;code&gt;--ci&lt;/code&gt; makes warnings fail the recipe.&lt;/p&gt;&#xA;&lt;p&gt;The first isolated scan found four warnings:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;drskill scan — 1 harness, 11 skills&#xA;&#xA;WARNINGS&#xA;  [e91d] new frontmatter-angle-brackets: &amp;#39;bloggify&amp;#39; ...&#xA;  [a551] new frontmatter-angle-brackets: &amp;#39;gha&amp;#39; ...&#xA;  [6335] new frontmatter-angle-brackets: &amp;#39;new-apkbuild&amp;#39; ...&#xA;  [206e] new missing-activation: 2 skills never say when to use them ...&#xA;&#xA;0 errors, 4 warnings (4 new) · token counts are approximate&#xA;exit=2&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The fixes were small: remove angle brackets from &lt;code&gt;argument-hint&lt;/code&gt; values and add&#xA;explicit activation conditions to &lt;code&gt;catchup&lt;/code&gt; and &lt;code&gt;handoff&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;-argument-hint: &amp;#34;&amp;lt;url&amp;gt;&amp;#34;&#xA;&amp;#43;argument-hint: &amp;#34;url&amp;#34;&#xA;&#xA;-description: Create portable handoff document for another agent or harness to continue current work.&#xA;&amp;#43;description: Create a portable handoff document for another agent or harness to continue current work. Use when the user asks for a handoff, continuation brief, or context transfer.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Now the repository has its own linter:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% just doctor&#xA;drskill scan — 1 harness, 11 skills&#xA;&#xA;No findings.&#xA;0 errors, 0 warnings · token counts are approximate&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Restoring &lt;code&gt;&amp;lt;url&amp;gt;&lt;/code&gt; temporarily made &lt;code&gt;just doctor&lt;/code&gt; exit 2 again, so future warnings&#xA;will break the check. I should add it to CI at some point.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: drskill: lint claude skills&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: block dangerous commands syntax-aware
      </title>
      <link>https://perrotta.dev/2026/07/pi-block-dangerous-commands-syntax-aware/</link>
      <pubDate>Tue, 28 Jul 2026 10:57:06 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/07/pi-block-dangerous-commands-syntax-aware/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: block destructive shell commands issued by&#xA;&lt;a href=&#34;https://pi.dev/&#34;&gt;pi&lt;/a&gt; without resorting to grepping shell source with regular&#xA;expressions.&lt;/p&gt;&#xA;&lt;p&gt;My first guard was a direct port of an older hook in Claude Code:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const BLOCKED_COMMANDS: ReadonlyArray&amp;lt;{ pattern: RegExp; reason: string }&amp;gt; = [&#xA;  { pattern: /terraform\s&amp;#43;apply/, reason: &amp;#34;terraform apply is blocked - use terraform plan first&amp;#34; },&#xA;  { pattern: /terraform\s&amp;#43;destroy/, reason: &amp;#34;terraform destroy is blocked for safety&amp;#34; },&#xA;  { pattern: /git\s&amp;#43;reset\s&amp;#43;.*--hard/, reason: &amp;#34;git reset --hard is blocked - discards changes irreversibly&amp;#34; },&#xA;];&#xA;&#xA;const blocked = BLOCKED_COMMANDS.find(({ pattern }) =&amp;gt; pattern.test(command));&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This matched harmless strings such as &lt;code&gt;echo &#39;terraform destroy&#39;&lt;/code&gt;, while shell&#xA;quoting, wrappers, substitutions, and reordered options kept opening holes.&#xA;Regexes aren&amp;rsquo;t robust in this context.&lt;/p&gt;&#xA;&lt;p&gt;Enter ASTs! Shell source could leverage a shell parser.&lt;/p&gt;&#xA;&lt;p&gt;I switched the extension to the Bash grammar from&#xA;&lt;a href=&#34;https://github.com/tree-sitter/tree-sitter-bash&#34;&gt;&lt;code&gt;tree-sitter-bash&lt;/code&gt;&lt;/a&gt;, loaded&#xA;through the WASM Tree-sitter runtime:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;await Parser.init();&#xA;const languagePath = require.resolve(&amp;#34;tree-sitter-bash/tree-sitter-bash.wasm&amp;#34;);&#xA;const language = await Language.load(languagePath);&#xA;return new Parser().setLanguage(language);&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://tree-sitter.github.io/&#34;&gt;Tree sitter&lt;/a&gt; is an amazing parser.&lt;/p&gt;&#xA;&lt;p&gt;The policy now works on executable names and arguments:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;if (executable === &amp;#34;rm&amp;#34;) {&#xA;  const recursive = staticArguments.some(&#xA;    (argument) =&amp;gt; argument === &amp;#34;--recursive&amp;#34; || hasShortOption(argument, &amp;#34;r&amp;#34;) || hasShortOption(argument, &amp;#34;R&amp;#34;),&#xA;  );&#xA;  const force = staticArguments.some(&#xA;    (argument) =&amp;gt; argument === &amp;#34;--force&amp;#34; || hasShortOption(argument, &amp;#34;f&amp;#34;),&#xA;  );&#xA;  if (recursive &amp;amp;&amp;amp; force) return { command, reason: &amp;#34;rm -rf is blocked for safety&amp;#34; };&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The AST walker also follows command substitutions, heredocs, &lt;code&gt;bash -c&lt;/code&gt;, &lt;code&gt;eval&lt;/code&gt;,&#xA;&lt;code&gt;find -exec&lt;/code&gt;, &lt;code&gt;xargs&lt;/code&gt;, and common wrappers. Comments and quoted data remain data.&#xA;Malformed syntax and dynamic guarded arguments fail closed.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;ℹ tests 48&#xA;ℹ suites 0&#xA;ℹ pass 48&#xA;ℹ fail 0&#xA;ℹ cancelled 0&#xA;ℹ skipped 0&#xA;ℹ todo 0&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Local extensions do not get npm dependencies installed by Pi. I kept &lt;code&gt;index.ts&lt;/code&gt;&#xA;dependency-free: on first load it checks for the parser files, runs a pinned&#xA;installation when needed, then imports the real guard.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const result = await pi.exec(&amp;#34;npm&amp;#34;, [&amp;#34;ci&amp;#34;, &amp;#34;--ignore-scripts&amp;#34;], {&#xA;  cwd: extensionDirectory,&#xA;  timeout: 120_000,&#xA;});&#xA;&#xA;const { default: dangerousCommandGuard } = await import(&amp;#34;./guard.ts&amp;#34;);&#xA;await dangerousCommandGuard(pi);&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Finally, testing the real tool call:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% rm -rf /tmp/foo&#xA;rm -rf is blocked for safety&#xA;% test -f /tmp/foo/marker &amp;amp;&amp;amp; cat /tmp/foo/marker&#xA;guard-test&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The guard blocked the command; &lt;code&gt;/tmp/foo/marker&lt;/code&gt; survived.&lt;/p&gt;&#xA;&lt;p&gt;The source lives in my &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/tree/d917c6b98e8107490bea92367c89d1b05e1de89b/pi/.pi/agent/extensions/dangerous-command-guard&#34;&gt;dotfiles&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: block dangerous commands syntax-aware&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: asynchronous exit summaries
      </title>
      <link>https://perrotta.dev/2026/07/pi-asynchronous-exit-summaries/</link>
      <pubDate>Mon, 27 Jul 2026 18:18:38 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>pkm</category>
      <guid>https://perrotta.dev/2026/07/pi-asynchronous-exit-summaries/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;ctrl+d&lt;/code&gt; in &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt;&#xA;took several seconds to quit.&lt;/p&gt;&#xA;&lt;p&gt;The culprit was &lt;a href=&#34;https://www.npmjs.com/package/pi-memory&#34;&gt;&lt;code&gt;pi-memory&lt;/code&gt;&lt;/a&gt;&amp;rsquo;s&#xA;&lt;code&gt;session_shutdown&lt;/code&gt; handler. It sent one last LLM request to summarize the&#xA;session, wrote its answer to the daily log, then ran &lt;code&gt;qmd update&lt;/code&gt;. Pi awaits&#xA;all shutdown handlers, correctly, so quitting waited for everything:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;pi.on(&amp;#34;session_shutdown&amp;#34;, async (event, ctx) =&amp;gt; {&#xA;  const result = await generateExitSummary(ctx);&#xA;  fs.writeFileSync(filePath, entry, &amp;#34;utf-8&amp;#34;);&#xA;  await runQmdUpdateNow();&#xA;});&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Making that promise fire-and-forget would not work: Pi exits immediately after&#xA;shutdown, and either the in-flight request keeps Node alive or gets killed&#xA;before it can write anything.&lt;/p&gt;&#xA;&lt;p&gt;Instead, I made the shutdown path write a durable job, synchronously:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const job: ExitSummaryJob = {&#xA;  version: 1,&#xA;  id: randomUUID(),&#xA;  createdAt: new Date().toISOString(),&#xA;  date: todayStr(),&#xA;  reason,&#xA;  sessionId: shortSessionId(ctx.sessionManager.getSessionId()),&#xA;  conversationText: truncated.text,&#xA;  truncated: truncated.truncated,&#xA;  totalChars: truncated.totalChars,&#xA;};&#xA;&#xA;const jobPath = path.join(&#xA;  EXIT_SUMMARY_JOBS_DIR,&#xA;  `${job.createdAt.replace(/[:.]/g, &amp;#34;-&amp;#34;)}-${job.id}.json`,&#xA;);&#xA;fs.writeFileSync(jobPath, `${JSON.stringify(job)}\n`, { encoding: &amp;#34;utf-8&amp;#34;, flag: &amp;#34;wx&amp;#34; });&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The next interactive session starts an unref&amp;rsquo;ed worker. It atomically renames&#xA;one job to claim it, generates summary with active model, writes it back to&#xA;original day&amp;rsquo;s daily log, and deletes job. Failed jobs return to queue. A&#xA;stale claimed job retries after ten minutes.&lt;/p&gt;&#xA;&lt;p&gt;I kept upstream package intact by replacing it with a local package, effectively&#xA;forking it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi remove npm:pi-memory&#xA;Removed npm:pi-memory&#xA;% pi install ~/.pi/agent/local/pi-memory-async&#xA;Installed /Users/thiago.perrotta/.pi/agent/local/pi-memory-async&#xA;% pi list&#xA;User packages:&#xA;  local/pi-memory-async&#xA;    /Users/thiago.perrotta/.pi/agent/local/pi-memory-async&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Jobs live in &lt;code&gt;~/.pi/agent/memory/exit-summary-jobs/&lt;/code&gt;. &lt;code&gt;ctrl+d&lt;/code&gt; only writes one&#xA;JSON file now; summary happens during next session, without blocking exits.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;:&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/tree/1f175b77a960a460b4e5cc677d6d6390e0cc0530/pi/.pi/agent/local/pi-memory-async&#34;&gt;pi-memory-async&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: asynchronous exit summaries&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: update pinned npm packages
      </title>
      <link>https://perrotta.dev/2026/07/pi-update-pinned-npm-packages/</link>
      <pubDate>Mon, 27 Jul 2026 14:28:59 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/07/pi-update-pinned-npm-packages/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: pin &lt;a href=&#34;https://pi.dev/&#34;&gt;pi&lt;/a&gt; npm packages without giving up&#xA;convenient updates.&lt;/p&gt;&#xA;&lt;p&gt;My&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/master/pi/.pi/agent/settings.json&#34;&gt;&lt;code&gt;settings.json&lt;/code&gt;&lt;/a&gt;&#xA;used to leave some package versions unpinned, until now:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;     &amp;#34;npm:@heyhuynhgiabuu/pi-diff@0.7.6&amp;#34;,&#xA;-    &amp;#34;npm:@tifan/pi-recap&amp;#34;,&#xA;-    &amp;#34;npm:@tmustier/pi-tab-status&amp;#34;,&#xA;-    &amp;#34;npm:@tintinweb/pi-subagents&amp;#34;&#xA;&amp;#43;    &amp;#34;npm:@tifan/pi-recap@0.4.4&amp;#34;,&#xA;&amp;#43;    &amp;#34;npm:@tmustier/pi-tab-status@0.1.4&amp;#34;,&#xA;&amp;#43;    &amp;#34;npm:@tintinweb/pi-subagents@0.14.3&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Exact versions make package installation reproducible (&amp;amp; safer), but Pi then&#xA;does not automatically pull newer packages.&lt;/p&gt;&#xA;&lt;p&gt;To tackle that, I added a &lt;code&gt;just update-pi&lt;/code&gt; recipe that asks npm for each&#xA;package&amp;rsquo;s current &lt;code&gt;latest&lt;/code&gt; version and writes back an exact pin:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-just&#34;&gt;[doc(&amp;#39;Update pinned Pi npm packages&amp;#39;)]&#xA;[group(&amp;#39;update&amp;#39;)]&#xA;update-pi:&#xA;    #!/usr/bin/env bash&#xA;    set -euo pipefail&#xA;&#xA;    settings=&amp;#34;pi/.pi/agent/settings.json&amp;#34;&#xA;    updates=&amp;#34;{}&amp;#34;&#xA;    while IFS= read -r spec; do&#xA;        if [[ &amp;#34;$spec&amp;#34; =~ ^npm:((@[^/]&amp;#43;/[^@]&amp;#43;)|([^@]&amp;#43;))(@[^@]&amp;#43;)?$ ]]; then&#xA;            package=&amp;#34;${BASH_REMATCH[1]}&amp;#34;&#xA;        else&#xA;            echo &amp;#34;Invalid npm package spec: $spec&amp;#34; &amp;gt;&amp;amp;2&#xA;            exit 1&#xA;        fi&#xA;&#xA;        latest=&amp;#34;$(npm view &amp;#34;$package@latest&amp;#34; version)&amp;#34;&#xA;        pinned=&amp;#34;npm:$package@$latest&amp;#34;&#xA;        updates=&amp;#34;$(jq -cn \&#xA;            --argjson updates &amp;#34;$updates&amp;#34; \&#xA;            --arg spec &amp;#34;$spec&amp;#34; \&#xA;            --arg pinned &amp;#34;$pinned&amp;#34; \&#xA;            &amp;#39;$updates &amp;#43; {($spec): $pinned}&amp;#39;)&amp;#34;&#xA;        echo &amp;#34;✓ $pinned&amp;#34;&#xA;    done &amp;lt; &amp;lt;(jq -r &amp;#39;.packages[] | select(type == &amp;#34;string&amp;#34; and startswith(&amp;#34;npm:&amp;#34;))&amp;#39; &amp;#34;$settings&amp;#34;)&#xA;&#xA;    updated_settings=&amp;#34;$(jq --argjson updates &amp;#34;$updates&amp;#34; \&#xA;        &amp;#39;.packages |= map($updates[.] // .)&amp;#39; &amp;#34;$settings&amp;#34;)&amp;#34;&#xA;    printf &amp;#39;%s\n&amp;#39; &amp;#34;$updated_settings&amp;#34; &amp;gt; &amp;#34;$settings&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It handles every &lt;code&gt;npm:&lt;/code&gt; entry, including scoped packages:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% just update-pi&#xA;✓ npm:@heyhuynhgiabuu/pi-diff@0.7.6&#xA;✓ npm:@heyhuynhgiabuu/pi-pretty@0.6.20&#xA;✓ npm:@tifan/pi-recap@0.4.4&#xA;✓ npm:@tmustier/pi-tab-status@0.1.4&#xA;✓ npm:@tintinweb/pi-subagents@0.14.3&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The aggregate &lt;code&gt;just update&lt;/code&gt; recipe invokes it alongside submodule, prek hook,&#xA;and vendored-file updates.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: update pinned npm packages&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: /btw side-chat
      </title>
      <link>https://perrotta.dev/2026/07/pi-/btw-side-chat/</link>
      <pubDate>Wed, 22 Jul 2026 14:07:54 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-/btw-side-chat/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-auto-retry-stalled-requests/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: mid-task, I want to ask the LLM agent a quick tangential&#xA;question — &amp;ldquo;will this change cause downtime?&amp;rdquo; — without polluting the main&#xA;agent thread.&lt;/p&gt;&#xA;&lt;p&gt;Claude Code has &lt;code&gt;/btw&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-/btw-side-chat/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; for exactly this.&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt; doesn&amp;rsquo;t, but&#xA;&lt;a href=&#34;https://github.com/patriceckhart/pi-btw&#34;&gt;patriceckhart/pi-btw&lt;/a&gt; does, as an&#xA;extension. It opens a side-chat overlay: the LLM gets to see the full main&#xA;thread context and answers, but nothing is written back to the conversation&#xA;history.&lt;/p&gt;&#xA;&lt;p&gt;The upstream extension imports the pre-fork &lt;code&gt;@mariozechner/*&lt;/code&gt; package names,&#xA;which are now deprecated. As such, I use&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;earendil-works/pi&lt;/a&gt;. Rather than trust a&#xA;diff I hadn&amp;rsquo;t read, I reimplemented it against the current API, and&#xA;cross-checked against the bundled &lt;code&gt;examples/extensions/summarize.ts&lt;/code&gt; (the&#xA;canonical example for calling the model from an extension):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;import { complete } from &amp;#34;@earendil-works/pi-ai/compat&amp;#34;;&#xA;import type { ExtensionAPI, SessionEntry } from &amp;#34;@earendil-works/pi-coding-agent&amp;#34;;&#xA;import { convertToLlm, getMarkdownTheme } from &amp;#34;@earendil-works/pi-coding-agent&amp;#34;;&#xA;&#xA;// ...inside the /btw command handler, once at entry:&#xA;const branch = ctx.sessionManager.getBranch();&#xA;const mainMessages = branch&#xA;  .filter((entry): entry is SessionEntry &amp;amp; { type: &amp;#34;message&amp;#34; } =&amp;gt; entry.type === &amp;#34;message&amp;#34;)&#xA;  .map((entry) =&amp;gt; entry.message);&#xA;const mainLlmMessages = convertToLlm(mainMessages);   // frozen snapshot&#xA;const systemPrompt = ctx.getSystemPrompt();&#xA;&#xA;// ...per question, the side-chat calls the model directly:&#xA;const response = await complete(&#xA;  ctx.model!,&#xA;  { systemPrompt, messages: [...mainLlmMessages, ...btwLlmMessages] },&#xA;  { apiKey: auth.apiKey, headers: auth.headers, env: auth.env, signal },&#xA;);&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The key move is what&amp;rsquo;s &lt;em&gt;missing&lt;/em&gt;: the handler never calls &lt;code&gt;pi.sendMessage()&lt;/code&gt;&#xA;or appends anything. It reads the branch once, talks to the model in an&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#custom-ui&#34;&gt;&lt;code&gt;ctx.ui.custom()&lt;/code&gt;&lt;/a&gt;&#xA;overlay, and returns. The main session is untouched, so the next real prompt&#xA;doesn&amp;rsquo;t pay for any of it.&lt;/p&gt;&#xA;&lt;p&gt;Added to &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/1f42eb6f6632f946c23d1278e7e3f18da78d5de8/pi/.pi/agent/extensions/btw.ts&#34;&gt;&lt;code&gt;~/.pi/agent/extensions/btw.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;I am no longer using &lt;a href=&#34;https://archlinux.org/&#34;&gt;Arch&lt;/a&gt;, btw (sad!)&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-/btw-side-chat/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: /btw side-chat&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: auto-retry stalled requests
      </title>
      <link>https://perrotta.dev/2026/07/pi-auto-retry-stalled-requests/</link>
      <pubDate>Wed, 22 Jul 2026 13:45:41 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-auto-retry-stalled-requests/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: sometimes a prompt in &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt;&#xA;just hangs, as if the network dropped. It never recovers on its own — I have to&#xA;hit &lt;code&gt;Esc&lt;/code&gt; and re-prompt.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s 2026, this symptom should be self-recoverable.&lt;/p&gt;&#xA;&lt;p&gt;Pi already has agent-level retry on transient errors (&lt;code&gt;retry.enabled&lt;/code&gt; defaults&#xA;to &lt;code&gt;true&lt;/code&gt;). The catch is &lt;em&gt;when&lt;/em&gt; it fires: a stalled connection isn&amp;rsquo;t an error&#xA;until the HTTP idle timeout trips, and that defaults to five minutes:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;code&gt;httpIdleTimeoutMs&lt;/code&gt; | number | &lt;code&gt;300000&lt;/code&gt; | HTTP header/body idle timeout in&#xA;milliseconds [&amp;hellip;] Set to &lt;code&gt;0&lt;/code&gt; to disable.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;So pi would have &lt;em&gt;eventually&lt;/em&gt; recovered. I was just quitting long before the&#xA;five-minute clock ran out. Who has patience to wait for &lt;strong&gt;5&lt;/strong&gt; minutes?!&lt;/p&gt;&#xA;&lt;p&gt;:∴ Lower the idle timeout to O(seconds) instead:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;httpIdleTimeoutMs&amp;#34;: 30000,&#xA;  &amp;#34;retry&amp;#34;: {&#xA;    &amp;#34;enabled&amp;#34;: true,&#xA;    &amp;#34;maxRetries&amp;#34;: 5,&#xA;    &amp;#34;baseDelayMs&amp;#34;: 2000&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Dropped into &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/commit/aae17ba8d7546901820d7474136a628b16d5feee&#34;&gt;&lt;code&gt;~/.pi/agent/settings.json&lt;/code&gt;&lt;/a&gt;, documented in&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/settings.md&#34;&gt;&lt;code&gt;docs/settings.md&lt;/code&gt;&lt;/a&gt;.&#xA;Now a stall errors out after 30s and &lt;code&gt;pi&lt;/code&gt; retries automatically with exponential&#xA;backoff (2s, 4s, 8s…), up to five attempts. There&amp;rsquo;s no need for &lt;code&gt;Esc&lt;/code&gt; nor&#xA;re-prompting.&lt;/p&gt;&#xA;&lt;p&gt;The idle timer resets on every byte received, so long thinking or a slow stream&#xA;won&amp;rsquo;t trip it — only a genuinely dead connection will. Restart &lt;code&gt;pi&lt;/code&gt; for the&#xA;change to take effect.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: auto-retry stalled requests&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: cycle models backward with ctrl&#43;n
      </title>
      <link>https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/</link>
      <pubDate>Wed, 22 Jul 2026 13:23:53 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-caveman-mode/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt; binds &lt;code&gt;ctrl+p&lt;/code&gt;&#xA;to &lt;code&gt;app.model.cycleForward&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; by default, but has no default binding to cycle&#xA;backward other than &lt;code&gt;shift+ctrl+p&lt;/code&gt;, which is arguably difficult to remember.&lt;/p&gt;&#xA;&lt;p&gt;Every keybinding in pi is namespaced and overridable via&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/1f42eb6f6632f946c23d1278e7e3f18da78d5de8/pi/.pi/agent/keybindings.json&#34;&gt;&lt;code&gt;~/.pi/agent/keybindings.json&lt;/code&gt;&lt;/a&gt;, documented in&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/keybindings.md&#34;&gt;&lt;code&gt;docs/keybindings.md&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;app.model.cycleBackward&amp;#34;: [&#xA;    &amp;#34;shift&amp;#43;ctrl&amp;#43;p&amp;#34;,&#xA;    &amp;#34;ctrl&amp;#43;n&amp;#34;&#xA;  ]&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;ctrl+n&lt;/code&gt; is also the default for &lt;code&gt;app.session.toggleNamedFilter&lt;/code&gt;, but that&#xA;only fires inside the session picker, not while a model selector or the main&#xA;editor has focus.&lt;/p&gt;&#xA;&lt;p&gt;Run &lt;code&gt;/reload&lt;/code&gt; in pi to pick up the keybinding addition without restarting the&#xA;session.&lt;/p&gt;&#xA;&lt;p&gt;∴ &lt;code&gt;ctrl+p&lt;/code&gt;/&lt;code&gt;ctrl+n&lt;/code&gt; now cycle models forward/backward, matching the readline&#xA;(emacs!) history bindings I already have muscle memory for.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;It cycles through models selected in &lt;code&gt;/scoped-models&lt;/code&gt;.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: cycle models backward with ctrl+n&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: caveman mode
      </title>
      <link>https://perrotta.dev/2026/07/pi-caveman-mode/</link>
      <pubDate>Wed, 22 Jul 2026 12:14:47 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/07/pi-caveman-mode/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2025/12/pi/&#34;&gt;Previously&lt;/a&gt;,&#xA;&lt;a href=&#34;https://perrotta.dev/2026/06/caveman-talk-like-caveman-save-tokens/&#34;&gt;previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: adopt &lt;a href=&#34;https://github.com/JuliusBrussee/caveman&#34;&gt;caveman&lt;/a&gt;&#xA;in &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Pi isn&amp;rsquo;t one of the 30+ agents caveman auto-detects. The install matrix&#xA;(&lt;code&gt;INSTALL.md&lt;/code&gt;) lists agent detection rules one by one — &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;cursor&lt;/code&gt;,&#xA;&lt;code&gt;windsurf&lt;/code&gt;, &lt;code&gt;opencode&lt;/code&gt;, &lt;code&gt;openclaw&lt;/code&gt;, etc — and pi isn&amp;rsquo;t in it. Running the&#xA;one-liner installer here would just skip everything silently. Booo!&lt;/p&gt;&#xA;&lt;p&gt;Rather than run someone else&amp;rsquo;s &lt;code&gt;curl | bash&lt;/code&gt; against an agent it wasn&amp;rsquo;t built&#xA;for, &amp;ldquo;&amp;ldquo;&amp;ldquo;I&amp;rdquo;&amp;rdquo;&amp;rdquo; reimplemented the terse-speak part natively, since &lt;code&gt;pi&lt;/code&gt; already ships the&#xA;two primitives needed: &lt;a href=&#34;https://agentskills.io/specification&#34;&gt;skills&lt;/a&gt; and&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md&#34;&gt;extensions&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;A&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/7c495489141fd6036e4be70de9a065c53f7a8394/pi/.pi/agent/extensions/caveman.ts&#34;&gt;skill&lt;/a&gt;&#xA;for &lt;code&gt;/skill:caveman&lt;/code&gt; documentation, and an extension that rewrites the system&#xA;prompt on every turn via &lt;code&gt;pi.on(&amp;quot;before_agent_start&amp;quot;, (event) =&amp;gt; ...)&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const INSTRUCTIONS = `Caveman mode: on. Respond terse. Drop articles (a/an/the), filler (just/really/basically), pleasantries, hedging. Fragments OK. Short synonyms. Technical terms exact. Pattern: [thing] [action] [reason]. [next step]. Example — not: &amp;#34;Sure! I&amp;#39;d be happy to help you with that.&amp;#34; yes: &amp;#34;Bug in auth middleware. Fix:&amp;#34;&#xA;Boundaries, always: code blocks, commit messages, and PR descriptions stay normal full verbosity, never compressed. Drop caveman style for security warnings, irreversible/destructive actions, or when the user seems confused — resume caveman after.`;&#xA;&#xA;export default function (pi: ExtensionAPI) {&#xA;  let on = true; // auto-on by default, every session&#xA;&#xA;  const applyStatus = (ctx: { ui: { setStatus: (k: string, v?: string) =&amp;gt; void } }) =&amp;gt; {&#xA;    ctx.ui.setStatus(&amp;#34;caveman&amp;#34;, on ? &amp;#34;[caveman]&amp;#34; : undefined);&#xA;  };&#xA;&#xA;  pi.on(&amp;#34;session_start&amp;#34;, async (_event, ctx) =&amp;gt; applyStatus(ctx));&#xA;&#xA;  pi.on(&amp;#34;before_agent_start&amp;#34;, (event) =&amp;gt; {&#xA;    if (!on) return;&#xA;    return { systemPrompt: `${event.systemPrompt}\n\n${INSTRUCTIONS}` };&#xA;  });&#xA;&#xA;  pi.registerCommand(&amp;#34;caveman&amp;#34;, {&#xA;    description: &amp;#34;Toggle caveman terse-response mode: on|off (default: on)&amp;#34;,&#xA;    handler: async (args, ctx) =&amp;gt; {&#xA;      const arg = args.trim().toLowerCase();&#xA;      if (arg === &amp;#34;off&amp;#34; || arg === &amp;#34;stop&amp;#34; || arg === &amp;#34;normal&amp;#34;) on = false;&#xA;      else if (arg === &amp;#34;on&amp;#34; || arg === &amp;#34;&amp;#34;) on = true;&#xA;      else return ctx.ui.notify(`Unknown arg &amp;#34;${arg}&amp;#34;. Use: on | off`, &amp;#34;error&amp;#34;);&#xA;      applyStatus(ctx);&#xA;      ctx.ui.notify(on ? &amp;#34;Caveman mode on.&amp;#34; : &amp;#34;Caveman mode off.&amp;#34;, &amp;#34;info&amp;#34;);&#xA;    },&#xA;  });&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#before_agent_start&#34;&gt;&lt;code&gt;before_agent_start&lt;/code&gt;&lt;/a&gt;&#xA;fires on &lt;strong&gt;every prompt&lt;/strong&gt;, before the agent loop starts, so&#xA;the instructions are re-appended each turn instead of relying on one message&#xA;that could get pruned from context. Dropped in&#xA;&lt;code&gt;~/.pi/agent/extensions/caveman.ts&lt;/code&gt;, auto-discovered, no &lt;code&gt;/reload&lt;/code&gt; needed after&#xA;the first load.&lt;/p&gt;&#xA;&lt;p&gt;Unlike the Claude Code plugin, I skipped &lt;code&gt;lite&lt;/code&gt;/&lt;code&gt;full&lt;/code&gt;/&lt;code&gt;ultra&lt;/code&gt;/&lt;code&gt;wenyan&lt;/code&gt; — I&#xA;called that bloat back in June, and I&amp;rsquo;m not adding it to my own version&#xA;either. On or off is enough.&lt;/p&gt;&#xA;&lt;p&gt;The result, &lt;em&gt;unprompted&lt;/em&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;user&lt;/strong&gt;: caveman on or off now?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;assistant&lt;/strong&gt;: Caveman: on.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;user&lt;/strong&gt;: how to turn it off?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;assistant&lt;/strong&gt;: &lt;code&gt;/caveman off&lt;/code&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I also skipped the parts that don&amp;rsquo;t have a pi equivalent: the statusline&#xA;savings counter, &lt;code&gt;/caveman-stats&lt;/code&gt;, the &lt;code&gt;caveman-shrink&lt;/code&gt; MCP proxy that rewrites&#xA;tool descriptions (a proxy that rewrites what an agent sees is exactly the&#xA;kind of thing worth reading before installing). The 40-line extension above&#xA;covers what I actually wanted.&lt;/p&gt;&#xA;&lt;p&gt;One rule baked into both the skill and the extension: code, commits, and PRs&#xA;stay full verbosity, never compressed.&lt;/p&gt;&#xA;&lt;p&gt;This experience was an eye-opener for me to realize how bloated the upstream&#xA;caveman project became.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: caveman mode&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>myrepos: a wip action to find unfinished work
      </title>
      <link>https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/</link>
      <pubDate>Fri, 17 Jul 2026 12:23:10 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/03/mr-update/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: I manage a few dozen repos via&#xA;&lt;a href=&#34;https://myrepos.branchable.com/&#34;&gt;myrepos&lt;/a&gt; (&lt;code&gt;mr&lt;/code&gt;), and I wanted a single command&#xA;to show which ones have work in progress (&amp;ldquo;WIP&amp;rdquo;) — uncommitted changes, dirty&#xA;staging area, stray branches and alike.&lt;/p&gt;&#xA;&lt;p&gt;I want essentially a filtered version of &lt;code&gt;mr ls&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;So I defined my own command. A repo is &lt;em&gt;not&lt;/em&gt; WIP when all three hold:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;the default branch is checked out;&lt;/li&gt;&#xA;&lt;li&gt;no other local branches exist;&lt;/li&gt;&#xA;&lt;li&gt;and the working tree is fully clean.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Anything else gets flagged with a reason.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/28376040a292a9898cca59f8ce27de81482d1a26/mr/.mrconfig.defaults#L14-L23&#34;&gt;The definition&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;# List repos with work in progress: not on default branch, extra local branches, or dirty tree.&#xA;# Run as `mr -m wip`: the -m (minimal) flag drops mr&amp;#39;s per-repo &amp;#34;mr wip:&amp;#34; header and blank&#xA;# lines, leaving only the flagged repos. This cannot be defaulted per-action (mr only honors&#xA;# -m/-q as CLI flags, not via config).&#xA;wip =&#xA; reasons=&amp;#34;&amp;#34;&#xA; cur=$(git symbolic-ref --quiet --short HEAD 2&amp;gt;/dev/null || echo &amp;#34;(detached)&amp;#34;)&#xA; def=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2&amp;gt;/dev/null | sed &amp;#39;s|^origin/||&amp;#39;)&#xA; if [ -z &amp;#34;$def&amp;#34; ]; then if git show-ref --verify --quiet refs/heads/main; then def=main; elif git show-ref --verify --quiet refs/heads/master; then def=master; fi; fi&#xA; if [ -n &amp;#34;$def&amp;#34; ] &amp;amp;&amp;amp; [ &amp;#34;$cur&amp;#34; != &amp;#34;$def&amp;#34; ]; then reasons=&amp;#34;$reasons branch:$cur&amp;#34;; fi&#xA; n=$(git for-each-ref --format=&amp;#39;%(refname)&amp;#39; refs/heads | wc -l | tr -d &amp;#39; &amp;#39;)&#xA; if [ &amp;#34;$n&amp;#34; -gt 1 ]; then reasons=&amp;#34;$reasons &amp;#43;$((n-1))_branches&amp;#34;; fi&#xA; if [ -n &amp;#34;$(git status --porcelain)&amp;#34; ]; then reasons=&amp;#34;$reasons dirty&amp;#34;; fi&#xA; if [ -n &amp;#34;$reasons&amp;#34; ]; then printf &amp;#39;%s [%s]\n&amp;#39; &amp;#34;$MR_REPO&amp;#34; &amp;#34;${reasons# }&amp;#34;; fi&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Naturally, the snippet above is art from the LLM, but it accomplishes exactly&#xA;what I want:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;thiago.perrotta ~&#xA;% mr -m wip&#xA;mr wip: /Users/thiago.perrotta/Corp/gitops&#xA;/Users/thiago.perrotta/Corp/gitops [branch:thiagowfx/acme &amp;#43;1_branches]&#xA;&#xA;mr wip: /Users/thiago.perrotta/Corp/terraform&#xA;/Users/thiago.perrotta/Corp/terraform [&amp;#43;1_branches]&#xA;&#xA;mr wip: /Users/thiago.perrotta/workspace/perrotta.dev&#xA;/Users/thiago.perrotta/workspace/perrotta.dev [&amp;#43;3_branches dirty]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Clean repos stay silent. I get to see all repos with unfinished work, and I can&#xA;see exactly why each one is dirty.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;mr wip&lt;/code&gt; works too but it&amp;rsquo;s noisier. I need to add &lt;code&gt;mr -m wip&lt;/code&gt; to my muscle&#xA;memory&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. Without the &lt;code&gt;-m&lt;/code&gt; flag, &lt;code&gt;mr&lt;/code&gt; prepends a &lt;code&gt;mr wip:&lt;/code&gt; header and a blank&#xA;line for &lt;em&gt;every&lt;/em&gt; repo, WIP or not.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;-m&lt;/code&gt; can&amp;rsquo;t be baked into the action definition — &lt;code&gt;mr&lt;/code&gt; only honors it as a CLI&#xA;flag — so the comment above the action documents it.&lt;/p&gt;&#xA;&lt;p&gt;Previously I&amp;rsquo;d run &lt;code&gt;mr xl&lt;/code&gt; (an alias for &lt;code&gt;git branch -vv&lt;/code&gt; across every repo) and&#xA;eyeball the result — but that&amp;rsquo;s dozens of lines of branch tips for tens of&#xA;repos, and I still have to scan for the ones that are actually behind or dirty.&#xA;&lt;code&gt;wip&lt;/code&gt; does the scanning: it only prints the repos that need attention, with the&#xA;reason attached. Much more ergonomic than visually diffing a wall of green.&lt;/p&gt;&#xA;&lt;p&gt;Runs read-only, so it&amp;rsquo;s safe to run twice.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;Should I rely on this workflow more often, I could leverage &lt;a href=&#34;https://perrotta.dev/2025/05/espanso-hello-world/&#34;&gt;Espanso&lt;/a&gt; or a&#xA;shell alias/function to easen its recall.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: myrepos: a wip action to find unfinished work&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>claude code: replace a hand-rolled loop with /goal
      </title>
      <link>https://perrotta.dev/2026/07/claude-code-replace-a-hand-rolled-loop-with-/goal/</link>
      <pubDate>Thu, 16 Jul 2026 13:44:25 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>claude</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/07/claude-code-replace-a-hand-rolled-loop-with-/goal/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/06/claude-code-ship-your-skills-as-a-plugin-marketplace/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: one of my skills, &lt;code&gt;/pr-pass&lt;/code&gt;, was a hand-rolled loop —&#xA;push, poll CI, fix a failure, re-push, repeat, bail after five iterations.&lt;/p&gt;&#xA;&lt;p&gt;It works quite well, and it&amp;rsquo;s one of my most used skills.&lt;/p&gt;&#xA;&lt;p&gt;That said, recently Claude Code shipped&#xA;&lt;a href=&#34;https://code.claude.com/docs/en/goal&#34;&gt;&lt;code&gt;/goal&lt;/code&gt;&lt;/a&gt; (in v2.1.139), which is exactly&#xA;that loop, packaging it as a primitive. So the skill was reimplementing something&#xA;the harness &lt;em&gt;now&lt;/em&gt; does natively.&lt;/p&gt;&#xA;&lt;p&gt;In fact,&#xA;&lt;a href=&#34;https://newsletter.pragmaticengineer.com/p/what-is-loop-engineering&#34;&gt;most&lt;/a&gt;&#xA;harnesses ship &lt;code&gt;/goal&lt;/code&gt; natively these days.&lt;/p&gt;&#xA;&lt;p&gt;We shall&#xA;&lt;a href=&#34;https://github.com/thiagowfx/skills/commit/322d4523f10c8ab1fa3cf8eeb95d776c6debf17c&#34;&gt;refactor&lt;/a&gt;!&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;/goal&lt;/code&gt; sets a completion condition. After each turn a fast model checks whether&#xA;it holds; if not, Claude starts another turn on its own. It auto-clears once the&#xA;condition is met. From the docs:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The &lt;code&gt;/goal&lt;/code&gt; command sets a completion condition and Claude keeps working&#xA;toward it without you prompting each step. After each turn, a small fast model&#xA;checks whether the condition holds. If not, Claude starts another turn instead&#xA;of returning control to you.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;That deletes three things the old skill hand-built: the &lt;code&gt;Monitor&lt;/code&gt; poll waiting&#xA;on CI state, the &amp;ldquo;go back to Step 1&amp;rdquo; loop, and the max-5-iterations guard. The&#xA;guard becomes a clause in the condition itself:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/goal every check on this PR has passed (gh pr checks shows no FAILURE&#xA;and no PENDING), or stop after 5 turns&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;One catch worth knowing before reaching for &lt;code&gt;/goal&lt;/code&gt;: the evaluator (tester?) is&#xA;a fresh model that &lt;strong&gt;only reads the transcript&lt;/strong&gt;. It does not run &lt;code&gt;gh&lt;/code&gt; or read&#xA;files.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;It does not call tools, so it can only judge what Claude has already surfaced&#xA;in the conversation.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;So the condition has to be something Claude&amp;rsquo;s own output demonstrates. The skill&#xA;now says to paste the &lt;code&gt;gh pr checks&lt;/code&gt; result into the reply every turn, so the&#xA;evaluator has a CI state to judge against. Otherwise the goal would have never&#xA;resolved thus it would have looped forever.&lt;/p&gt;&#xA;&lt;p&gt;Another gotcha: &lt;code&gt;/goal&lt;/code&gt; doesn&amp;rsquo;t touch permissions. Each turn&amp;rsquo;s &lt;code&gt;git push&lt;/code&gt;&#xA;still prompts unless paired it with &lt;a href=&#34;https://code.claude.com/docs/en/auto-mode-config&#34;&gt;auto&#xA;mode&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The lesson generalizes past this one skill: whenever the harness grows a&#xA;primitive, the custom code that predates it becomes technical debt. &lt;code&gt;/loop&lt;/code&gt;,&#xA;Stop hooks, and &lt;code&gt;/goal&lt;/code&gt; now cover most of the &amp;ldquo;keep going until X&amp;rdquo; constructs I&#xA;used previously.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: claude code: replace a hand-rolled loop with /goal&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <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>bloggify</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/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/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>★ 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>bloggify</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/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ 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>bloggify</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/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/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>bloggify</category>
      <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/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>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>bloggify</category>
      <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/bloggify/&#34;&gt;#bloggify&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>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>bloggify</category>
      <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/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/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>bloggify</category>
      <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/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&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>bloggify</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/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/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>
  </channel>
</rss>
