<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Inkstone</title><link>https://me.125520.xyz/</link><description>A minimal bilingual Hugo template for longreads.</description><generator>Hugo</generator><language>en</language><managingEditor>Jason</managingEditor><copyright>© 2026 Jason</copyright><lastBuildDate>Sat, 20 Jun 2026 12:27:17 +0000</lastBuildDate><atom:link href="https://me.125520.xyz/en/tags/github/index.xml" rel="self" type="application/rss+xml"/><item><title>Managing Multiple GitHub Accounts on One Machine</title><link>https://me.125520.xyz/en/github-multi-account-config/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://me.125520.xyz/en/github-multi-account-config/</guid><description>SSH key isolation + Git conditional config + Host aliases: let personal and work projects use separate accounts automatically and cleanly.</description><content:encoded><![CDATA[<blockquote>
<p>Running both personal and work projects on one dev machine, with commits going to different GitHub accounts. Don&rsquo;t want to manually change <code>user.email</code> after every clone, and don&rsquo;t want a global account-switching script. SSH key isolation + Git conditional config solves it once and for all.</p>
</blockquote>
<h2 id="1-scenario">1. Scenario</h2>
<p>Typical needs:</p>
<ul>
<li><strong>Personal projects</strong> (<code>~/personal/*</code>) commit with personal GitHub account</li>
<li><strong>Work projects</strong> (<code>~/work/*</code>) commit with company GitHub account</li>
<li>Each account has its own SSH key, no interference</li>
<li>Identity switches automatically when entering different directories, no manual setup</li>
</ul>
<p>Additional pain points:</p>
<ul>
<li>Corporate network might block SSH default port 22, need to use 443</li>
<li>Some projects need aliases (like <code>work.github.com</code>) to differentiate remote URLs</li>
</ul>
<h2 id="2-overall-strategy">2. Overall Strategy</h2>
<p>Three-layer configuration working together:</p>
<ol>
<li><strong>SSH layer</strong> (<code>~/.ssh/config</code>): Configure separate keys and Host aliases for different accounts</li>
<li><strong>Git global layer</strong> (<code>~/.gitconfig</code>): Set default account and SSH command</li>
<li><strong>Git conditional layer</strong> (<code>~/.gitconfig-work</code>): Auto-override account info based on directory path</li>
</ol>
<p>Key mechanism: <code>includeIf</code> lets Git load additional config files in specific directories.</p>
<h2 id="3-steps">3. Steps</h2>
<h3 id="31-generate-ssh-key-pairs">3.1 Generate SSH Key Pairs</h3>
<p>Generate independent keys for each account:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Personal account key</span>
</span></span><span class="line"><span class="cl">ssh-keygen -t rsa -b <span class="m">4096</span> -C <span class="s2">&#34;personal@example.com&#34;</span> -f ~/.ssh/id_rsa_personal
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Work account key</span>
</span></span><span class="line"><span class="cl">ssh-keygen -t rsa -b <span class="m">4096</span> -C <span class="s2">&#34;work@company.com&#34;</span> -f ~/.ssh/id_rsa_work
</span></span></code></pre></div><p>After generation you&rsquo;ll have:</p>
<ul>
<li><code>~/.ssh/id_rsa_personal</code> + <code>~/.ssh/id_rsa_personal.pub</code></li>
<li><code>~/.ssh/id_rsa_work</code> + <code>~/.ssh/id_rsa_work.pub</code></li>
</ul>
<p>Add the <code>.pub</code> public keys to the SSH keys settings page of the corresponding GitHub accounts.</p>
<h3 id="32-configure-ssh-host-aliases">3.2 Configure SSH Host Aliases</h3>
<p>Edit <code>~/.ssh/config</code> (create if it doesn&rsquo;t exist):</p>
<pre tabindex="0"><code class="language-ssh-config" data-lang="ssh-config"># Personal account (default)
Host github.com
  HostName ssh.github.com
  User git
  IdentityFile ~/.ssh/id_rsa_personal
  Port 443

# Work account (using alias work.github.com)
Host work.github.com
  HostName ssh.github.com
  User git
  IdentityFile ~/.ssh/id_rsa_work
  Port 443
</code></pre><p><strong>Key points</strong>:</p>
<ul>
<li><strong>Port 443</strong>: Bypass firewall blocking of port 22, GitHub officially supports <code>ssh.github.com:443</code></li>
<li><strong>Host alias</strong>: <code>work.github.com</code> is a local alias, actual connection is still to <code>ssh.github.com</code></li>
<li><strong>IdentityFile</strong>: Specify which key to use</li>
</ul>
<p>Test connections:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Test personal account</span>
</span></span><span class="line"><span class="cl">ssh -T git@github.com
</span></span><span class="line"><span class="cl"><span class="c1"># Expected output: Hi personal-username! You&#39;ve successfully authenticated...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Test work account</span>
</span></span><span class="line"><span class="cl">ssh -T git@work.github.com
</span></span><span class="line"><span class="cl"><span class="c1"># Expected output: Hi work-username! You&#39;ve successfully authenticated...</span>
</span></span></code></pre></div><h3 id="33-configure-git-global-defaults">3.3 Configure Git Global Defaults</h3>
<p>Edit <code>~/.gitconfig</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[user]</span>
</span></span><span class="line"><span class="cl">  <span class="na">name</span> <span class="o">=</span> <span class="s">Your Name
</span></span></span><span class="line"><span class="cl"><span class="s">  email = personal@example.com</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[core]</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Globally force use of personal account key</span>
</span></span><span class="line"><span class="cl">  <span class="na">sshCommand</span> <span class="o">=</span> <span class="s">ssh -i ~/.ssh/id_rsa_personal -F /dev/null</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Conditional config: work directory auto-switches account</span>
</span></span><span class="line"><span class="cl"><span class="k">[includeIf &#34;gitdir:~/work/&#34;]</span>
</span></span><span class="line"><span class="cl">  <span class="na">path</span> <span class="o">=</span> <span class="s">~/.gitconfig-work</span>
</span></span></code></pre></div><p><strong>Key points</strong>:</p>
<ul>
<li><code>sshCommand</code> forces use of specified key, <code>-F /dev/null</code> prevents <code>~/.ssh/config</code> interference (optional, depends on your priority strategy)</li>
<li><code>includeIf</code> path matching: the path after <code>gitdir:</code> <strong>must end with <code>/</code></strong> to match subdirectories</li>
<li>Path supports <code>~</code> expansion and wildcards <code>*</code></li>
</ul>
<h3 id="34-configure-work-account-conditional-file">3.4 Configure Work Account Conditional File</h3>
<p>Create <code>~/.gitconfig-work</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[user]</span>
</span></span><span class="line"><span class="cl">  <span class="na">name</span> <span class="o">=</span> <span class="s">Your Name
</span></span></span><span class="line"><span class="cl"><span class="s">  email = work@company.com</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[core]</span>
</span></span><span class="line"><span class="cl">  <span class="na">sshCommand</span> <span class="o">=</span> <span class="s">ssh -i ~/.ssh/id_rsa_work -F /dev/null</span>
</span></span></code></pre></div><p>Now the logic is:</p>
<ul>
<li>Default uses personal account (<code>~/.gitconfig</code>)</li>
<li>Entering any subdirectory of <code>~/work/</code>, auto-switch to work account (<code>~/.gitconfig-work</code> overrides)</li>
</ul>
<h3 id="35-clone-work-projects-using-host-alias">3.5 Clone Work Projects Using Host Alias</h3>
<p>When cloning work projects, modify the URL:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Original URL (would use personal account)</span>
</span></span><span class="line"><span class="cl">git clone git@github.com:company/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Use Host alias (will use work account key)</span>
</span></span><span class="line"><span class="cl">git clone git@work.github.com:company/project.git
</span></span></code></pre></div><p>Or modify remote after cloning:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git remote set-url origin git@work.github.com:company/project.git
</span></span></code></pre></div><p><strong>Why need alias?</strong></p>
<p>Even in <code>~/work/</code> directory, if remote URL is <code>git@github.com</code>, SSH will prioritize matching the <code>Host github.com</code> config in <code>~/.ssh/config</code>. Host aliases let you differentiate accounts at the URL level.</p>
<h2 id="4-verify-configuration">4. Verify Configuration</h2>
<h3 id="41-check-active-config-in-current-directory">4.1 Check Active Config in Current Directory</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># In personal project directory</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/personal/my-project
</span></span><span class="line"><span class="cl">git config user.email
</span></span><span class="line"><span class="cl"><span class="c1"># Output: personal@example.com</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># In work project directory</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/work/company-project
</span></span><span class="line"><span class="cl">git config user.email
</span></span><span class="line"><span class="cl"><span class="c1"># Output: work@company.com</span>
</span></span></code></pre></div><h3 id="42-check-ssh-key-being-used">4.2 Check SSH Key Being Used</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># View actual SSH command in use</span>
</span></span><span class="line"><span class="cl">git config core.sshCommand
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Manual test (with verbose logging)</span>
</span></span><span class="line"><span class="cl"><span class="nv">GIT_SSH_COMMAND</span><span class="o">=</span><span class="s2">&#34;ssh -v&#34;</span> git ls-remote origin
</span></span><span class="line"><span class="cl"><span class="c1"># Output will show &#34;Offering public key: /path/to/id_rsa_xxx&#34;</span>
</span></span></code></pre></div><h3 id="43-test-commit-identity">4.3 Test Commit Identity</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create test commit</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;test&#34;</span> &gt; test.txt
</span></span><span class="line"><span class="cl">git add test.txt
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;test commit&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># View commit author</span>
</span></span><span class="line"><span class="cl">git log -1 --format<span class="o">=</span><span class="s2">&#34;%an &lt;%ae&gt;&#34;</span>
</span></span></code></pre></div><h2 id="5-common-issues">5. Common Issues</h2>
<h3 id="51-port-22-blocked">5.1 Port 22 Blocked</h3>
<p>Symptom: <code>ssh -T git@github.com</code> times out</p>
<p>Solution: Add to <code>~/.ssh/config</code>:</p>
<pre tabindex="0"><code class="language-ssh-config" data-lang="ssh-config">Host github.com
  HostName ssh.github.com
  Port 443
</code></pre><p>GitHub officially provides SSH service on port 443 at <code>ssh.github.com</code>, specifically for bypassing firewalls.</p>
<h3 id="52-key-permission-issues">5.2 Key Permission Issues</h3>
<p>Symptom: <code>Permissions 0644 for 'id_rsa_work' are too open</code></p>
<p>Solution:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod <span class="m">600</span> ~/.ssh/id_rsa_*
</span></span><span class="line"><span class="cl">chmod <span class="m">644</span> ~/.ssh/id_rsa_*.pub
</span></span></code></pre></div><p>Private keys must be readable/writable only by owner (<code>600</code>), public keys can be more permissive (<code>644</code>).</p>
<h3 id="53-includeif-not-taking-effect">5.3 <code>includeIf</code> Not Taking Effect</h3>
<p>Checklist:</p>
<ol>
<li><strong>Path must end with <code>/</code></strong>: <code>gitdir:~/work/</code> (correct) vs <code>gitdir:~/work</code> (wrong)</li>
<li><strong>Git version</strong>: <code>includeIf</code> requires Git 2.13+, run <code>git --version</code> to confirm</li>
<li><strong>Path expansion</strong>: <code>~</code> expands, but relative paths don&rsquo;t, recommend absolute paths or <code>~/</code></li>
<li><strong>Debug</strong>: Run <code>git config --show-origin user.email</code> to see config source</li>
</ol>
<h3 id="54-switching-account-for-existing-projects">5.4 Switching Account for Existing Projects</h3>
<p>If project has already committed with wrong account:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. Modify remote URL (if needed)</span>
</span></span><span class="line"><span class="cl">git remote set-url origin git@work.github.com:company/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Modify most recent commit author (if not yet pushed)</span>
</span></span><span class="line"><span class="cl">git commit --amend --reset-author
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Batch modify historical commits (use with caution, rewrites history)</span>
</span></span><span class="line"><span class="cl">git filter-branch --env-filter <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">if [ &#34;$GIT_COMMITTER_EMAIL&#34; = &#34;wrong@example.com&#34; ]; then
</span></span></span><span class="line"><span class="cl"><span class="s1">  export GIT_COMMITTER_EMAIL=&#34;correct@example.com&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">  export GIT_AUTHOR_EMAIL=&#34;correct@example.com&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">fi
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span> --tag-name-filter cat -- --branches --tags
</span></span></code></pre></div><h2 id="6-advanced-finer-grained-control">6. Advanced: Finer-Grained Control</h2>
<h3 id="61-project-specific-configuration">6.1 Project-Specific Configuration</h3>
<p>If a project needs special configuration, run within the project:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config user.email <span class="s2">&#34;special@example.com&#34;</span>
</span></span><span class="line"><span class="cl">git config core.sshCommand <span class="s2">&#34;ssh -i ~/.ssh/id_rsa_special&#34;</span>
</span></span></code></pre></div><p>Project-level config (<code>.git/config</code>) has highest priority, overrides global and conditional configs.</p>
<h3 id="62-multiple-work-directories">6.2 Multiple Work Directories</h3>
<p>If you have multiple work directories:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[includeIf &#34;gitdir:~/work/&#34;]</span>
</span></span><span class="line"><span class="cl">  <span class="na">path</span> <span class="o">=</span> <span class="s">~/.gitconfig-work</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[includeIf &#34;gitdir:~/company-a/&#34;]</span>
</span></span><span class="line"><span class="cl">  <span class="na">path</span> <span class="o">=</span> <span class="s">~/.gitconfig-company-a</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[includeIf &#34;gitdir:~/company-b/&#34;]</span>
</span></span><span class="line"><span class="cl">  <span class="na">path</span> <span class="o">=</span> <span class="s">~/.gitconfig-company-b</span>
</span></span></code></pre></div><h3 id="63-https-protocol-multi-account">6.3 HTTPS Protocol Multi-Account</h3>
<p>This article focuses on SSH, but HTTPS also supports multi-account:</p>
<ul>
<li>macOS Keychain stores credentials, managed by <code>git-credential-osxkeychain</code></li>
<li>Or use GitHub CLI: <code>gh auth login</code> supports multi-account switching</li>
<li>Conditional config can set different <code>credential.helper</code></li>
</ul>
<h2 id="7-summary">7. Summary</h2>
<p>Workflow after configuration:</p>
<ol>
<li>Personal projects in <code>~/personal/</code>, auto-use personal account</li>
<li>Work projects in <code>~/work/</code>, auto-use work account</li>
<li>Remember to use <code>git@work.github.com</code> when cloning work projects</li>
<li>Forgot to configure? Check with <code>git config user.email</code> before committing</li>
</ol>
<p>Core benefits:</p>
<ul>
<li><strong>Zero manual switching</strong>: Automatic upon entering directory</li>
<li><strong>Thorough isolation</strong>: Keys, identity, directories three-layer isolation</li>
<li><strong>Verifiable</strong>: Clear testing methods for each step</li>
</ul>
<p>This setup has been in use for two years, never had a commit with the wrong account since.</p>
]]></content:encoded></item><item><title>Claude Code Advanced Patterns: Hooks, MCP, Parallel Agents, Worktrees, and Context</title><link>https://me.125520.xyz/en/claude-code-advanced-tips/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://me.125520.xyz/en/claude-code-advanced-tips/</guid><description>Eight modules I actually use day-to-day with Claude Code at the intermediate-to-advanced tier: hooks, CLAUDE.md, MCP, parallel agents, worktrees, slash commands, permissions, context. Each section ships with the config I'm running.</description><content:encoded><![CDATA[<blockquote>
<p>Half a year of running Claude Code, and the framing has shifted on me. Started out treating it like &ldquo;VSCode + Cursor with a different vibe&rdquo;; now it feels closer to managing a small team that can run jobs in parallel. This post is a sweep of the high-leverage features I actually depend on — hooks, MCP, subagents, worktrees, layered CLAUDE.md, when to <code>/clear</code>. Not a doc rewrite. The lens is &ldquo;what a user with opinions actually does&rdquo;, and you should be able to lift any block straight into your own setup.</p>
</blockquote>
<h2 id="0-map-of-the-article">0. Map of the article</h2>
<p>Eight modules, ordered from &ldquo;do this first&rdquo; to &ldquo;feel this out&rdquo;:</p>
<table>
  <thead>
      <tr>
          <th>#</th>
          <th>Module</th>
          <th>Type</th>
          <th>One line</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>Hook system</td>
          <td>Hands-on</td>
          <td>Hang your own scripts on lifecycle events</td>
      </tr>
      <tr>
          <td>2</td>
          <td>Layered CLAUDE.md</td>
          <td>Hands-on</td>
          <td>A project manual the agent always carries</td>
      </tr>
      <tr>
          <td>3</td>
          <td>MCP server</td>
          <td>Hands-on</td>
          <td>Wrap any external capability into a callable tool</td>
      </tr>
      <tr>
          <td>4</td>
          <td>Parallel agents</td>
          <td>Hands-on</td>
          <td>Fan out subagents from one session</td>
      </tr>
      <tr>
          <td>5</td>
          <td>Worktree isolation</td>
          <td>Hands-on</td>
          <td><code>git worktree</code> keeps multiple sessions physically apart</td>
      </tr>
      <tr>
          <td>6</td>
          <td>Slash commands / Skills</td>
          <td>Hands-on</td>
          <td>Freeze repeated workflows into <code>/cmd</code></td>
      </tr>
      <tr>
          <td>7</td>
          <td>Permission &amp; safety model</td>
          <td>Conceptual</td>
          <td>What sits behind allow / deny / skip</td>
      </tr>
      <tr>
          <td>8</td>
          <td>Context management</td>
          <td>Conceptual</td>
          <td><code>/clear</code>, <code>/compact</code>, dealing with context rot</td>
      </tr>
  </tbody>
</table>
<p>Hands-on sections come with copy-paste-able snippets. Conceptual sections explain &ldquo;why this is shaped this way&rdquo; and &ldquo;when to reach for it&rdquo;.</p>
<h2 id="1-hooks-scripts-on-lifecycle-events">1. Hooks: scripts on lifecycle events</h2>
<p>Hooks are the single most underrated capability in Claude Code. They slice the session into a set of lifecycle events and let you inject your own scripts at each point:</p>
<table>
  <thead>
      <tr>
          <th>Event</th>
          <th>Fires when</th>
          <th>What I do with it</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SessionStart</code></td>
          <td>Session begins</td>
          <td>Inject project state, git status, unread issues</td>
      </tr>
      <tr>
          <td><code>UserPromptSubmit</code></td>
          <td>After enter, before model reads</td>
          <td>Rewrite or reject the prompt (block secrets, inject project metadata)</td>
      </tr>
      <tr>
          <td><code>PreToolUse</code></td>
          <td>Before a tool call</td>
          <td>Intercept dangerous commands, block writes to certain paths</td>
      </tr>
      <tr>
          <td><code>PostToolUse</code></td>
          <td>After a tool call</td>
          <td>Auto-lint, auto-test, append to changelog</td>
      </tr>
      <tr>
          <td><code>Notification</code></td>
          <td>Model is waiting for input</td>
          <td>Toast / phone push</td>
      </tr>
      <tr>
          <td><code>Stop</code></td>
          <td>Main turn ends</td>
          <td>Wrap-up notification, log line</td>
      </tr>
      <tr>
          <td><code>SubagentStop</code></td>
          <td>Subagent turn ends</td>
          <td>Per-agent timing / signal</td>
      </tr>
  </tbody>
</table>
<p>Registration goes in <code>~/.claude/settings.json</code> or the project&rsquo;s <code>.claude/settings.json</code>. The two most useful patterns:</p>
<p><strong>Pattern one: hang automation on either side of model actions.</strong> For example, run ESLint on every Edit and feed the errors back. Telling lint errors to the model directly works far better than &ldquo;you forgot to lint, please remember next time&rdquo; — the model fixes them on the next turn without being asked.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;hooks&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;PostToolUse&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;matcher&#34;</span><span class="p">:</span> <span class="s2">&#34;Edit|Write&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;hooks&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">          <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;command&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;node /Users/me/.claude/hooks/lint-changed.js&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;timeout&#34;</span><span class="p">:</span> <span class="mi">30</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>matcher</code> is a regex over tool names — <code>Edit|Write</code> triggers on file-write tools. The script reads the hook payload from stdin (including the path of the file that changed), runs lint, and prints errors to stdout. A non-zero exit code makes Claude Code feed stderr into the model&rsquo;s next input.</p>
<p><strong>Pattern two: wire session state to the outside world.</strong> I wrote a <a href="/claude-code-mac-bark-notify/">whole post on Mac notifications + Bark push</a>
 earlier — the meat of it is a Python script bound to <code>Stop</code> and <code>Notification</code>, throwing a local macOS toast and a remote push to my phone. Long jobs no longer require staring at the screen.</p>
<div class="callout callout-info" role="note">
  <div class="callout-icon" aria-hidden="true"></div>
  <div class="callout-body"><p class="callout-title">Paths must be absolute</p>
    A hook&rsquo;s cwd is unpredictable (it depends on which project the session was launched from), and <code>~/...</code> doesn&rsquo;t expand. Write everything as absolute paths.
  </div>
</div>

<p>The real strength of hooks is that <strong>they turn &ldquo;remind the model to do X&rdquo; into &ldquo;the system enforces X&rdquo;</strong>. Instructional prompts get forgotten. Hooks don&rsquo;t.</p>
<h2 id="2-claudemd-a-project-manual-for-the-agent">2. CLAUDE.md: a project manual for the agent</h2>
<p>CLAUDE.md looks trivial — it&rsquo;s just a Markdown file injected into every session&rsquo;s system prompt. The leverage comes from layering:</p>
<pre tabindex="0"><code>~/.claude/CLAUDE.md         # global: injected for all projects
&lt;repo&gt;/CLAUDE.md            # project: only when cd&#39;d into that repo
&lt;repo&gt;/&lt;subdir&gt;/CLAUDE.md   # subdir: only for work in that subtree
</code></pre><p>Merge order is outside-in, with inner files taking precedence. Combine them like this:</p>
<ul>
<li><strong>Global</strong> holds &ldquo;who I am, what I prefer&rdquo;. e.g. &ldquo;I&rsquo;m Jason, prefer functional style&rdquo;, &ldquo;use formal terms over slang in Chinese replies&rdquo;.</li>
<li><strong>Project</strong> holds &ldquo;the implicit rules of this repo&rdquo;. e.g. &ldquo;use pnpm not npm&rdquo;, &ldquo;must run <code>pnpm test</code> before committing&rdquo;, &ldquo;Tailwind colors only from <code>tokens.ts</code>, no <code>bg-red-500</code>&rdquo;.</li>
<li><strong>Subdir</strong> holds &ldquo;the local conventions for this slice&rdquo;. e.g. <code>apps/api/CLAUDE.md</code> says &ldquo;every endpoint needs a zod schema&rdquo;.</li>
</ul>
<p>What works best to write in there? A few rules I&rsquo;ve internalized:</p>
<ol>
<li><strong>Write the traps the model will fall into, not the obvious.</strong> Don&rsquo;t write &ldquo;we use React&rdquo; — the model will read package.json. Do write &ldquo;our <code>useFetch</code> must live outside the try/catch because internal handling already wraps&rdquo;.</li>
<li><strong>Write things that pay off forever once taught.</strong> Boris Cherny&rsquo;s &ldquo;compounding engineering&rdquo; — each mistake the model makes, once captured, never has to be re-explained. Add the small ones: &ldquo;yesterday it imported logger from the wrong path again&rdquo;.</li>
<li><strong>Give a reason where you can.</strong> &ldquo;Use X not Y&rdquo; is weaker than &ldquo;use X not Y because Y triggers ESM/CJS interop issues&rdquo;. Reasons make the rule generalize.</li>
<li><strong>Keep it lean.</strong> CLAUDE.md loads every session and eats context. My personal red line is 200 lines per file. Beyond that, split into subdirs or move to slash commands that load on demand.</li>
</ol>
<p>A trimmed sample of my project-level template:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># Project: lifelog
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Stack
</span></span></span><span class="line"><span class="cl"><span class="k">-</span> Hugo (extended) + custom theme <span class="sb">`inkstone`</span> (git submodule)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Posts in content/posts/, bilingual: foo.md + foo.en.md
</span></span><span class="line"><span class="cl"><span class="k">-</span> Linked via translationKey
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Writing conventions
</span></span></span><span class="line"><span class="cl"><span class="k">-</span> First person, opinionated
</span></span><span class="line"><span class="cl"><span class="k">-</span> Open with a blockquote
</span></span><span class="line"><span class="cl"><span class="k">-</span> For long posts, give a map of the article in section 0
</span></span><span class="line"><span class="cl"><span class="k">-</span> Avoid colloquial translations of jargon
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Operational conventions
</span></span></span><span class="line"><span class="cl"><span class="k">-</span> Local preview: <span class="sb">`hugo server -D`</span>
</span></span><span class="line"><span class="cl"><span class="k">-</span> Before committing: <span class="sb">`hugo --minify`</span> to check for build errors
</span></span><span class="line"><span class="cl"><span class="k">-</span> Never <span class="sb">`git push`</span> directly. I&#39;ll do it.
</span></span></code></pre></div><p>That last line pairs with §7 below — agents that see this will pull back on their own.</p>
<h2 id="3-mcp-server-wrap-external-capabilities-as-tools">3. MCP server: wrap external capabilities as tools</h2>
<p>MCP (Model Context Protocol) is Anthropic&rsquo;s open protocol that lets any external tool/data source plug into Claude (or any compatible client) through a uniform interface. Anthropic&rsquo;s own line: MCP is to LLMs what USB-C is to hardware.</p>
<p>Claude Code ships with a few built in (filesystem, bash, web fetch), but the real unlock is custom MCP servers. The three primitives map to three &ldquo;ways an agent knows things&rdquo;:</p>
<table>
  <thead>
      <tr>
          <th>Primitive</th>
          <th>Meaning</th>
          <th>Analogy</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Tools</strong></td>
          <td>Actions the model can invoke</td>
          <td>Functions</td>
      </tr>
      <tr>
          <td><strong>Resources</strong></td>
          <td>Data the model can read</td>
          <td>Files</td>
      </tr>
      <tr>
          <td><strong>Prompts</strong></td>
          <td>Templates the user can trigger</td>
          <td>Shortcuts</td>
      </tr>
  </tbody>
</table>
<p>Fastest way to register a server with Claude Code is the CLI:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Add a local stdio server</span>
</span></span><span class="line"><span class="cl">claude mcp add my-tools <span class="se">\
</span></span></span><span class="line"><span class="cl">  --command <span class="s2">&#34;node /Users/me/mcp-servers/my-tools/index.js&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add an HTTP server</span>
</span></span><span class="line"><span class="cl">claude mcp add notion <span class="se">\
</span></span></span><span class="line"><span class="cl">  --url <span class="s2">&#34;https://mcp.example.com/notion&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --header <span class="s2">&#34;Authorization: Bearer xxx&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># See what&#39;s currently mounted</span>
</span></span><span class="line"><span class="cl">claude mcp list
</span></span></code></pre></div><p>Writing a minimal custom server isn&rsquo;t hard either. Here&rsquo;s a &ldquo;read a Linear issue&rdquo; skeleton in Node.js + the official SDK:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// index.js
</span></span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">Server</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">&#34;@modelcontextprotocol/sdk/server/index.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">StdioServerTransport</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">&#34;@modelcontextprotocol/sdk/server/stdio.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Server</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;linear-mini&#34;</span><span class="p">,</span> <span class="nx">version</span><span class="o">:</span> <span class="s2">&#34;0.1.0&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span> <span class="nx">capabilities</span><span class="o">:</span> <span class="p">{</span> <span class="nx">tools</span><span class="o">:</span> <span class="p">{}</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">server</span><span class="p">.</span><span class="nx">setRequestHandler</span><span class="p">(</span><span class="s2">&#34;tools/list&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">tools</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;get_issue&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">description</span><span class="o">:</span> <span class="s2">&#34;Fetch a Linear issue by identifier (e.g. ENG-123)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">inputSchema</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">type</span><span class="o">:</span> <span class="s2">&#34;object&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">properties</span><span class="o">:</span> <span class="p">{</span> <span class="nx">id</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s2">&#34;string&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="nx">required</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;id&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">}));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">server</span><span class="p">.</span><span class="nx">setRequestHandler</span><span class="p">(</span><span class="s2">&#34;tools/call&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">name</span> <span class="o">!==</span> <span class="s2">&#34;get_issue&#34;</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;unknown tool&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">arguments</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="sb">`https://api.linear.app/graphql`</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">method</span><span class="o">:</span> <span class="s2">&#34;POST&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">headers</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Content-Type&#34;</span><span class="o">:</span> <span class="s2">&#34;application/json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">Authorization</span><span class="o">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">LINEAR_API_KEY</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">body</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">      <span class="nx">query</span><span class="o">:</span> <span class="sb">`query($id:String!){ issue(id:$id){ title description state { name } } }`</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">variables</span><span class="o">:</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">r</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">{</span> <span class="nx">content</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">type</span><span class="o">:</span> <span class="s2">&#34;text&#34;</span><span class="p">,</span> <span class="nx">text</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="p">}]</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">await</span> <span class="nx">server</span><span class="p">.</span><span class="nx">connect</span><span class="p">(</span><span class="k">new</span> <span class="nx">StdioServerTransport</span><span class="p">());</span>
</span></span></code></pre></div><p>Once mounted, just say &ldquo;look at issue ENG-123&rdquo; in the session and the model calls <code>get_issue</code>.</p>
<p>How I think about it:</p>
<ul>
<li><strong>If a stock MCP exists, don&rsquo;t write one.</strong> GitHub, Notion, Slack, Postgres all have official/community servers. Browse <a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener">modelcontextprotocol.io<span class="external-mark" aria-hidden="true">↗</span></a>
 first.</li>
<li><strong>Custom MCP fits &ldquo;private / internal / cross-tool&rdquo; capabilities.</strong> Internal status board, your personal notes vault, the NAS at home — that kind of thing.</li>
<li><strong>A failing MCP server doesn&rsquo;t kill the session, it degrades.</strong> If a server can&rsquo;t start at launch, the whole session still runs; that one tool is just unavailable. So mount freely.</li>
</ul>
<h2 id="4-parallel-agents-fan-out-subagents">4. Parallel agents: fan out subagents</h2>
<p>Claude Code ships with a <code>Task</code> tool that fans out subagents from the main session in parallel. Not a gimmick — it&rsquo;s the feature that genuinely changes work rhythm.</p>
<p>Typical fits:</p>
<ul>
<li><strong>Large refactors</strong>: &ldquo;rename X to Y&rdquo; splits into &ldquo;find all references&rdquo;, &ldquo;rewrite src/&rdquo;, &ldquo;rewrite test/&rdquo;, &ldquo;rewrite docs/&rdquo;, four lanes in parallel</li>
<li><strong>Multi-stack comparison</strong>: two subagents implementing the same component in React and Vue, return for diffing</li>
<li><strong>Code review</strong>: one subagent for security, one for performance, one for readability</li>
</ul>
<p>Triggering it from the main session is just natural language: &ldquo;fan out 3 subagents to check type errors in <code>src/api/</code>, <code>src/components/</code>, <code>src/hooks/</code> respectively, then summarize.&rdquo; The model uses Task automatically.</p>
<p>To freeze it as a standard flow, define named subagents under <code>.claude/agents/</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">&lt;!-- .claude/agents/security-reviewer.md --&gt;
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">name: security-reviewer
</span></span><span class="line"><span class="cl">description: Code review agent focused on security
</span></span><span class="line"><span class="cl">tools: Read, Grep, Glob, Bash
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You are a code reviewer focused on the OWASP Top 10.
</span></span><span class="line"><span class="cl">For each finding, output:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> **Severity**: Critical / High / Medium / Low
</span></span><span class="line"><span class="cl"><span class="k">-</span> **Location**: file:line
</span></span><span class="line"><span class="cl"><span class="k">-</span> **Class**: SQL Injection / XSS / SSRF / ...
</span></span><span class="line"><span class="cl"><span class="k">-</span> **Fix**: a specific change
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Security only. No style or performance feedback.
</span></span></code></pre></div><p>After this, &ldquo;use the security-reviewer subagent to scan src/api/&rdquo; lights it up directly. The <code>tools:</code> field locks down what the subagent is allowed to use (omit Edit/Write to keep it strictly read-only).</p>
<p>A handful of things I&rsquo;ve learned the hard way:</p>
<ul>
<li><strong>The practical ceiling is ~5–8 in parallel.</strong> Beyond that, token cost and context-switching cost dominate.</li>
<li><strong>Subagents don&rsquo;t share the main session&rsquo;s context.</strong> They only see their own prompt and the tools they can call. The instruction has to be self-contained.</li>
<li><strong>Merging the results is the main agent&rsquo;s job.</strong> After getting N outputs back, the main agent still has to synthesize. This step quietly determines whether the parallelism actually paid off.</li>
<li><strong>Not every task is parallelizable.</strong> Strongly coupled tasks (B&rsquo;s input is A&rsquo;s output) get slower when forced into parallel. Test: can you write the work as several truly independent subtasks?</li>
</ul>
<h2 id="5-worktree-isolation-physically-separate-sessions">5. Worktree isolation: physically separate sessions</h2>
<p>Another granularity of parallelism is <strong>multiple Claude Code main sessions on the same repo at once</strong>. The pitfall: they all touch the same working tree and stomp each other.</p>
<p><code>git worktree</code> was made for this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Main repo at ~/code/myapp (working on main as usual)</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/code/myapp
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Spawn two isolated working dirs on independent branches</span>
</span></span><span class="line"><span class="cl">git worktree add ../myapp-feat-a -b feat/a
</span></span><span class="line"><span class="cl">git worktree add ../myapp-feat-b -b feat/b
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Three physical directories now exist:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   ~/code/myapp        (main)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   ~/code/myapp-feat-a (feat/a)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   ~/code/myapp-feat-b (feat/b)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Same .git, fully isolated working trees</span>
</span></span></code></pre></div><p>Open three terminals, <code>cd</code> into each, and <code>claude</code> in each. Three agents working on three things, no cross-contamination.</p>
<p>My flow:</p>
<ol>
<li>For complex tasks, drop into plan mode (<code>shift+tab</code>) in the main repo and split the work with the model into 3–5 <strong>orthogonal</strong> subtasks.</li>
<li>Each subtask gets a worktree (named <code>&lt;repo&gt;-&lt;task-slug&gt;</code>).</li>
<li>Open three terminal panes (tmux / Warp split), one Claude per pane.</li>
<li>After everything finishes, return to the main repo, <code>git worktree list</code>, review each, merge, <code>git worktree remove</code>.</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree list                      <span class="c1"># what&#39;s currently active</span>
</span></span><span class="line"><span class="cl">git worktree remove ../myapp-feat-a    <span class="c1"># tear down when done</span>
</span></span><span class="line"><span class="cl">git worktree prune                     <span class="c1"># clean stale references</span>
</span></span></code></pre></div><p>A few notes:</p>
<ul>
<li><strong>Each worktree gets its own branch.</strong> Two worktrees on the same branch is rejected by git (and that&rsquo;s a feature).</li>
<li><strong><code>node_modules</code> / <code>.venv</code> aren&rsquo;t shared.</strong> Each worktree reinstalls. pnpm&rsquo;s content-addressable store or a venv symlink can help, at the cost of more setup.</li>
<li><strong>Cross-worktree env vars / <code>.env</code> don&rsquo;t auto-copy.</strong> <code>cp</code> or symlink as needed.</li>
<li><strong>A worktree is not a fork.</strong> All worktrees share the same <code>.git/</code>, so <code>git fetch</code> updates everything once and commits land in the same object database. That&rsquo;s why it&rsquo;s much faster than a full <code>git clone</code>.</li>
</ul>
<p>Worktrees and parallel subagents are two different parallels. Subagents fan out <em>inside</em> one session. Worktrees physically isolate <em>between</em> sessions. The former is fast but coupled, the latter is slower but cleaner. I reach for worktrees on refactors / cross-module changes, and for subagents on investigation / inspection / generation tasks.</p>
<h2 id="6-slash-commands--skills-freeze-the-workflow">6. Slash commands / Skills: freeze the workflow</h2>
<p><code>/command</code> is the other compounding investment in Claude Code. Anything you&rsquo;ve done twice should be considered for a slash command on the third pass.</p>
<p>Where they live:</p>
<pre tabindex="0"><code>~/.claude/commands/&lt;name&gt;.md         # global, available everywhere
&lt;repo&gt;/.claude/commands/&lt;name&gt;.md    # project-scoped
</code></pre><p>The file itself is Markdown that gets injected as a prompt. Here&rsquo;s the <code>/review</code> I use:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">&lt;!-- .claude/commands/review.md --&gt;
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">description: Self-review the diff between this branch and main
</span></span><span class="line"><span class="cl">allowed-tools: Bash(git diff:*), Bash(git log:*), Read
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Self-review every change on this branch relative to main:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Run <span class="sb">`git diff main...HEAD --stat`</span> to see which files changed
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Run <span class="sb">`git diff main...HEAD`</span> for the full content
</span></span><span class="line"><span class="cl"><span class="k">3.</span> Give feedback on:
</span></span><span class="line"><span class="cl">   <span class="k">-</span> Abstractions that add complexity without value
</span></span><span class="line"><span class="cl">   <span class="k">-</span> Violations of established conventions (consult CLAUDE.md)
</span></span><span class="line"><span class="cl">   <span class="k">-</span> Test coverage (especially new public functions)
</span></span><span class="line"><span class="cl">   <span class="k">-</span> Lingering console.log, debugger, TODO
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Finish with a markdown review report I can paste straight into the PR.
</span></span></code></pre></div><p>Type <code>/review</code> in a session and the whole thing fires. The <code>allowed-tools</code> field temporarily whitelists git diff/log even if they aren&rsquo;t in the global allowlist — safe by default.</p>
<p>Arguments are easy too — use <code>$ARGUMENTS</code> in the file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">&lt;!-- .claude/commands/explain.md --&gt;
</span></span><span class="line"><span class="cl">Explain in detail what $ARGUMENTS does, why it&#39;s written this way,
</span></span><span class="line"><span class="cl">and whether there&#39;s a simpler alternative.
</span></span></code></pre></div><p>In session, <code>/explain src/utils/debounce.ts</code> substitutes the path in.</p>
<p>My current resident commands:</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/review</code></td>
          <td>Self-review the current branch</td>
      </tr>
      <tr>
          <td><code>/plan</code></td>
          <td>Decompose a request into a TODO list, wait for approval before acting</td>
      </tr>
      <tr>
          <td><code>/explain &lt;path&gt;</code></td>
          <td>Explain a piece of code</td>
      </tr>
      <tr>
          <td><code>/tidy</code></td>
          <td>Run lint + format + fix obvious type errors</td>
      </tr>
      <tr>
          <td><code>/post</code></td>
          <td>Blog-only — scaffold front matter + outline for a new post</td>
      </tr>
  </tbody>
</table>
<p>Skills (the broader &ldquo;packaged capability&rdquo; idea) are still evolving, but the principle is the same: <strong>turn &ldquo;things you keep re-explaining to Claude&rdquo; into &ldquo;one-line triggers that are now project assets&rdquo;</strong>. Each one is permanent dividend.</p>
<h2 id="7-permission--safety-model-allow--deny--skip">7. Permission &amp; safety model: allow / deny / skip</h2>
<p>The permission system is one of the things that sets Claude Code apart from generic &ldquo;AI editors&rdquo;. Three tiers:</p>
<table>
  <thead>
      <tr>
          <th>Mode</th>
          <th>Behavior</th>
          <th>When</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Default (ask)</strong></td>
          <td>Every tool call prompts</td>
          <td>New projects, first time using an MCP</td>
      </tr>
      <tr>
          <td><strong>Auto-accept</strong></td>
          <td>Tools on the allowlist auto-pass</td>
          <td>Daily</td>
      </tr>
      <tr>
          <td><strong>Bypass / dangerouslySkipPermissions</strong></td>
          <td>Everything passes</td>
          <td>Sandbox runs, throwaway scripts</td>
      </tr>
  </tbody>
</table>
<p>The <code>permissions</code> block in <code>settings.json</code> is where you tune most often:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;permissions&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;allow&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(npm test:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(pnpm:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git status)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git diff:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git log:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Edit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Write&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Read&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;deny&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(rm -rf:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(chmod:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(curl:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git push:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git commit:*)&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>deny beats allow. My personal style is conservative:</p>
<ol>
<li><strong>Deny anything genuinely dangerous you can name.</strong> <code>rm -rf</code>, <code>git push</code>, <code>git commit</code>, <code>curl</code> (defends against exfil), <code>chmod</code>, <code>sudo</code>.</li>
<li><strong>Allow common reads.</strong> <code>git status</code>, <code>git log</code>, <code>ls</code>, <code>pwd</code>, <code>cat</code>, the test commands.</li>
<li><strong>Don&rsquo;t allowlist writes.</strong> I leave <code>Edit</code> / <code>Write</code> on default ask — the extra prompt feels slow but the floor is solid.</li>
</ol>
<p><code>--dangerously-skip-permissions</code> (a launch flag) is the nuclear option: every check is bypassed. It is <strong>not</strong> for daily use. Reserve it for:</p>
<ul>
<li><strong>Docker / VM sandboxes</strong> where blowing things up is &ldquo;rebuild and move on&rdquo;</li>
<li><strong>One-shot script tasks</strong>, e.g. batch-renaming 1000 files while you&rsquo;re literally watching</li>
<li><strong>CI / Headless</strong> runs where the environment is throwaway by design</li>
</ul>
<p>Never flip on <code>dangerouslySkipPermissions</code> on your daily-driver setup. The day a hallucination decides to <code>rm -rf ~</code>, you&rsquo;ll cry.</p>
<p>A safer alternative is <strong>sandboxing</strong>: run Claude in a devcontainer / Docker / Multipass VM, physically isolated. Anthropic&rsquo;s own <a href="https://docs.claude.com/" target="_blank" rel="noopener">security best practices<span class="external-mark" aria-hidden="true">↗</span></a>
 recommend this.</p>
<p>The point of the permission model is to <strong>let agents act autonomously inside a defined zone, and force a stop at the edge</strong>. Allow too loose and you eat damage; deny too aggressive and you&rsquo;re back to &ldquo;co-pilot mode&rdquo; pressing accept on everything, losing the agentic value. The dial is per-project and per-personality.</p>
<h2 id="8-context-management-context-is-king-but-it-rots">8. Context management: context is king, but it rots</h2>
<p>Last one, conceptual. Karpathy&rsquo;s much-quoted line:</p>
<blockquote>
<p>&ldquo;Context engineering is the delicate art and science of filling the context window with just the right information for the next step.&rdquo;</p>
</blockquote>
<p>And Anthropic&rsquo;s own research keeps confirming a counterintuitive point: <strong>more context isn&rsquo;t better</strong>. Models exhibit &ldquo;context rot&rdquo; under long inputs — even on simple tasks, performance degrades as input length grows.</p>
<p>Claude Code gives you a few knives:</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>What</th>
          <th>When</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/clear</code></td>
          <td>Wipe context, start fresh</td>
          <td>Switch to a totally unrelated task</td>
      </tr>
      <tr>
          <td><code>/compact</code></td>
          <td>Have the model self-summarize the current chat</td>
          <td>Same task, but too much accumulated chatter</td>
      </tr>
      <tr>
          <td><code>/resume</code></td>
          <td>Recover detail that was compacted away</td>
          <td>Occasional need to look back</td>
      </tr>
  </tbody>
</table>
<p>How I use them:</p>
<ul>
<li><strong>Task boundaries are the natural <code>/clear</code> point.</strong> Done with feature A and now fixing bug B? <code>/clear</code> first.</li>
<li><strong>30+ turns deep on the same task and the model starts drifting? <code>/compact</code>.</strong> It re-focuses noticeably.</li>
<li><strong>CLAUDE.md is &ldquo;permanent context&rdquo;, don&rsquo;t let it rot into a junk drawer.</strong> Every line should earn its keep.</li>
<li><strong>Long reference material doesn&rsquo;t go in chat — write it to a file and let the model <code>Read</code> it.</strong> <code>Read</code> is on-demand; pasting is once-and-stuck.</li>
</ul>
<p>A deeper point: <strong>subagents are a context-management tool</strong>. Each fanned-out subagent gets a clean window; only the summary returns to the main. That&rsquo;s why a big refactor is faster as fan-out than as one main agent grinding through — not because parallelism saves wall time, but because each sub-context stays cleaner.</p>
<p>Like the permission model, context management is a question of timing. The only rule: <strong>when the model starts repeating, looping, or forgetting, act.</strong> That intuition only develops with reps.</p>
<h2 id="9-closing-from-tool-to-team">9. Closing: from tool to team</h2>
<p>Stepping back from eight sections, the through-line is:</p>
<blockquote>
<p>What&rsquo;s most valuable about Claude Code isn&rsquo;t that it can write code. It&rsquo;s that it can be <strong>configured into a part of how you work</strong>.</p>
</blockquote>
<p>Hooks are reflex arcs. CLAUDE.md is long-term memory. MCP is sense extension. Subagents are clones. Worktrees are physical isolation. Slash commands are muscle memory. The permission model is the boundary. Context management is restraint. Composed together, what you&rsquo;re managing isn&rsquo;t &ldquo;an AI editor&rdquo; anymore — it&rsquo;s a small team with reflexes, memory, parallelism, boundaries, and the discipline to rest.</p>
<p>If you can only adopt three:</p>
<ol>
<li><strong>Every project gets a CLAUDE.md under 200 lines.</strong> Cheapest compounding move there is.</li>
<li><strong>Anything you&rsquo;ve done twice, on the third pass becomes a slash command or a hook.</strong></li>
<li><strong>Tasks longer than 30 minutes go through plan mode; complex changes go straight into a worktree.</strong></li>
</ol>
<p>The rest — MCP, subagents, custom servers — is &ldquo;next step&rdquo;. Add when needed, skip when not. The value of a tool is <strong>what it does when you&rsquo;re not there</strong>, not how many of them you collected.</p>
<hr>
<p><strong>References:</strong></p>
<ul>
<li><a href="https://docs.claude.com/en/docs/claude-code" target="_blank" rel="noopener">Claude Code official docs<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li><a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener">MCP protocol homepage<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li><a href="https://www.anthropic.com/engineering/claude-code-best-practices" target="_blank" rel="noopener">Anthropic Engineering: Claude Code Best Practices<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li><a href="https://claude.com/blog/how-anthropic-teams-use-claude-code" target="_blank" rel="noopener">How Anthropic Teams Use Claude Code<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li>My earlier post: <a href="/claude-code-mac-bark-notify/">Wiring Claude Code into macOS Notifications + Bark Push</a>
</li>
<li>My earlier post: <a href="/cs146s-modern-software-dev-notes/">CS146S course notes</a>
 (Week 3 / Week 4 overlap most with this one)</li>
</ul>
]]></content:encoded></item><item><title>Stanford CS146S Notes: An 8-Week Map from Prompt to Agent Manager</title><link>https://me.125520.xyz/en/cs146s-modern-software-dev-notes/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://me.125520.xyz/en/cs146s-modern-software-dev-notes/</guid><description>My notes on Stanford CS146S, The Modern Software Developer. Eight weeks of assignments, readings, and guest lectures collapsed into three storylines: Prompt → Context, Coder → Agent Manager, Speed → Defensible.</description><content:encoded><![CDATA[<blockquote>
<p><a href="https://themodernsoftware.dev/" target="_blank" rel="noopener">CS146S — The Modern Software Developer<span class="external-mark" aria-hidden="true">↗</span></a>
 is a new Stanford course from Fall 2025, taught by Mihail Eric, with a heavyweight guest each week (Boris Cherny, Zach Lloyd, Isaac Evans, Tomas Reimers&hellip;). I worked through all eight weeks in the official <a href="https://github.com/mihail911/modern-software-dev-assignments" target="_blank" rel="noopener">assignments repo<span class="external-mark" aria-hidden="true">↗</span></a>
 and did the exercises along the way. This post is the consolidated note.</p>
</blockquote>
<h2 id="1-the-courses-real-theme-a-shift-in-developer-identity">1. The Course&rsquo;s Real Theme: A Shift in Developer Identity</h2>
<p>The tagline on the course site is unusually direct:</p>
<blockquote>
<p>&ldquo;Many AI coding courses only teach you how to write fast — this course draws the baseline for shipping: testable, auditable, defensible.&rdquo;</p>
</blockquote>
<p>If you only skim the weekly schedule, you&rsquo;d think it&rsquo;s &ldquo;An AI Tools Survey.&rdquo; But after reading the materials and doing the assignments, a more honest description is:</p>
<p><strong>It is systematically training you to become an Agent Manager.</strong></p>
<p>Not &ldquo;learn to code with ChatGPT,&rdquo; but: stop being the person who writes the code and become the person who directs Agents to write it — and who owns the result. Boris Cherny (creator of Claude Code) gets quoted again and again:</p>
<blockquote>
<p>&ldquo;It&rsquo;s not so much about deep work, it&rsquo;s about how good I am at context switching and jumping across multiple different contexts very quickly.&rdquo;</p>
</blockquote>
<p>The eight weeks fall into three storylines:</p>
<ol>
<li><strong>Prompt → Context</strong> (Weeks 1 → 3): from writing one good prompt to architecting a whole information environment</li>
<li><strong>Coder → Agent Manager</strong> (Weeks 2 → 5): from writing code yourself to directing and orchestrating multiple Agents</li>
<li><strong>Speed → Defensible</strong> (Weeks 6 → 8): from rapid output to auditable, defensible, ship-ready</li>
</ol>
<p>The rest of the post follows those three lines.</p>
<h2 id="2-storyline-one-prompt--context">2. Storyline One: Prompt → Context</h2>
<h3 id="week-1-six-foundational-prompting-techniques">Week 1: Six Foundational Prompting Techniques</h3>
<p>Week 1 runs local models through Ollama and drills six techniques in turn: K-shot, Chain-of-Thought, Tool Calling, Self-consistency, RAG, Reflexion. One Python file per technique, iterate the prompt until tests pass.</p>
<p>The takeaway isn&rsquo;t &ldquo;I learned six tricks.&rdquo; It&rsquo;s: <strong>these aren&rsquo;t parallel options, they form an evolutionary chain.</strong></p>
<ul>
<li>K-shot gives the model a pattern</li>
<li>CoT gives it room to reason</li>
<li>Tool Calling lets it stop guessing and actually look things up</li>
<li>Self-consistency trades sampling cost for stability</li>
<li>RAG plugs external knowledge into the context</li>
<li>Reflexion lets the model critique its own output and try again</li>
</ul>
<p>By Reflexion, the model has gone from &ldquo;answer machine&rdquo; to &ldquo;small self-correcting loop.&rdquo; That is the seed of an Agent.</p>
<h3 id="week-2-first-time-closing-the-loop-on-a-real-task">Week 2: First Time Closing the Loop on a Real Task</h3>
<p>The assignment is a minimal FastAPI + SQLite Action Item Extractor: free-form notes → structured todo list. Use Cursor&rsquo;s Agentic Mode to land five TODOs: replace rule-based extraction with an LLM, add unit tests, refactor, add a new endpoint + frontend button, generate a README.</p>
<p>In my own practice notes I distilled a minimum capability model for &ldquo;a local Agent that executes simple requirements&rdquo;:</p>
<pre tabindex="0"><code>understand intent → fetch code context → plan → generate code → verify → feedback
        ↑                                                                  |
        └────────────── on failure, backtrack &amp; correct ←─────────────────┘
</code></pre><p>Each step maps onto a Week 1 technique. Agents are not a new invention; they are those atomic capabilities wired up around this loop.</p>
<p>After running it end-to-end the strongest impression: <strong>today&rsquo;s Agents shine on tasks with clear boundaries that you can verify.</strong> Scaffolding, single-function implementations, happy-path tests, doc generation — these are strengths. Deciding &ldquo;what <em>not</em> to change,&rdquo; reasoning about cross-module side effects, refactors that involve real tradeoffs — those still need a human.</p>
<h3 id="week-3-from-prompt-engineering-to-context-engineering">Week 3: From Prompt Engineering to Context Engineering</h3>
<p>Week 3 is the densest week of the course (the other one being Week 6). Boris Cherny is the guest. The thesis traces back to Andrej Karpathy&rsquo;s definition:</p>
<blockquote>
<p>&ldquo;Context engineering is the delicate art and science of filling the context window with just the right information for the next step.&rdquo;</p>
</blockquote>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>Prompt Engineering</th>
          <th>Context Engineering</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Focus</td>
          <td>What you say to the model right now</td>
          <td>What the model knows when you say it</td>
      </tr>
      <tr>
          <td>Scope</td>
          <td>One carefully crafted instruction</td>
          <td>The whole information ecosystem: memory, tools, retrieval</td>
      </tr>
      <tr>
          <td>Nature</td>
          <td>Static template</td>
          <td>Dynamic infrastructure</td>
      </tr>
      <tr>
          <td>Analogy</td>
          <td>Handing a script to an improv actor</td>
          <td>Building the whole stage, props included</td>
      </tr>
  </tbody>
</table>
<p>Two things in this week made me re-examine how I work:</p>
<p><strong>&ldquo;Context is King&rdquo; — and a precondition for intelligence.</strong> A line that hits hard: &ldquo;Claude is already smart enough — intelligence isn&rsquo;t the bottleneck, context is. When the Agent fails, the instinct is to blame the model, but it&rsquo;s almost always wrong. What&rsquo;s actually underperforming is the information environment around it.&rdquo;</p>
<p><strong>Context Rot.</strong> Model performance degrades with input length, even on simple tasks. So <code>/clear</code>, <code>/compact</code>, tiered memory, keeping CLAUDE.md lean — these aren&rsquo;t aesthetic choices, they&rsquo;re performance strategies.</p>
<p>The Week 3 assignment is to build a custom MCP Server. I built a GitHub Issue assistant: 3 Tools (<code>search_issues</code> / <code>create_issue</code> / <code>get_issue_detail</code>) + 1 Resource (repo metadata) + 1 Prompt (bug report template), STDIO transport into Claude Desktop.</p>
<p>Building it by hand is the only way I really understood: <strong>the three MCP primitives (Tools / Resources / Prompts) aren&rsquo;t abstract concepts — they map to three different ways an Agent &ldquo;knows&rdquo; something</strong> — what it can do, what it can look up, how it should phrase things.</p>
<p>Boris also shared his daily workflow: 20–30 PRs a day, sustained by running 5 Claude Code instances in terminals plus 5–10 sessions on claude.ai/code, every instance starting in Plan Mode, with CLAUDE.md as shared team memory. The line that gets hammered home:</p>
<blockquote>
<p>&ldquo;Give Claude a way to verify its own work — browser tests, test suites, simulators. Verification is what gives you a 2-3x quality improvement on the final output. Everything else (subagents, slash commands, MCP integrations) is built on top of that.&rdquo;</p>
</blockquote>
<h2 id="3-storyline-two-coder--agent-manager">3. Storyline Two: Coder → Agent Manager</h2>
<h3 id="week-4-claude-code-as-agent-manager">Week 4: Claude Code as Agent Manager</h3>
<p>Week 4&rsquo;s central line:</p>
<blockquote>
<p>&ldquo;The goal is to train you as an Agent Manager — not someone who writes code, but someone who directs Agents to write code.&rdquo;</p>
</blockquote>
<p>This week catalogs 5 agentic workflow patterns:</p>
<ul>
<li><strong>Sequential Flow</strong> — the default. About 70% of daily work.</li>
<li><strong>Operator</strong> — hierarchical: a central planner + specialized sub-agents</li>
<li><strong>Split-and-Merge</strong> — fan-out / fan-in, up to ~10 parallel branches</li>
<li><strong>Agent Teams</strong> — collaborative; agents can talk to each other across independent sessions</li>
<li><strong>Headless</strong> — triggered by CI/CD, Cron, webhooks; no human in the loop</li>
</ul>
<p>Plus 12 &ldquo;Agent Harness Patterns&rdquo; (persistent instructions, directory-scoped instructions, tiered memory, memory hygiene, context compaction, parallel branching, checkpoint review, selective tool loading, purpose-built tools, permission granularity, lifecycle hooks, compounding corrections).</p>
<p>The line that stuck:</p>
<blockquote>
<p>&ldquo;The problem isn&rsquo;t your model — it&rsquo;s your harness. What separates a flashy demo from a production-grade AI system isn&rsquo;t intelligence, it&rsquo;s control.&rdquo;</p>
</blockquote>
<p>The assignment: build at least 2 automations (Slash Commands, CLAUDE.md, SubAgents) on a FastAPI starter app. After this week my muscle memory shifted: every time I see a repeated workflow I now ask &ldquo;could this become a <code>.claude/commands/</code> entry?&rdquo;; every time Claude makes the same mistake twice I ask &ldquo;should this go into CLAUDE.md?&rdquo;</p>
<p>The phrase <strong>Compounding Engineering</strong> is one I like a lot: every recorded mistake becomes permanent leverage.</p>
<h3 id="week-5-the-terminal-is-not-a-power-users-toy">Week 5: The Terminal Is Not a Power User&rsquo;s Toy</h3>
<p>Week 5&rsquo;s guest is Warp CEO Zach Lloyd. Warp 2.0 reframes itself from &ldquo;a better terminal&rdquo; to ADE — Agentic Development Environment.</p>
<p>The comparison table I found most useful:</p>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>Strength</th>
          <th>Weakness</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Warp</strong></td>
          <td>Multi-tab parallel agents, visual diff review, multi-model support</td>
          <td>Slightly weaker at deep code reasoning</td>
      </tr>
      <tr>
          <td><strong>Claude Code</strong></td>
          <td>Deep context, fine-grained CLAUDE.md control, autonomous reasoning</td>
          <td>Anthropic models only</td>
      </tr>
  </tbody>
</table>
<p>These stack rather than compete: <strong>Claude Code can run inside Warp</strong>. The key tool for parallel Agents is <code>git worktree</code> — each Agent works in an isolated working tree, no clobbering, merge branches at the end. I started using this in real work after reading this week.</p>
<p>But the Warp team is honest about the cost: <strong>parallelism isn&rsquo;t free.</strong> Saving 6–7 hours a week is real, but only if your cognitive bandwidth can actually steer 5 tabs at once — that turns out to be the real bottleneck.</p>
<h2 id="4-storyline-three-speed--defensible">4. Storyline Three: Speed → Defensible</h2>
<h3 id="week-6-semgrep-and-ai-code-security">Week 6: Semgrep and AI Code Security</h3>
<p>Week 6 brings Semgrep CEO Isaac Evans. The opening number made me pause:</p>
<blockquote>
<p>Claude Code, run as a security scanner, surfaced 46 real vulnerabilities — at a <strong>14% true positive rate</strong> and <strong>86% false positive rate</strong>.</p>
</blockquote>
<p>This isn&rsquo;t &ldquo;Claude is bad.&rdquo; It&rsquo;s that <strong>AI&rsquo;s security intuition isn&rsquo;t reliable — out of 7 alerts only 1 is a real issue.</strong> Semgrep flips that ratio: 80% fewer false positives, 250% more true positives.</p>
<p>Semgrep&rsquo;s three products:</p>
<ul>
<li><strong>Code (SAST)</strong> — first-party code vulns, with cross-file analysis</li>
<li><strong>Supply Chain (SCA)</strong> — <em>reachable</em> CVEs in dependencies (reachability analysis filters out unreachable ones, cutting high/critical-severity false positives by 98%)</li>
<li><strong>Secrets</strong> — hardcoded credentials, with semantic + entropy analysis + live validation</li>
</ul>
<p>The assignment: run <code>semgrep ci --subdir week6</code> on a deliberately vulnerable FastAPI app and fix at least 3 findings. The fixes are unglamorous but fundamental: parameterized SQL, restricted CORS, secrets to env vars, dependency upgrades, stronger crypto, sanitized DOM writes.</p>
<p>The blunt takeaway: <strong>&ldquo;Secure Vibe Coding&rdquo; wants speed and safety unified, not opposed</strong> — the way you get there is automating scans into CI rather than retrofitting safety after the fact.</p>
<h3 id="week-7-graphite-diamond-and-ai-code-review">Week 7: Graphite Diamond and AI Code Review</h3>
<p>Week 7&rsquo;s guest is Graphite CPO Tomas Reimers. The thesis:</p>
<blockquote>
<p>&ldquo;AI will never replace human code review. But every PR should be reviewed by AI + human together.&rdquo;</p>
</blockquote>
<p>AI&rsquo;s strengths in review: style consistency, common bug patterns, security vulnerabilities, code duplication, 100% PR coverage, second-level latency.
Human strengths: architectural judgment, business logic correctness, tradeoff analysis, creative alternatives, team context, UX design.</p>
<p>The recommended pattern is &ldquo;AI First, Human Second&rdquo;: AI filters the mechanical stuff, humans focus on the high-leverage judgment calls. Graphite&rsquo;s other product innovation is <strong>Stacked PRs</strong> — break a big PR into a chain of small sequential ones, because AI review performs noticeably better on small PRs.</p>
<p>The assignment is dense: 4 independent tasks, each going through &ldquo;create branch → 1-shot AI implementation → manual line-by-line review → open PR → run Graphite Diamond → compare.&rdquo; Then a written reflection: my review vs Graphite&rsquo;s, which one was better, when, and why.</p>
<p>That <strong>comparative reflection</strong> is the most valuable part of the week, in my view. It isn&rsquo;t teaching you to trust AI. It&rsquo;s teaching you <strong>to know when AI is better than you, and when it isn&rsquo;t.</strong> That meta-cognition is more durable than any specific tool.</p>
<h3 id="week-8-boltnew-and-multi-stack-rapid-builds">Week 8: Bolt.new and Multi-Stack Rapid Builds</h3>
<p>Week 8 pivots to &ldquo;prototype to production.&rdquo; The guest is Vercel&rsquo;s Head of AI Research. The framing:</p>
<blockquote>
<p>&ldquo;Rapid prototyping is just the starting point. Between demo and production lies a chasm.&rdquo;</p>
</blockquote>
<p>Bolt.new uses StackBlitz&rsquo;s WebContainer technology to run Node.js entirely in the browser, generating full-stack apps from prompts. But its capability curve is steep:</p>
<table>
  <thead>
      <tr>
          <th>Complexity</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>3–5 components</td>
          <td>Usable in minutes, almost no debugging</td>
      </tr>
      <tr>
          <td>10–15 components</td>
          <td>Needs prompt iteration, but generally works</td>
      </tr>
      <tr>
          <td>15–20+ components</td>
          <td>Context degrades, token use spikes</td>
      </tr>
      <tr>
          <td>Complex state + auth + 3rd-party integrations</td>
          <td>Success rate drops to <strong>31%</strong></td>
      </tr>
  </tbody>
</table>
<p>The assignment asks you to build the same app in 3 different stacks, at least one via Bolt and at least one in a non-JS language. The design is sneakily clever — it trains two things at once:</p>
<ol>
<li><strong>Abstraction</strong> — you have to pull the business logic out of the framework and figure out what&rsquo;s essence vs implementation</li>
<li><strong>AI-tool literacy</strong> — the <em>same</em> prompt produces wildly different quality across Next.js / Django / Rails, so you have to develop a feel for &ldquo;what does production-ready code actually look like&rdquo;</li>
</ol>
<h2 id="5-three-lessons-i-actually-use-daily">5. Three Lessons I Actually Use Daily</h2>
<p>After eight weeks of material and a stack of assignments, these are the three I&rsquo;ve actually absorbed into my workflow:</p>
<h3 id="51-plan-mode-is-free-leverage">5.1 Plan Mode Is Free Leverage</h3>
<p>Boris&rsquo;s &ldquo;never let Claude write code before you have reviewed and approved a written plan&rdquo; is the most-quoted line of the course. For any task longer than 30 minutes I now hit <code>shift+tab</code> into Plan Mode, iterate with Claude until the plan is right, then switch to auto-accept. Rework rate dropped noticeably.</p>
<h3 id="52-claudemd-is-where-compounding-lives">5.2 CLAUDE.md Is Where Compounding Lives</h3>
<p>Every time Claude makes a mistake or I notice a non-obvious project convention, it goes into CLAUDE.md. The habit is incredibly cheap and incredibly profitable — once it&rsquo;s in there I never explain it again. Just keep it lean: it loads every session and eats context.</p>
<h3 id="53-always-give-the-agent-a-way-to-verify-itself">5.3 Always Give the Agent a Way to Verify Itself</h3>
<p>Boris&rsquo;s &ldquo;most important insight.&rdquo; Concretely:</p>
<ul>
<li>Backend work: have Claude run the tests, fail-then-fix, never &ldquo;looks fine, ship it&rdquo;</li>
<li>Frontend work: spin up the dev server and have Claude actually click through the golden path with Playwright</li>
<li>API integrations: get a minimal curl/httpie reproduction script first</li>
</ul>
<p>This is just the engineering version of Reflexion (Week 1) — bake &ldquo;self-review&rdquo; directly into the workflow.</p>
<h2 id="6-closing-the-eight-week-arc">6. Closing: The Eight-Week Arc</h2>
<pre tabindex="0"><code>Week 1:  How to talk to AI                  → Prompt Engineering
Week 2:  How AI uses tools                  → Coding Agents &amp; MCP
Week 3:  How to build AI&#39;s information env  → Context Engineering
Week 4:  How to manage AI                   → Agent Manager
Week 5:  AI-native terminal                 → AI Terminal
Week 6:  AI code security                   → Semgrep
Week 7:  AI code review                     → Graphite Diamond
Week 8:  AI-built applications              → Bolt.new multi-stack
(Weeks 9-10: AI ops + the future            → second half of the course)
</code></pre><p>Each week is the foundation for the next. What you walk away with isn&rsquo;t a tool manual — it&rsquo;s a new posture:</p>
<p><strong>You aren&rsquo;t the person writing code anymore. You&rsquo;re the person designing the information environment, directing the Agents, and answering for the final output.</strong></p>
<p>Coding speed is up 10x. So is the speed at which you can ship the wrong thing. The baseline this course draws — <strong>testable, auditable, defensible</strong> — isn&rsquo;t a constraint. It&rsquo;s the floor that makes the new posture viable.</p>
<hr>
<p><strong>References:</strong></p>
<ul>
<li>Course homepage: <a href="https://themodernsoftware.dev/" target="_blank" rel="noopener">themodernsoftware.dev<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li>Assignments repo: <a href="https://github.com/mihail911/modern-software-dev-assignments" target="_blank" rel="noopener">mihail911/modern-software-dev-assignments<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li>Community Chinese edition: <a href="https://github.com/ShouZhengAI/CS146S_CN" target="_blank" rel="noopener">ShouZhengAI/CS146S_CN<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li>Claude Code best practices: <a href="https://www.anthropic.com/engineering/claude-code-best-practices" target="_blank" rel="noopener">anthropic.com/engineering/claude-code-best-practices<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li>12 Agentic Harness Patterns: <a href="https://generativeprogrammer.com/p/12-agentic-harness-patterns-from" target="_blank" rel="noopener">generativeprogrammer.com<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li>How Anthropic Teams Use Claude Code: <a href="https://claude.com/blog/how-anthropic-teams-use-claude-code" target="_blank" rel="noopener">claude.com/blog<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li>Andrej Karpathy on Context Engineering (original X post)</li>
<li>Boris Cherny&rsquo;s workflow: <a href="https://howborisusesclaudecode.com" target="_blank" rel="noopener">howborisusesclaudecode.com<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
</ul>
]]></content:encoded></item><item><title>Wiring Claude Code into macOS Notifications + Bark Push</title><link>https://me.125520.xyz/en/claude-code-mac-bark-notify/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://me.125520.xyz/en/claude-code-mac-bark-notify/</guid><description>Translating a Windows PowerShell gist to macOS, then bolting on a Bark channel so the phone buzzes when long jobs finish too.</description><content:encoded><![CDATA[<blockquote>
<p>Origin story: spotted a <a href="https://gist.github.com/RisenMyth/6297cab96eb9dec912cc6c126cbed965" target="_blank" rel="noopener">Windows gist that wires Claude Code into system notifications<span class="external-mark" aria-hidden="true">↗</span></a>
, written in PowerShell, useless on a Mac. Rewrote it for macOS, then added a <a href="https://day.app/" target="_blank" rel="noopener">Bark<span class="external-mark" aria-hidden="true">↗</span></a>
 channel — when a long job finishes and you&rsquo;ve wandered off, the phone in your pocket should be the one that pings.</p>
</blockquote>
<h2 id="1-the-shape-of-it">1. The shape of it</h2>
<p>Claude Code&rsquo;s hook system exposes a set of lifecycle events. For &ldquo;notify me&rdquo;, you only need two:</p>
<ul>
<li><strong><code>Stop</code></strong> — the model finished its turn. Time to ring the bell.</li>
<li><strong><code>Notification</code></strong> — the model wants confirmation or input (permission prompts, questions). More urgent.</li>
</ul>
<p>The hook command receives JSON on stdin: <code>cwd</code> (project path), <code>transcript_path</code> (full JSONL conversation), and <code>Notification</code> events also carry a <code>message</code>.</p>
<p>Design choices:</p>
<ul>
<li><strong>Single Python file</strong> — macOS ships <code>python3</code> by default, so no jq / curl JSON-escaping ceremony.</li>
<li><strong>Local toast prefers <code>terminal-notifier</code></strong> — use it if installed (clickable, supports group dedup); fall back to <code>osascript</code> otherwise. Zero install required.</li>
<li><strong>Bark is opt-in</strong> — only fires when <code>BARK_KEY</code> is set. Failures are silent; the hook&rsquo;s exit code stays clean.</li>
<li><strong>No chmod</strong> — see the &ldquo;gotchas&rdquo; section below. The script isn&rsquo;t executable; we invoke it as <code>python3 path/to/notify.py</code>.</li>
</ul>
<h2 id="2-the-script-claudehooksnotifypy">2. The script: <code>~/.claude/hooks/notify.py</code></h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;Claude Code notification hook for macOS.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Local toast (osascript or terminal-notifier) + optional Bark push.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Usage: notify.py &lt;Stop|Notification|...&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Reads JSON from stdin (Claude Code hook payload):
</span></span></span><span class="line"><span class="cl"><span class="s2">  cwd               -&gt; current working directory
</span></span></span><span class="line"><span class="cl"><span class="s2">  transcript_path   -&gt; path to JSONL transcript (Stop event)
</span></span></span><span class="line"><span class="cl"><span class="s2">  message           -&gt; notification message (Notification event)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Environment variables (all optional):
</span></span></span><span class="line"><span class="cl"><span class="s2">  BARK_KEY            Bark device key. If unset, Bark push is skipped.
</span></span></span><span class="line"><span class="cl"><span class="s2">  BARK_SERVER         Bark server (default: https://api.day.app).
</span></span></span><span class="line"><span class="cl"><span class="s2">  BARK_GROUP          Notification group (default: ClaudeCode).
</span></span></span><span class="line"><span class="cl"><span class="s2">  BARK_SOUND          Notification sound (default: minuet).
</span></span></span><span class="line"><span class="cl"><span class="s2">  BARK_ICON           Custom icon URL.
</span></span></span><span class="line"><span class="cl"><span class="s2">  BARK_STOP_LEVEL     Level for Stop events (default: passive).
</span></span></span><span class="line"><span class="cl"><span class="s2">  BARK_NOTIFY_LEVEL   Level for Notification events (default: timeSensitive).
</span></span></span><span class="line"><span class="cl"><span class="s2">                      Valid: active | timeSensitive | passive | critical.
</span></span></span><span class="line"><span class="cl"><span class="s2">  CLAUDE_NOTIFY_OFF   If set to &#34;1&#34;, suppress all notifications.
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">annotations</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">shutil</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">urllib.request</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">TITLE_PREFIX</span> <span class="o">=</span> <span class="s2">&#34;ClaudeCode&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">MAX_BODY_LEN</span> <span class="o">=</span> <span class="mi">200</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">read_stdin_json</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">raw</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">raw</span><span class="o">.</span><span class="n">strip</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">last_assistant_text</span><span class="p">(</span><span class="n">transcript_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">p</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">transcript_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">is_file</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">p</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s2">&#34;r&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="s2">&#34;replace&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">lines</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">readlines</span><span class="p">()[</span><span class="o">-</span><span class="mi">30</span><span class="p">:]</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="n">lines</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">entry</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="cl">        <span class="n">msg</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;message&#34;</span><span class="p">)</span> <span class="ow">or</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">msg</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;role&#34;</span><span class="p">)</span> <span class="o">!=</span> <span class="s2">&#34;assistant&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="cl">        <span class="n">content</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">text</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">text</span> <span class="o">=</span> <span class="n">content</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="nb">list</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">block</span> <span class="ow">in</span> <span class="n">content</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">block</span><span class="p">,</span> <span class="nb">dict</span><span class="p">)</span> <span class="ow">and</span> <span class="n">block</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;type&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;text&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">t</span> <span class="o">=</span> <span class="n">block</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;text&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="k">if</span> <span class="n">t</span><span class="o">.</span><span class="n">strip</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">                        <span class="n">text</span> <span class="o">=</span> <span class="n">t</span>
</span></span><span class="line"><span class="cl">                        <span class="k">break</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">text</span><span class="o">.</span><span class="n">strip</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">text</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">truncate</span><span class="p">(</span><span class="n">s</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">n</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">s</span> <span class="o">=</span> <span class="p">(</span><span class="n">s</span> <span class="ow">or</span> <span class="s2">&#34;&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">n</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">s</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">s</span><span class="p">[:</span> <span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="s2">&#34;…&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">send_mac_notification</span><span class="p">(</span><span class="n">title</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">body</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">tn</span> <span class="o">=</span> <span class="n">shutil</span><span class="o">.</span><span class="n">which</span><span class="p">(</span><span class="s2">&#34;terminal-notifier&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">tn</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="p">[</span><span class="n">tn</span><span class="p">,</span> <span class="s2">&#34;-title&#34;</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="s2">&#34;-message&#34;</span><span class="p">,</span> <span class="n">body</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                 <span class="s2">&#34;-sound&#34;</span><span class="p">,</span> <span class="s2">&#34;Glass&#34;</span><span class="p">,</span> <span class="s2">&#34;-group&#34;</span><span class="p">,</span> <span class="s2">&#34;claude-code&#34;</span><span class="p">,</span> <span class="s2">&#34;-ignoreDnD&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">                <span class="n">check</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">DEVNULL</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">DEVNULL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">pass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">esc</span><span class="p">(</span><span class="n">s</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\\</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\\\\</span><span class="s2">&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s1">&#39;&#34;&#39;</span><span class="p">,</span> <span class="s1">&#39;</span><span class="se">\\</span><span class="s1">&#34;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">script</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="sa">f</span><span class="s1">&#39;display notification &#34;</span><span class="si">{</span><span class="n">esc</span><span class="p">(</span><span class="n">body</span><span class="p">)</span><span class="si">}</span><span class="s1">&#34; &#39;</span>
</span></span><span class="line"><span class="cl">        <span class="sa">f</span><span class="s1">&#39;with title &#34;</span><span class="si">{</span><span class="n">esc</span><span class="p">(</span><span class="n">title</span><span class="p">)</span><span class="si">}</span><span class="s1">&#34; sound name &#34;Glass&#34;&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="p">[</span><span class="s2">&#34;osascript&#34;</span><span class="p">,</span> <span class="s2">&#34;-e&#34;</span><span class="p">,</span> <span class="n">script</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">            <span class="n">check</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">DEVNULL</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">DEVNULL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">pass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">send_bark_push</span><span class="p">(</span><span class="n">title</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">body</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">event</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">key</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;BARK_KEY&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">key</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">server</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;BARK_SERVER&#34;</span><span class="p">,</span> <span class="s2">&#34;https://api.day.app&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s2">&#34;/&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">event</span> <span class="o">==</span> <span class="s2">&#34;Notification&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">level</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;BARK_NOTIFY_LEVEL&#34;</span><span class="p">,</span> <span class="s2">&#34;timeSensitive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">event</span> <span class="o">==</span> <span class="s2">&#34;Stop&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">level</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;BARK_STOP_LEVEL&#34;</span><span class="p">,</span> <span class="s2">&#34;passive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">level</span> <span class="o">=</span> <span class="s2">&#34;active&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;title&#34;</span><span class="p">:</span> <span class="n">title</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;body&#34;</span><span class="p">:</span> <span class="n">body</span> <span class="ow">or</span> <span class="s2">&#34; &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;group&#34;</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;BARK_GROUP&#34;</span><span class="p">,</span> <span class="s2">&#34;ClaudeCode&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;level&#34;</span><span class="p">:</span> <span class="n">level</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;sound&#34;</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;BARK_SOUND&#34;</span><span class="p">,</span> <span class="s2">&#34;minuet&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">icon</span> <span class="o">:=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;BARK_ICON&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">payload</span><span class="p">[</span><span class="s2">&#34;icon&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">icon</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span> <span class="o">=</span> <span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">Request</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">server</span><span class="si">}</span><span class="s2">/</span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">data</span><span class="o">=</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">ensure_ascii</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">&#34;utf-8&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">method</span><span class="o">=</span><span class="s2">&#34;POST&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;Content-Type&#34;</span><span class="p">:</span> <span class="s2">&#34;application/json; charset=utf-8&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">urllib</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span> <span class="k">as</span> <span class="n">r</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">r</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">pass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;CLAUDE_NOTIFY_OFF&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;1&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">event</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="k">else</span> <span class="s2">&#34;Stop&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="n">read_stdin_json</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">cwd</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;cwd&#34;</span><span class="p">)</span> <span class="ow">or</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">project</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="n">cwd</span><span class="p">)</span> <span class="k">if</span> <span class="n">cwd</span> <span class="k">else</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">event</span> <span class="o">==</span> <span class="s2">&#34;Stop&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">title</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">TITLE_PREFIX</span><span class="si">}</span><span class="s2"> - </span><span class="si">{</span><span class="n">project</span><span class="si">}</span><span class="s2">&#34;</span> <span class="k">if</span> <span class="n">project</span> <span class="k">else</span> <span class="n">TITLE_PREFIX</span>
</span></span><span class="line"><span class="cl">        <span class="n">body</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">transcript</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;transcript_path&#34;</span><span class="p">)</span> <span class="ow">or</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">transcript</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">body</span> <span class="o">=</span> <span class="n">last_assistant_text</span><span class="p">(</span><span class="n">transcript</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">body</span><span class="o">.</span><span class="n">strip</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="n">body</span> <span class="o">=</span> <span class="s2">&#34;Task completed, please review results.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">event</span> <span class="o">==</span> <span class="s2">&#34;Notification&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">title</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">TITLE_PREFIX</span><span class="si">}</span><span class="s2"> - Needs Attention&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">project</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">title</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="s2"> - </span><span class="si">{</span><span class="n">project</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">body</span> <span class="o">=</span> <span class="p">(</span><span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;message&#34;</span><span class="p">)</span> <span class="ow">or</span> <span class="s2">&#34;&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> \
</span></span><span class="line"><span class="cl">            <span class="ow">or</span> <span class="s2">&#34;Claude is waiting for your input or approval.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">title</span> <span class="o">=</span> <span class="n">TITLE_PREFIX</span>
</span></span><span class="line"><span class="cl">        <span class="n">body</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;Event received: </span><span class="si">{</span><span class="n">event</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">body</span> <span class="o">=</span> <span class="n">truncate</span><span class="p">(</span><span class="n">body</span><span class="p">,</span> <span class="n">MAX_BODY_LEN</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">send_mac_notification</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">send_bark_push</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">body</span><span class="p">,</span> <span class="n">event</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</span></span></code></pre></div><p>A few details worth flagging:</p>
<ul>
<li><strong>Walking the transcript backwards</strong> — read the last 30 JSONL lines, iterate in reverse, take the first entry where <code>role == &quot;assistant&quot;</code> with non-empty text. The <code>content</code> field can be either a string or an array depending on SDK version; both shapes are handled.</li>
<li><strong>Truncation uses <code>…</code></strong> — a single ellipsis character, not three ASCII dots. Saves two bytes for actual prose.</li>
<li><strong>Bark <code>level</code> differs by event</strong> — <code>Stop</code> defaults to <code>passive</code> (silent, just lands in the push list), <code>Notification</code> defaults to <code>timeSensitive</code> (pierces Focus mode). Long jobs finishing shouldn&rsquo;t shriek; mid-task confirmations must.</li>
<li><strong>Silent failure</strong> — Bark unreachable, osascript erroring, transcript missing — all caught and swallowed. A hook should never wedge the model.</li>
</ul>
<h2 id="3-wiring-it-into-claudesettingsjson">3. Wiring it into <code>~/.claude/settings.json</code></h2>
<p>Merge this block into <code>settings.json</code>, <strong>preserving your existing <code>env / permissions / enabledPlugins</code></strong> etc.:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;hooks&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;Notification&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;matcher&#34;</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;hooks&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">          <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;command&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;python3 /Users/you/.claude/hooks/notify.py Notification&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;timeout&#34;</span><span class="p">:</span> <span class="mi">15</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;Stop&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;matcher&#34;</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;hooks&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">          <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;command&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;python3 /Users/you/.claude/hooks/notify.py Stop&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;timeout&#34;</span><span class="p">:</span> <span class="mi">15</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>matcher: &quot;&quot;</code> is &ldquo;match anything&rdquo; — <code>Stop</code> and <code>Notification</code> don&rsquo;t have sub-matchers anyway. The <code>timeout: 15</code> seconds is a backstop; timing out doesn&rsquo;t break the main flow.</p>
<div class="callout callout-warn" role="note">
  <div class="callout-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
  <div class="callout-body"><p class="callout-title">Path must be absolute</p>
    The hook command&rsquo;s working directory is unpredictable — it depends on which project Claude is sitting in at the time. <code>~/.claude/hooks/notify.py</code> won&rsquo;t tilde-expand here. Spell it out: <code>/Users/&lt;you&gt;/.claude/hooks/notify.py</code>.
  </div>
</div>

<h2 id="4-enabling-bark">4. Enabling Bark</h2>
<p>Bark is a long-running iOS push tool. The free key comes from <a href="https://day.app/" target="_blank" rel="noopener">day.app<span class="external-mark" aria-hidden="true">↗</span></a>
. Install the app and the home screen shows a URL like <code>https://api.day.app/abc123xyz/</code> — the middle slug is your device key.</p>
<p>Drop it into the <code>env</code> block of <code>settings.json</code> (hooks inherit env from there):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;env&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;BARK_KEY&#34;</span><span class="p">:</span> <span class="s2">&#34;abc123xyz&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Quick reference for the optional knobs:</p>
<table>
  <thead>
      <tr>
          <th>Variable</th>
          <th>Default</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>BARK_SERVER</code></td>
          <td><code>https://api.day.app</code></td>
          <td>Override for self-hosted Bark</td>
      </tr>
      <tr>
          <td><code>BARK_GROUP</code></td>
          <td><code>ClaudeCode</code></td>
          <td>Notification group (folds on phone)</td>
      </tr>
      <tr>
          <td><code>BARK_SOUND</code></td>
          <td><code>minuet</code></td>
          <td>Sound name</td>
      </tr>
      <tr>
          <td><code>BARK_STOP_LEVEL</code></td>
          <td><code>passive</code></td>
          <td>Level for &ldquo;task finished&rdquo; pushes</td>
      </tr>
      <tr>
          <td><code>BARK_NOTIFY_LEVEL</code></td>
          <td><code>timeSensitive</code></td>
          <td>Level for &ldquo;needs your input&rdquo; pushes</td>
      </tr>
      <tr>
          <td><code>CLAUDE_NOTIFY_OFF</code></td>
          <td>(unset)</td>
          <td>Set to <code>1</code> to mute everything temporarily</td>
      </tr>
  </tbody>
</table>
<h2 id="5-verification">5. Verification</h2>
<p>With <code>BARK_KEY</code> set, fire a manual Notification simulation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">BARK_KEY</span><span class="o">=</span>your_key <span class="nb">echo</span> <span class="s1">&#39;{&#34;cwd&#34;:&#34;/tmp&#34;,&#34;message&#34;:&#34;test&#34;}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 ~/.claude/hooks/notify.py Notification
</span></span></code></pre></div><p>Expected:</p>
<ol>
<li>A macOS toast pops in the top-right corner.</li>
<li>A Bark notification arrives on your phone, grouped under <code>ClaudeCode</code>, level <code>timeSensitive</code>.</li>
</ol>
<p>To preview local-only behavior, just unset <code>BARK_KEY</code> for the call.</p>
<h2 id="6-gotchas">6. Gotchas</h2>
<div class="callout callout-warn" role="note">
  <div class="callout-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
  <div class="callout-body"><p class="callout-title">macOS notification permissions</p>
    <code>osascript display notification</code> posts under the identity of the <strong>terminal app that spawned it</strong> (Terminal / iTerm / Ghostty / Warp). If nothing appears at all: System Settings → Notifications → find the terminal in the list → allow notifications.
  </div>
</div>

<p><strong>Hook not firing?</strong> The Claude Code settings watcher only watches directories that already had a settings file at session start. If <code>.claude/</code> was created mid-session, edits may not reload. Open the <code>/hooks</code> menu once (forces a reload), or restart the Claude Code session.</p>
<p><strong><code>chmod</code> denied in your permissions?</strong> My settings include <code>Bash(chmod:*)</code> in <code>permissions.deny</code>, so the script can&rsquo;t be <code>chmod +x</code>&rsquo;d. Fix: invoke via <code>python3 path/to/notify.py</code> so the executable bit is irrelevant. Bonus — it&rsquo;s more explicit and removes a hidden piece of state.</p>
<p><strong>Body too long?</strong> The script caps at <code>MAX_BODY_LEN = 200</code> (199 chars + <code>…</code>). macOS Notification Center will truncate too, but capping early avoids ugly mid-JSON breakage on the Bark side.</p>
<p><strong>Want to dial it back?</strong> <code>Stop</code> fires every turn — easy to find noisy. Two ways out:</p>
<ul>
<li>The Bark side already uses <code>BARK_STOP_LEVEL = passive</code> — silent push, lands in the list only.</li>
<li>For local silence on Stop too: add <code>if event == &quot;Stop&quot;: return</code> to the top of <code>send_mac_notification</code>. Five-line change.</li>
</ul>
<h2 id="7-where-to-take-it-next">7. Where to take it next</h2>
<p>Saving these for future me:</p>
<ul>
<li><strong>Telegram / Lark / Slack</strong> — rename <code>send_bark_push</code> to <code>send_remote_push</code>, dispatch on a <code>REMOTE_KIND</code> env var. Bark is just one of many.</li>
<li><strong>Sharper Notification copy</strong> — currently I just pass through whatever <code>message</code> the model sent. Could branch on <code>tool_name</code> (if hook payload starts including it) for sharper titles.</li>
<li><strong>Debounce a burst</strong> — multiple <code>Stop</code> events within 5s collapsed into one. Right now every turn rings.</li>
<li><strong>Stats</strong> — append a line to <code>~/.claude/hooks/notify.log</code> per fire. A week&rsquo;s worth of &ldquo;how many turns ran, how often the model got blocked on input&rdquo; tells you something interesting.</li>
</ul>
<hr>
<p>You set this up once and forget about it. When the model finishes a turn or stalls waiting for input, a soft chime in the top-right and a buzz in your pocket — long jobs can finally run while you make a coffee.</p>
]]></content:encoded></item></channel></rss>