<?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>极简双语 Hugo 写作模板，为长读、慢写而生。</description><generator>Hugo</generator><language>zh-cn</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/tags/agent/index.xml" rel="self" type="application/rss+xml"/><item><title>一台机器配置多个 GitHub 账号</title><link>https://me.125520.xyz/github-multi-account-config/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://me.125520.xyz/github-multi-account-config/</guid><description>SSH 密钥隔离 + Git 条件配置 + Host 别名，让个人项目和工作项目各走各的账号，自动且干净。</description><content:encoded><![CDATA[<blockquote>
<p>一台开发机上同时维护个人项目和工作项目，提交记录希望用不同的 GitHub 账号。不想每次 clone 后手动改 <code>user.email</code>，也不想搞全局切换脚本。SSH 密钥隔离 + Git 条件配置就能解决，配完一劳永逸。</p>
</blockquote>
<h2 id="1-场景">1. 场景</h2>
<p>典型需求：</p>
<ul>
<li><strong>个人项目</strong>（<code>~/personal/*</code>）用个人 GitHub 账号提交</li>
<li><strong>工作项目</strong>（<code>~/work/*</code>）用公司 GitHub 账号提交</li>
<li>每个账号有独立的 SSH 密钥，互不干扰</li>
<li>进入不同目录自动切换身份，无需手动配置</li>
</ul>
<p>额外痛点：</p>
<ul>
<li>公司网络可能封 SSH 默认端口 22，需要走 443</li>
<li>有些项目需要用别名（如 <code>work.github.com</code>）区分 remote URL</li>
</ul>
<h2 id="2-整体思路">2. 整体思路</h2>
<p>三层配置协同工作：</p>
<ol>
<li><strong>SSH 层</strong>（<code>~/.ssh/config</code>）：为不同账号配置独立密钥和 Host 别名</li>
<li><strong>Git 全局层</strong>（<code>~/.gitconfig</code>）：设置默认账号和 SSH 命令</li>
<li><strong>Git 条件层</strong>（<code>~/.gitconfig-work</code>）：按目录路径自动覆盖账号信息</li>
</ol>
<p>关键机制：<code>includeIf</code> 让 Git 在特定目录下加载额外配置文件。</p>
<h2 id="3-步骤">3. 步骤</h2>
<h3 id="31-生成-ssh-密钥对">3.1 生成 SSH 密钥对</h3>
<p>为每个账号生成独立密钥：</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"># 个人账号密钥</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"># 工作账号密钥</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>生成后会得到：</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>将 <code>.pub</code> 公钥分别添加到对应 GitHub 账号的 SSH keys 设置页面。</p>
<h3 id="32-配置-ssh-host-别名">3.2 配置 SSH Host 别名</h3>
<p>编辑 <code>~/.ssh/config</code>（没有则创建）：</p>
<pre tabindex="0"><code class="language-ssh-config" data-lang="ssh-config"># 个人账号（默认）
Host github.com
  HostName ssh.github.com
  User git
  IdentityFile ~/.ssh/id_rsa_personal
  Port 443

# 工作账号（使用别名 work.github.com）
Host work.github.com
  HostName ssh.github.com
  User git
  IdentityFile ~/.ssh/id_rsa_work
  Port 443
</code></pre><p><strong>重点说明</strong>：</p>
<ul>
<li><strong>Port 443</strong>：绕过防火墙对 22 端口的封锁，GitHub 官方支持 <code>ssh.github.com:443</code></li>
<li><strong>Host 别名</strong>：<code>work.github.com</code> 是本地别名，实际连接的还是 <code>ssh.github.com</code></li>
<li><strong>IdentityFile</strong>：指定使用哪个密钥</li>
</ul>
<p>测试连接：</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"># 测试个人账号</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"># 预期输出：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"># 测试工作账号</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"># 预期输出：Hi work-username! You&#39;ve successfully authenticated...</span>
</span></span></code></pre></div><h3 id="33-配置-git-全局默认">3.3 配置 Git 全局默认</h3>
<p>编辑 <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"># 全局强制使用个人账号密钥</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"># 条件配置：work 目录自动切换账号</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>关键点</strong>：</p>
<ul>
<li><code>sshCommand</code> 强制使用指定密钥，<code>-F /dev/null</code> 阻止 <code>~/.ssh/config</code> 干扰（可选，取决于你的优先级策略）</li>
<li><code>includeIf</code> 匹配路径时，<code>gitdir:</code> 后的路径<strong>必须以 <code>/</code> 结尾</strong>才能匹配子目录</li>
<li>路径支持 <code>~</code> 展开和通配符 <code>*</code></li>
</ul>
<h3 id="34-配置工作账号条件文件">3.4 配置工作账号条件文件</h3>
<p>创建 <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>现在逻辑是：</p>
<ul>
<li>默认用个人账号（<code>~/.gitconfig</code>）</li>
<li>进入 <code>~/work/</code> 任何子目录，自动切换到工作账号（<code>~/.gitconfig-work</code> 覆盖）</li>
</ul>
<h3 id="35-使用-host-别名-clone-工作项目">3.5 使用 Host 别名 clone 工作项目</h3>
<p>工作项目 clone 时需要修改 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"># 原始 URL（会用个人账号）</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"># 改用 Host 别名（会用工作账号密钥）</span>
</span></span><span class="line"><span class="cl">git clone git@work.github.com:company/project.git
</span></span></code></pre></div><p>或者 clone 后修改 remote：</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>为什么需要别名？</strong></p>
<p>即使在 <code>~/work/</code> 目录下，如果 remote URL 是 <code>git@github.com</code>，SSH 会优先匹配 <code>~/.ssh/config</code> 中的 <code>Host github.com</code> 配置。Host 别名让你在 URL 层面就区分账号。</p>
<h2 id="4-验证配置">4. 验证配置</h2>
<h3 id="41-检查当前目录生效的配置">4.1 检查当前目录生效的配置</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"># 在个人项目目录</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"># 输出：personal@example.com</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 在工作项目目录</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"># 输出：work@company.com</span>
</span></span></code></pre></div><h3 id="42-检查-ssh-连接使用的密钥">4.2 检查 SSH 连接使用的密钥</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"># 查看实际使用的 SSH 命令</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"># 手动测试（带详细日志）</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"># 输出中会显示 &#34;Offering public key: /path/to/id_rsa_xxx&#34;</span>
</span></span></code></pre></div><h3 id="43-测试提交身份">4.3 测试提交身份</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"># 创建测试提交</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"># 查看提交的 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-常见问题">5. 常见问题</h2>
<h3 id="51-端口-22-被墙">5.1 端口 22 被墙</h3>
<p>症状：<code>ssh -T git@github.com</code> 超时</p>
<p>解决：在 <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 官方在 <code>ssh.github.com</code> 的 443 端口提供 SSH 服务，专门用于绕过防火墙。</p>
<h3 id="52-密钥权限问题">5.2 密钥权限问题</h3>
<p>症状：<code>Permissions 0644 for 'id_rsa_work' are too open</code></p>
<p>解决：</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>私钥必须只有所有者可读写（<code>600</code>），公钥可以宽松一些（<code>644</code>）。</p>
<h3 id="53-includeif-不生效">5.3 <code>includeIf</code> 不生效</h3>
<p>检查清单：</p>
<ol>
<li><strong>路径必须以 <code>/</code> 结尾</strong>：<code>gitdir:~/work/</code>（对）vs <code>gitdir:~/work</code>（错）</li>
<li><strong>Git 版本</strong>：<code>includeIf</code> 需要 Git 2.13+，运行 <code>git --version</code> 确认</li>
<li><strong>路径展开</strong>：<code>~</code> 会展开，但相对路径不会，建议用绝对路径或 <code>~/</code></li>
<li><strong>调试</strong>：运行 <code>git config --show-origin user.email</code> 查看配置来源</li>
</ol>
<h3 id="54-已有项目切换账号">5.4 已有项目切换账号</h3>
<p>如果项目已经用错误账号提交过：</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. 修改 remote URL（如果需要）</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. 修改最近一次提交的 author（如果还没 push）</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. 批量修改历史提交（慎用，会改写历史）</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-进阶更细粒度的控制">6. 进阶：更细粒度的控制</h2>
<h3 id="61-按项目单独配置">6.1 按项目单独配置</h3>
<p>如果某个项目需要特殊配置，在项目内运行：</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>项目级配置（<code>.git/config</code>）优先级最高，会覆盖全局和条件配置。</p>
<h3 id="62-多个工作目录">6.2 多个工作目录</h3>
<p>如果有多个工作目录：</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-协议多账号">6.3 HTTPS 协议多账号</h3>
<p>本文聚焦 SSH，但 HTTPS 也可以多账号：</p>
<ul>
<li>macOS Keychain 存储凭据，<code>git-credential-osxkeychain</code> 管理</li>
<li>或用 GitHub CLI：<code>gh auth login</code> 支持多账号切换</li>
<li>条件配置中可以设置不同的 <code>credential.helper</code></li>
</ul>
<h2 id="7-总结">7. 总结</h2>
<p>配置后的工作流：</p>
<ol>
<li>个人项目放在 <code>~/personal/</code>，自动用个人账号</li>
<li>工作项目放在 <code>~/work/</code>，自动用工作账号</li>
<li>Clone 工作项目时记得用 <code>git@work.github.com</code></li>
<li>忘记配置也没关系，提交前 <code>git config user.email</code> 检查一下</li>
</ol>
<p>核心收益：</p>
<ul>
<li><strong>零手动切换</strong>：进入目录自动生效</li>
<li><strong>隔离彻底</strong>：密钥、身份、目录三层隔离</li>
<li><strong>可验证</strong>：每步都有明确的测试方法</li>
</ul>
<p>这套配置用了两年，再没有提交记录用错账号的情况。</p>
]]></content:encoded></item><item><title>Claude Code 高级用法盘点：Hook、MCP、并行 Agent、Worktree 与上下文管理</title><link>https://me.125520.xyz/claude-code-advanced-tips/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://me.125520.xyz/claude-code-advanced-tips/</guid><description>把 Claude Code 用到中高级阶段会真正用到的 8 个模块梳一遍：Hook、CLAUDE.md、MCP、并行 Agent、Worktree、Slash 命令、权限模型、上下文管理。每节附我自己在用的配置。</description><content:encoded><![CDATA[<blockquote>
<p>用 Claude Code 半年，从一开始把它当 &ldquo;VSCode + Cursor 的某种替代&rdquo;，到现在更像在管理一个能并行的小团队。这篇是把我自己日常真正在用的高级功能梳一遍 —— hook、MCP、subagent、worktree、CLAUDE.md 分层、<code>/clear</code> 时机这些。不是官方文档复述，是有取舍判断的使用者视角。读完应该能直接抄走改改就用。</p>
</blockquote>
<h2 id="0-文章地图">0. 文章地图</h2>
<p>8 个模块，按&quot;先操作后理念&quot;排：</p>
<table>
  <thead>
      <tr>
          <th>#</th>
          <th>模块</th>
          <th>类型</th>
          <th>一句话</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>Hook 系统</td>
          <td>操作</td>
          <td>在生命周期事件上挂自己的脚本</td>
      </tr>
      <tr>
          <td>2</td>
          <td>CLAUDE.md 分层</td>
          <td>操作</td>
          <td>给 Agent 一份会跟着它跑的项目说明书</td>
      </tr>
      <tr>
          <td>3</td>
          <td>MCP Server</td>
          <td>操作</td>
          <td>把任何外部能力包成 Agent 可调用的工具</td>
      </tr>
      <tr>
          <td>4</td>
          <td>并行 Agent</td>
          <td>操作</td>
          <td>用 Task 工具一次派出多个 subagent</td>
      </tr>
      <tr>
          <td>5</td>
          <td>Worktree 隔离</td>
          <td>操作</td>
          <td>git worktree 把多个 session 在物理目录上隔开</td>
      </tr>
      <tr>
          <td>6</td>
          <td>Slash 命令 / Skill</td>
          <td>操作</td>
          <td>把重复工作流固化成 <code>/cmd</code></td>
      </tr>
      <tr>
          <td>7</td>
          <td>权限与安全模型</td>
          <td>理念</td>
          <td>allow/deny/skip 三档背后的取舍</td>
      </tr>
      <tr>
          <td>8</td>
          <td>上下文管理</td>
          <td>理念</td>
          <td><code>/clear</code>、<code>/compact</code>、context rot 怎么对付</td>
      </tr>
  </tbody>
</table>
<p>操作类都附完整的、能复制就跑的片段；理念类讲的是&quot;为什么这样设计、什么时候动手&quot;。</p>
<h2 id="1-hook-系统在生命周期事件上挂脚本">1. Hook 系统：在生命周期事件上挂脚本</h2>
<p>Hook 是 Claude Code 最低估的一个能力。它把整个会话切成一组生命周期事件，让你在每个点插入自己的脚本：</p>
<table>
  <thead>
      <tr>
          <th>事件</th>
          <th>触发时机</th>
          <th>我用它干嘛</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SessionStart</code></td>
          <td>会话开始</td>
          <td>注入项目状态、git status、未读 issue</td>
      </tr>
      <tr>
          <td><code>UserPromptSubmit</code></td>
          <td>用户回车之后、模型读到之前</td>
          <td>改写或拒绝 prompt（敏感词、注入项目元数据）</td>
      </tr>
      <tr>
          <td><code>PreToolUse</code></td>
          <td>工具调用前</td>
          <td>拦截危险命令、拒绝写入特定目录</td>
      </tr>
      <tr>
          <td><code>PostToolUse</code></td>
          <td>工具调用后</td>
          <td>自动 lint、自动 test、把变更写到 changelog</td>
      </tr>
      <tr>
          <td><code>Notification</code></td>
          <td>模型等输入</td>
          <td>弹通知/手机推送</td>
      </tr>
      <tr>
          <td><code>Stop</code></td>
          <td>主回合结束</td>
          <td>收尾通知、写一行日志</td>
      </tr>
      <tr>
          <td><code>SubagentStop</code></td>
          <td>subagent 回合结束</td>
          <td>给并行 agent 单独计时/提醒</td>
      </tr>
  </tbody>
</table>
<p>注册位置在 <code>~/.claude/settings.json</code> 或者项目里的 <code>.claude/settings.json</code>。最常见的两种用法：</p>
<p><strong>用法一：模型动作前后挂自动化。</strong> 例如每次 Edit 之后自动跑一遍 ESLint。把 lint 错误回灌到模型上下文，比&quot;你忘了 lint 我帮你催一下&quot;靠谱得多 —— 模型会在下一轮直接修。</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> 支持工具名的正则 —— <code>Edit|Write</code> 表示文件写入类工具触发。脚本从 stdin 拿到 hook payload（包括被改的文件路径），跑 lint、把错误打到 stdout 即可。退出码非 0 时 Claude Code 会把 stderr 当作模型输入。</p>
<p><strong>用法二：把会话状态接到外部世界。</strong> 我之前写过 <a href="/claude-code-mac-bark-notify/">一篇专门讲 Mac 通知 + Bark 推送</a>
 的文章，本质就是 <code>Stop</code> + <code>Notification</code> 两个事件接一个 Python 脚本，本地弹一条 macOS 通知，远程发一条手机推送。长任务跑完不用一直盯屏幕。</p>
<div class="callout callout-info" role="note">
  <div class="callout-icon" aria-hidden="true"></div>
  <div class="callout-body"><p class="callout-title">路径必须绝对</p>
    hook command 的 cwd 不可预期（取决于会话当时在哪个项目里），<code>~/...</code> 这种波浪号也不会展开。所有路径写绝对路径。
  </div>
</div>

<p>Hook 真正的强项是 <strong>它把&quot;提醒模型做某件事&quot;变成了&quot;由系统强制做某件事&quot;</strong>。指令型的提示再怎么写也会被遗忘，hook 不会。</p>
<h2 id="2-claudemd给-agent-的项目说明书">2. CLAUDE.md：给 Agent 的项目说明书</h2>
<p>CLAUDE.md 看起来很简单 —— 就是一份 Markdown，自动注入每次会话的 system prompt。但它的真正威力来自分层：</p>
<pre tabindex="0"><code>~/.claude/CLAUDE.md         # 全局：所有项目都注入
&lt;repo&gt;/CLAUDE.md            # 项目级：cd 到这个 repo 才注入
&lt;repo&gt;/&lt;subdir&gt;/CLAUDE.md   # 子目录级：进入子目录的工作才注入
</code></pre><p>合并顺序是从外到内，越靠内的优先级越高。结合用法：</p>
<ul>
<li><strong>全局</strong> 写&quot;我是谁、偏好什么&quot;。例如&quot;我是 Jason，写代码时优先函数式风格&quot;，&ldquo;中文回复偏正式术语，避免口语化译法&rdquo;。</li>
<li><strong>项目级</strong> 写&quot;这个 repo 的隐式规则&quot;。例如&quot;用 pnpm 不用 npm&quot;、&ldquo;提交前必须跑 <code>pnpm test</code>&quot;、&ldquo;Tailwind 颜色只用 <code>tokens.ts</code> 里定义的，不准 <code>bg-red-500</code>&quot;。</li>
<li><strong>子目录级</strong> 写&quot;这块代码的局部约定&rdquo;。例如 <code>apps/api/CLAUDE.md</code> 写&quot;所有 endpoint 必须有 zod schema 验证&rdquo;。</li>
</ul>
<p>写什么最有效？我自己摸出来的几条：</p>
<ol>
<li><strong>写&quot;模型容易踩的坑&quot;，不写&quot;显然的规则&quot;。</strong> &ldquo;用 React&rdquo; 不用写，模型会自己看 package.json。&ldquo;我们的 useFetch 必须放在 try/catch 外层因为内部已经 catch&rdquo; —— 这种写。</li>
<li><strong>写&quot;一次教会、永久有效的&quot;。</strong> Boris Cherny 那句&quot;compounding engineering&quot; —— 每次 Claude 犯的错被记下来都是永久红利。把&quot;昨天它又把 logger 引错位置了&quot;这种小坑加进去。</li>
<li><strong>每条尽量给原因。</strong> &ldquo;用 X 不用 Y&rdquo; 不如&quot;用 X 不用 Y，因为 Y 会触发 ESM/CJS 互引问题&quot;。模型知道为什么之后泛化能力强一档。</li>
<li><strong>保持精简。</strong> CLAUDE.md 每次会话都加载，吃上下文。我个人的红线是单文件不超过 200 行。超过就考虑拆到子目录或 slash command 按需加载。</li>
</ol>
<p>一份我自己在用的项目级模板片段：</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) + 自定义主题 inkstone (git submodule)
</span></span><span class="line"><span class="cl"><span class="k">-</span> 内容在 content/posts/，中英双语：foo.md + foo.en.md
</span></span><span class="line"><span class="cl"><span class="k">-</span> 通过 translationKey 关联两版
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## 写作约定
</span></span></span><span class="line"><span class="cl"><span class="k">-</span> 第一人称、有取舍判断
</span></span><span class="line"><span class="cl"><span class="k">-</span> 引言用 blockquote 起手
</span></span><span class="line"><span class="cl"><span class="k">-</span> 长文在第 0 节给文章地图
</span></span><span class="line"><span class="cl"><span class="k">-</span> 避免口语化译法（专业术语用正式中文，不用流行翻译腔）
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## 操作约定
</span></span></span><span class="line"><span class="cl"><span class="k">-</span> 启动本地预览：<span class="sb">`hugo server -D`</span>
</span></span><span class="line"><span class="cl"><span class="k">-</span> 提交前跑：<span class="sb">`hugo --minify`</span> 确保没有报错
</span></span><span class="line"><span class="cl"><span class="k">-</span> 永远不直接 <span class="sb">`git push`</span>，让我自己来
</span></span></code></pre></div><p>最后一条是配合下面 §7 的权限模型用的 —— Agent 看到这条会克制自己。</p>
<h2 id="3-mcp-server把外部能力包成工具">3. MCP Server：把外部能力包成工具</h2>
<p>MCP（Model Context Protocol）是 Anthropic 开源的协议，作用是让任何外部工具/数据源都能以统一的方式接入到 Claude（或者其他兼容 client）。Anthropic 自己有一句话：MCP 之于 LLM 等价于 USB-C 之于硬件。</p>
<p>Claude Code 默认就内置了一些（filesystem、bash、web fetch），但真正解锁场景的是自定义 MCP server。三个原语对应 Agent 的三种&quot;知道方式&quot;：</p>
<table>
  <thead>
      <tr>
          <th>原语</th>
          <th>含义</th>
          <th>类比</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Tools</strong></td>
          <td>模型可以调用的动作</td>
          <td>函数</td>
      </tr>
      <tr>
          <td><strong>Resources</strong></td>
          <td>模型可以读取的数据</td>
          <td>文件</td>
      </tr>
      <tr>
          <td><strong>Prompts</strong></td>
          <td>用户可以触发的模板</td>
          <td>快捷指令</td>
      </tr>
  </tbody>
</table>
<p>注册一个 MCP server 到 Claude Code 最快的路径是用 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"># 加一个本地 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"># 加一个 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"># 看现在挂了哪些</span>
</span></span><span class="line"><span class="cl">claude mcp list
</span></span></code></pre></div><p>写一个最小的自定义 server 也不复杂。下面是一个&quot;读 Linear issue&quot;的最小骨架（Node.js + 官方 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>接进 Claude Code 之后，会话里直接说&quot;看一下 ENG-123 这条 issue&quot;，模型就会调 <code>get_issue</code>。</p>
<p>我的取舍判断：</p>
<ul>
<li><strong>能用现成 MCP 别自己写。</strong> GitHub、Notion、Slack、Postgres 这些都有官方/社区维护的 server，先去 <a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener">modelcontextprotocol.io<span class="external-mark" aria-hidden="true">↗</span></a>
 翻一遍。</li>
<li><strong>自定义 MCP 适合&quot;私有 / 内部 / 跨工具&quot;的能力。</strong> 例如把公司内部的状态板、自己的笔记库、家里的 NAS 接进来。</li>
<li><strong>MCP server 启动失败不会致命，但会降级</strong>。Claude Code 启动时如果 server 跑不起来，整个会话还是能跑，只是那个工具不可用。所以可以放心多挂几个。</li>
</ul>
<h2 id="4-并行-agent一次派多路-subagent">4. 并行 Agent：一次派多路 subagent</h2>
<p>Claude Code 内置了一个 <code>Task</code> 工具，可以在主会话里派出 subagent 并行干活。这不是噱头，是真正改变工作节奏的能力。</p>
<p>典型场景：</p>
<ul>
<li><strong>大重构</strong>：把&quot;重命名 X 到 Y&quot;拆成&quot;找所有引用&quot;、&ldquo;改 src/&quot;、&ldquo;改 test/&quot;、&ldquo;改 docs/&ldquo;四路并行</li>
<li><strong>多技术栈对照</strong>：让两个 subagent 分别用 React 和 Vue 实现同一个组件，回来对比</li>
<li><strong>代码审查</strong>：派一个 subagent 看安全、一个看性能、一个看可读性</li>
</ul>
<p>调用方式在主会话里就是说一句类似的话：&ldquo;请并行启动 3 个 subagent，分别检查 src/api/、src/components/、src/hooks/ 三个目录的类型错误，把结果汇总。&ldquo;模型会自动用 Task 工具 fan out。</p>
<p>如果想固化成标准流程，可以在 <code>.claude/agents/</code> 下定义命名 subagent：</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: 专注于安全漏洞的代码审查 agent
</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">你是一个专注于 OWASP Top 10 的代码审查者。
</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">-</span> **风险等级**: Critical / High / Medium / Low
</span></span><span class="line"><span class="cl"><span class="k">-</span> **位置**: file:line
</span></span><span class="line"><span class="cl"><span class="k">-</span> **类型**: SQL Injection / XSS / SSRF / ...
</span></span><span class="line"><span class="cl"><span class="k">-</span> **修复建议**: 具体改法
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">只看安全问题，不要给样式或性能反馈。
</span></span></code></pre></div><p>之后在主会话里&quot;用 security-reviewer 子代理审一下 src/api/&ldquo;就会精准触发。<code>tools:</code> 字段限制 subagent 能用的工具（不给它 Edit/Write 就让它只读）。</p>
<p>几条用了一阵之后的体会：</p>
<ul>
<li><strong>并行的极限大概在 5-8 个</strong>。再多 token 成本和上下文切换成本就上来了。</li>
<li><strong>subagent 不共享主会话的上下文</strong>。它只看自己的 prompt 和能调到的工具。所以指令必须自包含。</li>
<li><strong>结果的合并是主 agent 的活。</strong> 主 agent 拿回 N 份输出之后还要再做一轮整合，这一步的质量很大程度决定了并行的最终价值。</li>
<li><strong>不是所有任务都适合并行</strong>。强耦合的任务（A 的输出是 B 的输入）拆成并行反而更慢。判断标准：能不能写成&quot;互不影响、独立完成&quot;的若干份子任务。</li>
</ul>
<h2 id="5-worktree-隔离让多个-session-物理上分开">5. Worktree 隔离：让多个 session 物理上分开</h2>
<p>并行的另一种粒度是<strong>多个 Claude Code 主会话并行跑在同一个 repo 上</strong>。最大的坑是：它们都改同一个 working tree，互相覆盖。</p>
<p><code>git worktree</code> 是为这个量身打造的：</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"># 主 repo 在 ~/code/myapp（在 main 分支上正常开发）</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"># 派生两条独立分支的工作目录</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"># 现在物理上有 3 个目录：</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"># 共享同一个 .git，但工作树完全隔离</span>
</span></span></code></pre></div><p>然后开三个终端，各自 <code>cd</code> 到对应目录、<code>claude</code> 启动会话。三个 agent 同时干三件事，互不污染。</p>
<p>我自己的工作流：</p>
<ol>
<li>复杂任务先在主 repo <code>shift+tab</code> 进 plan mode，跟模型把任务拆成 3-5 个<strong>正交的</strong>子任务</li>
<li>每个子任务开一个 worktree（命名 <code>&lt;repo&gt;-&lt;task-slug&gt;</code>）</li>
<li>开三个终端窗口（用 tmux / Warp 的 split），各自跑一个 Claude</li>
<li>跑完之后回主 repo，用 <code>git worktree list</code> 检查，挨个 review、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"># 看现在有哪些 worktree</span>
</span></span><span class="line"><span class="cl">git worktree remove ../myapp-feat-a    <span class="c1"># 干完拆掉</span>
</span></span><span class="line"><span class="cl">git worktree prune                     <span class="c1"># 清理过期引用</span>
</span></span></code></pre></div><p>几条经验：</p>
<ul>
<li><strong>每个 worktree 用独立分支</strong>。在同一个分支上开两个 worktree 会被 git 拒绝（这是好事）。</li>
<li><strong>node_modules / .venv 不共享</strong>。每个 worktree 都要重新装。可以用 pnpm 的 store 或者 venv 的 symlink 缓解，但代价是配置变复杂，看权衡。</li>
<li><strong>跨 worktree 的环境变量、<code>.env</code> 不会自动复制</strong>。需要的话 <code>cp</code> 或者 symlink。</li>
<li><strong>worktree 不是 fork</strong>。所有 worktree 共享同一个 <code>.git/</code>，所以 <code>git fetch</code> 一次全更新，commit 也是同一个 object database。这是它比 <code>git clone</code> 多份快的原因。</li>
</ul>
<p>worktree + 并行 subagent 是两种不同的并行：subagent 是&quot;在一个会话里 fan out&rdquo;，worktree 是&quot;多个独立会话物理隔离&rdquo;。前者快但耦合，后者慢但干净。我一般在重构 / 跨模块改动时用 worktree，在调研 / 检查 / 生成类任务里用 subagent。</p>
<h2 id="6-slash-命令--skill把工作流固化">6. Slash 命令 / Skill：把工作流固化</h2>
<p><code>/command</code> 是 Claude Code 的另一个 compounding 投资点。任何你做过两次的工作流，第三次开始就该考虑把它变成 slash 命令。</p>
<p>定义位置：</p>
<pre tabindex="0"><code>~/.claude/commands/&lt;name&gt;.md         # 全局，所有项目都能用
&lt;repo&gt;/.claude/commands/&lt;name&gt;.md    # 项目级
</code></pre><p>文件本身就是一份 Markdown，会作为 prompt 注入。下面是我用的 <code>/review</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/commands/review.md --&gt;
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">description: 对当前分支 vs main 的 diff 做一轮自审
</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">请对当前分支相对于 main 的所有变更做一轮代码自审：
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> 跑 <span class="sb">`git diff main...HEAD --stat`</span> 看改了哪些文件
</span></span><span class="line"><span class="cl"><span class="k">2.</span> 跑 <span class="sb">`git diff main...HEAD`</span> 看具体内容
</span></span><span class="line"><span class="cl"><span class="k">3.</span> 按以下角度给反馈：
</span></span><span class="line"><span class="cl">   <span class="k">-</span> 有没有引入复杂度但没价值的抽象
</span></span><span class="line"><span class="cl">   <span class="k">-</span> 有没有破坏既有约定（参考 CLAUDE.md）
</span></span><span class="line"><span class="cl">   <span class="k">-</span> 测试覆盖是否够（重点看新增 public 函数）
</span></span><span class="line"><span class="cl">   <span class="k">-</span> 是否有遗留的 console.log、debugger、TODO
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">最后给出一份 markdown 形式的审查报告，可以直接贴进 PR。
</span></span></code></pre></div><p>会话里 <code>/review</code> 一敲就跑完整套。<code>allowed-tools</code> 字段限定只允许跑 git diff/log 类命令 —— 即使不在全局 allowlist 里也临时放行，保证安全。</p>
<p>支持参数也很简单，文件里用 <code>$ARGUMENTS</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/commands/explain.md --&gt;
</span></span><span class="line"><span class="cl">请详细解释 $ARGUMENTS 这段代码做了什么、为什么这样写、有没有更简单的写法。
</span></span></code></pre></div><p>会话里 <code>/explain src/utils/debounce.ts</code> 就会把路径替换进去。</p>
<p>我现在常驻的几条命令：</p>
<table>
  <thead>
      <tr>
          <th>命令</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/review</code></td>
          <td>自审当前分支</td>
      </tr>
      <tr>
          <td><code>/plan</code></td>
          <td>把一段需求拆成 todo 列表，等我审完再开干</td>
      </tr>
      <tr>
          <td><code>/explain &lt;path&gt;</code></td>
          <td>解释一段代码</td>
      </tr>
      <tr>
          <td><code>/tidy</code></td>
          <td>跑 lint + format + 修明显的 type 错误</td>
      </tr>
      <tr>
          <td><code>/post</code></td>
          <td>这个博客专用 —— 帮我起一个新文章的 front matter + 大纲</td>
      </tr>
  </tbody>
</table>
<p>Skill（更广义的&quot;打包能力&rdquo;）目前还在演进，但核心思路是一样的：<strong>把&quot;反复需要解释给 Claude 的工作流&quot;变成&quot;一行就能触发的资产&rdquo;</strong>。每条都是永久红利。</p>
<h2 id="7-权限与安全模型allow--deny--skip-三档">7. 权限与安全模型：allow / deny / skip 三档</h2>
<p>Claude Code 的权限系统是它跟普通&quot;AI 编辑器&quot;最大的区别之一。简单说有三档：</p>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>行为</th>
          <th>我什么时候用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Default (ask)</strong></td>
          <td>每个工具调用都问</td>
          <td>处理陌生项目、第一次用某 MCP</td>
      </tr>
      <tr>
          <td><strong>Auto-accept</strong></td>
          <td>在 allowlist 里的工具自动通过</td>
          <td>日常</td>
      </tr>
      <tr>
          <td><strong>Bypass / dangerouslySkipPermissions</strong></td>
          <td>全部放行</td>
          <td>沙盒里跑 / 一次性脚本</td>
      </tr>
  </tbody>
</table>
<p><code>settings.json</code> 里的 <code>permissions</code> 块是最常调的地方：</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 优先于 allow。我个人的风格偏保守：</p>
<ol>
<li><strong>能拒绝的危险动作全 deny。</strong> <code>rm -rf</code>、<code>git push</code>、<code>git commit</code>、<code>curl</code>（防 exfil）、<code>chmod</code>、<code>sudo</code>。</li>
<li><strong>常用读操作 allow。</strong> <code>git status</code>、<code>git log</code>、<code>ls</code>、<code>pwd</code>、<code>cat</code>、各种 test 命令。</li>
<li><strong>写操作不在 allowlist 里</strong>。<code>Edit</code> / <code>Write</code> 我反而留默认 ask —— 模型每次写文件都会再问一下，体感慢一点但底线很稳。</li>
</ol>
<p><code>--dangerously-skip-permissions</code>（启动参数）是核武器：所有权限检查全跳过。它不是给日常用的，是给以下场景：</p>
<ul>
<li><strong>Docker / VM 沙盒里</strong>，反正搞坏了重启就没了</li>
<li><strong>一次性脚本任务</strong>，例如批量重命名 1000 个文件，全程在自己看着的情况下跑</li>
<li><strong>CI / Headless</strong> 跑，整个环境本来就是临时的</li>
</ul>
<p>千万别在自己的工作环境里日常开 <code>dangerouslySkipPermissions</code>。代价是 —— 如果模型某次产生幻觉决定 <code>rm -rf ~</code>，你会哭。</p>
<p>更稳的做法是用 <strong>沙盒</strong>：把 Claude 跑在 devcontainer / Docker / Multipass VM 里，物理上隔离。Anthropic 自己的 <a href="https://docs.claude.com/" target="_blank" rel="noopener">安全最佳实践文档<span class="external-mark" aria-hidden="true">↗</span></a>
 就推荐了这种用法。</p>
<p>权限模型的真正用途是<strong>让 Agent 在合理范围内自主，越界时强制停下来问</strong>。allow 太松会被坑，deny 太多会变成每步都问的 &ldquo;co-pilot 模式&rdquo; 失去 agentic 价值。这个度需要项目和个人风格双重平衡。</p>
<h2 id="8-上下文管理context-是-king但会腐烂">8. 上下文管理：context 是 king，但会腐烂</h2>
<p>最后一条，理念为主。Andrej Karpathy 那句被引爛的话：</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>而 Anthropic 自家的研究反复确认了一个反直觉的事：<strong>上下文不是越多越好</strong>。模型在长上下文下会出现&quot;context rot&rdquo;（上下文腐烂）—— 即使是简单任务，输入越长性能越下滑。</p>
<p>所以 Claude Code 提供了几把刀：</p>
<table>
  <thead>
      <tr>
          <th>命令</th>
          <th>行为</th>
          <th>什么时候用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/clear</code></td>
          <td>清空上下文重启</td>
          <td>切换到完全无关的任务</td>
      </tr>
      <tr>
          <td><code>/compact</code></td>
          <td>让模型自己把当前对话压缩成摘要</td>
          <td>任务相关、但已经堆积太多废话</td>
      </tr>
      <tr>
          <td><code>/resume</code></td>
          <td>恢复之前 compact 掉的细节</td>
          <td>偶尔需要回头细看</td>
      </tr>
  </tbody>
</table>
<p>我的使用习惯：</p>
<ul>
<li><strong>任务边界是 <code>/clear</code> 的天然时机。</strong> 写完 feature A 要修 bug B，先 <code>/clear</code>。</li>
<li><strong>跑了 30+ 轮还在同一任务上、感觉模型开始&quot;漂&rdquo;，先 <code>/compact</code>。</strong> 体感上 <code>/compact</code> 之后模型会重新聚焦。</li>
<li><strong>CLAUDE.md 是&quot;永久上下文&rdquo;，不要让它变成废话仓库。</strong> 每条都该是高频用到的。</li>
<li><strong>真正长的参考资料别贴对话里，写到文件让模型 Read。</strong> <code>Read</code> 是按需加载，不贴整段 paste 进 prompt。</li>
</ul>
<p>更深一层的理念是 <strong>subagent 是上下文管理的工具</strong>。每派一个 subagent 出去，相当于给那个子任务一个干净的上下文窗口；它跑完只把摘要回传给主 agent。这就是为什么大重构用 subagent 比&quot;主 agent 一个人改&quot;更快 —— 不是因为并行省时间，是因为每个子上下文都更干净。</p>
<p>跟权限模型一样，上下文管理是个&quot;时机感&quot;问题，没有死规则。原则只有一条：<strong>当你感觉模型开始重复、绕圈、忘事，就该动手了</strong>。这个直觉是用出来的。</p>
<h2 id="9-收束从工具到团队">9. 收束：从工具到团队</h2>
<p>写完这 8 节，回头看共同的主线其实是：</p>
<blockquote>
<p>Claude Code 最值钱的不是它能写代码，是它能被你<strong>配置成你工作方式的一部分</strong>。</p>
</blockquote>
<p>Hook 是反射弧，CLAUDE.md 是长期记忆，MCP 是感官扩展，subagent 是分身，worktree 是物理隔离，slash 命令是肌肉记忆，权限模型是边界，上下文管理是节制 —— 这些组合起来，你管理的就不再是&quot;一个 AI 编辑器&quot;，是一个有反射、有记忆、能并行、有边界、会休息的小团队。</p>
<p>具体到日常，最容易落地的三条（如果只能选三条）：</p>
<ol>
<li><strong>每个项目至少有一份 200 行以内的 CLAUDE.md</strong>。这是最便宜的复合投资。</li>
<li><strong>任何做过两次的事，第三次包成 slash 命令或 hook</strong>。</li>
<li><strong>超过 30 分钟的任务先 plan mode，复杂改动直接开 worktree</strong>。</li>
</ol>
<p>剩下的 MCP、subagent、自定义 server 这些是&quot;再进一步&quot; —— 有需要再加，没需要不要为了用而用。工具的价值在于<strong>它在你不在的时候替你工作</strong>，不在于&quot;我用了多少种&quot;。</p>
<hr>
<p><strong>参考资源：</strong></p>
<ul>
<li><a href="https://docs.claude.com/en/docs/claude-code" target="_blank" rel="noopener">Claude Code 官方文档<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li><a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener">MCP 协议主页<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>我之前写的 <a href="/claude-code-mac-bark-notify/">给 Claude Code 接上 Mac 通知 + Bark 推送</a>
</li>
<li>我之前写的 <a href="/cs146s-modern-software-dev-notes/">CS146S 课程笔记</a>
（其中 Week 3 / Week 4 跟本文重叠最多）</li>
</ul>
]]></content:encoded></item><item><title>读完斯坦福 CS146S：从 Prompt 到 Agent Manager 的 8 周路线图</title><link>https://me.125520.xyz/cs146s-modern-software-dev-notes/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://me.125520.xyz/cs146s-modern-software-dev-notes/</guid><description>CS146S《The Modern Software Developer》8 周课程的整理笔记。把每周的内容归到三条主线：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>
是斯坦福 2025 秋季的新课，讲师 Mihail Eric，每周一位重磅客座（Boris Cherny、Zach Lloyd、Isaac Evans、Tomas Reimers……）。我把官方<a href="https://github.com/mihail911/modern-software-dev-assignments" target="_blank" rel="noopener">作业仓库<span class="external-mark" aria-hidden="true">↗</span></a>
里 8 周的内容捋了一遍，沿途做了实操。这篇是把 8 周的笔记整理。</p>
</blockquote>
<h2 id="1-课程的真正主题开发者身份的位移">1. 课程的真正主题：开发者身份的位移</h2>
<p>课程主页那句标语挺直白：</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>如果只看周次表，会以为这是一门 &ldquo;AI 工具使用大全&rdquo;。但读完 8 周的材料和作业，更准确的描述是：</p>
<p><strong>它在系统地训练你成为 Agent Manager。</strong></p>
<p>不是 &ldquo;学会用 ChatGPT 写代码&rdquo;，是从写代码的人变成指导 Agent 写代码、并且对结果负责的人。Boris Cherny（Claude Code 创造者）那句话被反复引用：</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>8 周内容可以归到三条主线：</p>
<ol>
<li><strong>Prompt → Context</strong>（Week 1 → 3）：从写一句好提示，到搭一整套信息环境</li>
<li><strong>Coder → Agent Manager</strong>（Week 2 → 5）：从亲手写代码，到指导/编排多个 Agent</li>
<li><strong>Speed → Defensible</strong>（Week 6 → 8）：从快速产出，到可审计、可防御、可上线</li>
</ol>
<p>下面按这三条主线展开。</p>
<h2 id="2-主线一prompt--context">2. 主线一：Prompt → Context</h2>
<h3 id="week-16-种基础提示技巧">Week 1：6 种基础提示技巧</h3>
<p>第一周用 Ollama 跑本地模型，挨个练 6 种技巧：K-shot、Chain-of-Thought、Tool Calling、Self-consistency、RAG、Reflexion。每个技巧一份 Python 文件，调到 test 通过为止。</p>
<p>练完最大的体感不是 &ldquo;我学了 6 种技巧&rdquo;，而是：<strong>这些技巧不是平行选项，是一条进化链</strong>。</p>
<ul>
<li>K-shot 给的是模式</li>
<li>CoT 给的是推理空间</li>
<li>Tool Calling 让模型不再&quot;猜&quot;，而是&quot;查&quot;</li>
<li>Self-consistency 用多次采样换稳定性</li>
<li>RAG 把外部知识接进上下文</li>
<li>Reflexion 让模型审视自己的输出再迭代</li>
</ul>
<p>到 Reflexion 这一步，模型已经从 &ldquo;回答机器&rdquo; 变成了 &ldquo;能自我修正的小循环&rdquo;。这就是 Agent 的雏形。</p>
<h3 id="week-2第一次让-ai-跑通需求闭环">Week 2：第一次让 AI 跑通需求闭环</h3>
<p>作业是一个最小的 FastAPI + SQLite 的 Action Item Extractor：自然语言笔记 → 结构化 todo 列表。要求用 Cursor 的 Agentic Mode 完成 5 个 TODO：用 LLM 替换规则提取、加单元测试、重构、加新端点+前端按钮、生成 README。</p>
<p>我在自己的实践笔记里总结了一个 &ldquo;本地需求执行 Agent&rdquo; 的最小能力模型：</p>
<pre tabindex="0"><code>需求理解 → 代码上下文获取 → 方案规划 → 代码生成 → 验证执行 → 结果反馈
    ↑                                                      |
    └──────────── 失败时回溯修正 ←───────────────────────┘
</code></pre><p>每一步都对应 Week 1 学过的某个技巧。Agent 不是新发明，是把那些原子能力按这个 loop 串起来。</p>
<p>跑完一遍最大的感受：<strong>当前的 AI Agent 真正擅长的是 &ldquo;边界清晰、可验证&rdquo; 的任务</strong>。脚手架代码、单函数实现、happy path 测试、文档生成 —— 这些是优势。判断&quot;什么不该改&quot;、跨模块副作用推理、涉及取舍的深层重构 —— 这些还得人来。</p>
<h3 id="week-3从-prompt-engineering-到-context-engineering">Week 3：从 Prompt Engineering 到 Context Engineering</h3>
<p>第三周是整门课信息密度最高的一周（另一个高密度周是 Week 6）。客座 Boris Cherny。核心命题是 Andrej Karpathy 的那句定义：</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>维度</th>
          <th>Prompt Engineering</th>
          <th>Context Engineering</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>焦点</td>
          <td>在某个时刻对模型说什么</td>
          <td>模型在你说话时知道什么</td>
      </tr>
      <tr>
          <td>范围</td>
          <td>一条精心编写的指令</td>
          <td>整个信息生态：记忆、工具、检索</td>
      </tr>
      <tr>
          <td>性质</td>
          <td>静态模板</td>
          <td>动态基础设施</td>
      </tr>
      <tr>
          <td>类比</td>
          <td>给即兴演员递剧本</td>
          <td>搭整个舞台、布道具</td>
      </tr>
  </tbody>
</table>
<p>里面最让我重新审视自己工作方式的两条：</p>
<p><strong>&ldquo;Context is King&rdquo; — 甚至是 intelligence 的前提。</strong> 课程里那句很狠：&ldquo;Claude 已经足够聪明 — 智能不是瓶颈，上下文才是。当 AI Agent 失败时，本能反应是 blame the model，但几乎总是错的。表现不好的是围绕它构建的信息环境。&rdquo;</p>
<p><strong>Context Rot（上下文腐烂）。</strong> 模型性能会随输入长度显著下降，即使是简单任务。所以 <code>/clear</code>、<code>/compact</code>、分层记忆、CLAUDE.md 精简，这些不是洁癖，是性能策略。</p>
<p>Week 3 的作业是写一个自定义 MCP Server。我做的是 GitHub Issue 助手，3 个 Tools（<code>search_issues</code> / <code>create_issue</code> / <code>get_issue_detail</code>）+ 1 个 Resource（仓库元数据）+ 1 个 Prompt（bug report 模板），STDIO 接 Claude Desktop。</p>
<p>亲手实现完才真的理解：<strong>MCP 的三大原语（Tools / Resources / Prompts）不是抽象概念，对应的是 Agent 三种不同的&quot;知道方式&quot;</strong> —— 能做什么、能查什么、该怎么说。</p>
<p>Boris Cherny 还分享了一套日常工作流：每天发 20-30 个 PR，靠的是同时跑 5 个 Claude Code 实例 + 5-10 个 claude.ai/code，每个实例都从 Plan Mode 起步，CLAUDE.md 当团队共享记忆。最被反复强调的一句：</p>
<blockquote>
<p>&ldquo;给 Claude 一种验证自己工作的方式 —— 浏览器测试、测试套件、模拟器。验证能让最终结果质量提升 2-3 倍。其他一切（subagent、slash command、MCP 集成）都建立在这个基础之上。&rdquo;</p>
</blockquote>
<h2 id="3-主线二coder--agent-manager">3. 主线二：Coder → Agent Manager</h2>
<h3 id="week-4作为-agent-manager-的-claude-code">Week 4：作为 Agent Manager 的 Claude Code</h3>
<p>Week 4 的核心句：</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>这周整理了 5 种 agentic 工作流模式：</p>
<ul>
<li><strong>Sequential Flow</strong>：默认模式，70% 的日常工作</li>
<li><strong>Operator</strong>：层级式，中央编排者 + 专门 sub-agent</li>
<li><strong>Split-and-Merge</strong>：fan-out/fan-in，最多 ~10 路并行</li>
<li><strong>Agent Teams</strong>：协作式，可互相通信，跨独立会话协调</li>
<li><strong>Headless</strong>：CI/CD、Cron、Webhook 触发，无人值守</li>
</ul>
<p>以及 12 个 &ldquo;Agent Harness 模式&rdquo;（持久指令、目录级指令、分层记忆、记忆卫生、上下文压缩、并行分支、检查点审查、选择性工具加载、定制工具、权限粒度、生命周期钩子、复合改进）。</p>
<p>最戳我的一句：</p>
<blockquote>
<p>&ldquo;问题不在于你的模型，而在于你的 harness（控制框架）。区分酷炫 demo 和生产级 AI 系统的不是智能，而是控制。&rdquo;</p>
</blockquote>
<p>作业是用 Slash Commands、CLAUDE.md、SubAgents 给一个 FastAPI starter app 加至少 2 个自动化。这周开始我的肌肉记忆变了：遇到任何重复的工作流，先想 &ldquo;这能不能塞进 <code>.claude/commands/</code>&quot;；遇到 Claude 反复犯的错，先想 &ldquo;这条该不该写进 CLAUDE.md&rdquo;。</p>
<p>复合工程（Compounding Engineering）这个词我喜欢：每次 AI 犯的错被记下来，都是永久的红利。</p>
<h3 id="week-5终端不再是高级用户的玩具">Week 5：终端不再是高级用户的玩具</h3>
<p>Week 5 客座是 Warp CEO Zach Lloyd。Warp 的 2.0 把自己从 &ldquo;更好的终端&rdquo; 重新定义为 ADE（Agentic Development Environment）。</p>
<p>这周的对照表对我比较有用：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>强项</th>
          <th>弱项</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Warp</strong></td>
          <td>多 tab 并行 Agent、Diff 面板可视化审查、多模型支持</td>
          <td>深度代码理解略弱</td>
      </tr>
      <tr>
          <td><strong>Claude Code</strong></td>
          <td>深度上下文、CLAUDE.md 精控、自主推理</td>
          <td>仅 Anthropic 模型</td>
      </tr>
  </tbody>
</table>
<p>它们其实是叠加关系：<strong>Claude Code 可以跑在 Warp 里</strong>。多 Agent 并行的关键工具是 <code>git worktree</code> —— 每个 Agent 在独立的工作目录里干活，互不污染，结束再合分支。这个技巧后面我的实际工作里也用上了。</p>
<p>但 Warp 团队也很坦诚：<strong>并行不是免费的</strong>。每周省 6-7 小时是真的，但前提是你的认知带宽能管得住 5 个 tab —— 这才是真正的 bottleneck。</p>
<h2 id="4-主线三speed--defensible">4. 主线三：Speed → Defensible</h2>
<h3 id="week-6semgrep-与-ai-代码安全">Week 6：Semgrep 与 AI 代码安全</h3>
<p>第六周邀请了 Semgrep CEO Isaac Evans。这周的开场数据让我停下来想了一下：</p>
<blockquote>
<p>Claude Code 在安全扫描实验中发现了 46 个真实漏洞（<strong>真阳性率 14%</strong>，<strong>误报率 86%</strong>）。</p>
</blockquote>
<p>这不是说 Claude 不行，是说 <strong>AI 的安全直觉不可靠 —— 7 条告警里只有 1 条真正是漏洞</strong>。Semgrep 的价值在于把这个比例反过来：减少 80% 误报，同时提升 250% 真阳性。</p>
<p>Semgrep 三件套：</p>
<ul>
<li><strong>Code (SAST)</strong>：第一方代码漏洞，跨文件分析</li>
<li><strong>Supply Chain (SCA)</strong>：依赖中的可达漏洞（reachability analysis 把不可达的 CVE 过滤掉，高/严重级别误报降 98%）</li>
<li><strong>Secrets</strong>：硬编码密钥，含语义分析 + 熵分析 + 验证</li>
</ul>
<p>作业是用 <code>semgrep ci --subdir week6</code> 扫描一个故意藏漏洞的 FastAPI app，挑 3 个修。修法朴素但本质：参数化 SQL、限制 CORS、密钥转环境变量、依赖升级、加密算法换强的、DOM 写入消毒。</p>
<p>这周收的最朴素的一句话：<strong>&ldquo;Secure Vibe Coding&rdquo; 的目标是让快和安全统一，不是对立</strong> —— 靠的是把扫描自动化进 CI，而不是事后人工补救。</p>
<h3 id="week-7graphite-diamond-与-ai-代码审查">Week 7：Graphite Diamond 与 AI 代码审查</h3>
<p>第七周客座是 Graphite CPO Tomas Reimers。命题是：</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 审查的优势：风格一致性、常见 Bug 模式、安全漏洞、代码重复、100% PR 覆盖、秒级响应。
人类审查的优势：架构判断、业务逻辑正确性、tradeoff 评估、创造性建议、团队上下文、UX 设计。</p>
<p>最佳模式是 &ldquo;AI First, Human Second&rdquo;：AI 第一层过滤掉机械问题，人类第二层聚焦高价值判断。Graphite 团队的另一个产品创新是 <strong>Stacked PRs</strong> —— 把一个大 PR 拆成多个小的、顺序的 PR，因为 AI 审查在小 PR 上表现明显更好。</p>
<p>作业很扎实：4 个独立任务，每个都按&quot;创建分支 → 1-shot AI prompt 实现 → 手动逐行审 → 开 PR → 用 Graphite Diamond 跑 AI 审查 → 对比&quot;的流程做一遍，最后写一份反思 —— 你的审查 vs Graphite 的审查，谁更好、什么时候更好。</p>
<p>这种 &ldquo;对比反思&rdquo; 的训练我觉得是这周最珍贵的部分。它不是教你信任 AI，是教你<strong>知道 AI 什么时候比你强、什么时候不如你</strong>。这种元认知，在 AI 时代比任何具体工具都重要。</p>
<h3 id="week-8boltnew-与多栈快速构建">Week 8：Bolt.new 与多栈快速构建</h3>
<p>第八周转向 &ldquo;原型到生产&rdquo; 主题。客座是 Vercel AI Research 负责人。核心句：</p>
<blockquote>
<p>&ldquo;Rapid prototyping is just the starting point. Between demo and production lies a chasm.&rdquo;</p>
</blockquote>
<p>Bolt.new 用 StackBlitz 的 WebContainer 技术做到完全在浏览器里跑 Node.js，从提示词直出全栈应用。但它的能力曲线很陡：</p>
<table>
  <thead>
      <tr>
          <th>复杂度</th>
          <th>表现</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>3-5 组件</td>
          <td>几分钟可用，几乎不用调</td>
      </tr>
      <tr>
          <td>10-15 组件</td>
          <td>需要迭代式提示，但总体可用</td>
      </tr>
      <tr>
          <td>15-20+ 组件</td>
          <td>上下文退化，token 飙升</td>
      </tr>
      <tr>
          <td>复杂状态管理 + 认证 + 第三方集成</td>
          <td>成功率降到 <strong>31%</strong></td>
      </tr>
  </tbody>
</table>
<p>作业要求用 3 种不同技术栈构建同一个应用，至少一个用 Bolt、至少一个用非 JS 语言。这设计很狡猾 —— 它在偷偷训练你两个能力：</p>
<ol>
<li><strong>抽象思维</strong>：把业务从框架里抽出来，理解什么是本质、什么是实现细节</li>
<li><strong>AI 工具评估</strong>：同一个需求，让 Bolt 在 Next.js / Django / Rails 上分别生成，质量差异巨大 —— 你必须培养出 &ldquo;什么是生产就绪代码&rdquo; 的直觉</li>
</ol>
<h2 id="5-三条收回来能用的实操结论">5. 三条收回来能用的实操结论</h2>
<p>读完 8 周的内容，做完几个作业，我自己往日常工作里搬的就是这三条：</p>
<h3 id="51-plan-mode-是免费的红利">5.1 Plan Mode 是免费的红利</h3>
<p>Boris 那句 &ldquo;在审查和批准书面计划之前，永远不要让 Claude 写代码&rdquo; —— 这是 8 周里被引用最多的一条。我现在做任何超过 30 分钟的任务都先 <code>shift+tab</code> 进 Plan Mode，跟 Claude 来回讨论清楚再切到 auto-accept。返工率明显下降。</p>
<h3 id="52-claudemd-是复合改进的载体">5.2 CLAUDE.md 是复合改进的载体</h3>
<p>每次 Claude 犯错或者我发现一个不显然的项目约定，都往 CLAUDE.md 里加一条。这个习惯非常便宜、非常划算：每条加了之后就再也不用解释第二次。但要保持精简 —— 它每次会话都加载，吃上下文。</p>
<h3 id="53-给-agent-一种验证自己的方式">5.3 给 Agent 一种验证自己的方式</h3>
<p>这是 Boris 的&quot;最重要的洞见&rdquo;。具体到我自己的实践：</p>
<ul>
<li>写后端：让 Claude 跑测试，失败再改，不要&quot;看起来对就提交&quot;</li>
<li>写前端：起 dev server，让 Claude 用 Playwright 实际点一遍 golden path</li>
<li>写 API 集成：先有一个 curl 或 httpie 的最小复现脚本</li>
</ul>
<p>这个习惯本身就是 Reflexion 模式（Week 1 学过的）的工程化版本 —— 把&quot;自我审视&quot;内置到工作流里。</p>
<h2 id="6-收尾8-周完整弧线">6. 收尾：8 周完整弧线</h2>
<pre tabindex="0"><code>Week 1:  如何与 AI 对话           → Prompt Engineering
Week 2:  AI 如何使用工具          → Coding Agents &amp; MCP
Week 3:  如何为 AI 构建信息环境   → Context Engineering
Week 4:  如何管理 AI              → Agent Manager
Week 5:  AI 增强的终端            → AI Terminal
Week 6:  AI 代码安全              → Semgrep
Week 7:  AI 代码审查              → Graphite Diamond
Week 8:  AI 构建应用              → Bolt.new 多栈
(Week 9-10:  AI 运维 + 未来       → 课程后半段)
</code></pre><p>每一周都是下一周的基础。最后留下的不是一堆工具的使用说明，是一种新的工作姿态：</p>
<p><strong>你不再是写代码的人，你是设计信息环境、指导 Agent、并且对最终交付物负责的人。</strong></p>
<p>写代码的速度提升了 10 倍，但写错代码的速度也提升了 10 倍。课程划的那条 baseline —— <strong>可测试、可审计、可防御</strong> —— 不是束缚，是这个新姿态能站住脚的前提。</p>
<hr>
<p><strong>参考资源：</strong></p>
<ul>
<li>课程主页：<a href="https://themodernsoftware.dev/" target="_blank" rel="noopener">themodernsoftware.dev<span class="external-mark" aria-hidden="true">↗</span></a>
</li>
<li>作业仓库：<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>中文版社区整理：<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 最佳实践：<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（X 原帖）</li>
<li>Boris Cherny 工作流：<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>给 Claude Code 接上 Mac 通知 + Bark 推送</title><link>https://me.125520.xyz/claude-code-mac-bark-notify/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://me.125520.xyz/claude-code-mac-bark-notify/</guid><description>把 Windows 版 PowerShell gist 翻译成 macOS 版本，顺手加一条 Bark 推送通道，跑长任务时手机也响。</description><content:encoded><![CDATA[<blockquote>
<p>起因：在 GitHub 上看到一份 <a href="https://gist.github.com/RisenMyth/6297cab96eb9dec912cc6c126cbed965" target="_blank" rel="noopener">Windows 下给 Claude Code 加系统通知的 gist<span class="external-mark" aria-hidden="true">↗</span></a>
，PowerShell 写的，落地 Mac 用不了。索性重写一份 macOS 版，再顺手加一条 <a href="https://day.app/" target="_blank" rel="noopener">Bark<span class="external-mark" aria-hidden="true">↗</span></a>
 推送 —— 长任务跑起来人离开屏幕，手机也能响。</p>
</blockquote>
<h2 id="1-思路">1. 思路</h2>
<p>Claude Code 的 hook 系统暴露一组生命周期事件，对&quot;被通知&quot;的需求，只需要两个：</p>
<ul>
<li><strong><code>Stop</code></strong> —— 模型当前回合结束。任务跑完该响一声。</li>
<li><strong><code>Notification</code></strong> —— 模型需要确认/输入（permission prompt、问题等）。这种更紧急。</li>
</ul>
<p>Hook 命令的 stdin 会拿到一份 JSON：<code>cwd</code>（项目路径）、<code>transcript_path</code>（JSONL 全量对话）、<code>Notification</code> 事件还会带 <code>message</code>。</p>
<p>设计取舍：</p>
<ul>
<li><strong>单文件 Python</strong> —— macOS 自带 <code>python3</code>，不再依赖 jq、curl 的 JSON 转义脏活。</li>
<li><strong>本地通知优先 <code>terminal-notifier</code></strong> —— 装了就用（可点击、有 group 去重），没装回落 <code>osascript</code>，零依赖。</li>
<li><strong>Bark 是可选项</strong> —— 只有当 <code>BARK_KEY</code> 环境变量存在才推。失败静默，不影响 hook 退出码。</li>
<li><strong>不 chmod</strong> —— 原因见末尾&quot;坑&quot;那节。脚本不可执行，靠 <code>python3 path/to/notify.py</code> 调用。</li>
</ul>
<h2 id="2-脚本claudehooksnotifypy">2. 脚本：<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>几个细节值得一提：</p>
<ul>
<li><strong>抓 transcript 倒数往前找</strong> —— 只读最后 30 行 JSONL，倒序遍历，第一条 <code>role == &quot;assistant&quot;</code> 且非空文本就用。<code>content</code> 字段在不同 SDK 版本下可能是 string 也可能是 array，两种都处理。</li>
<li><strong>truncate 用 <code>…</code> 单字符</strong> —— 而不是 <code>...</code> 三个 ASCII 句号，省 2 字节给正文。</li>
<li><strong>Bark <code>level</code> 分场景</strong> —— <code>Stop</code> 默认 <code>passive</code>（不打扰，只入推送列表），<code>Notification</code> 默认 <code>timeSensitive</code>（穿透专注模式）。等长任务跑完手机不必尖叫，但要确认时必须能听见。</li>
<li><strong>失败静默</strong> —— Bark 推不上、osascript 报错、transcript 读不到，全部吃异常返回。Hook 不应该把模型卡死。</li>
</ul>
<h2 id="3-接入-claudesettingsjson">3. 接入 <code>~/.claude/settings.json</code></h2>
<p>把下面这块合进 <code>settings.json</code>，<strong>保留你原有的 <code>env / permissions / enabledPlugins</code> 等字段</strong>：</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> 是匹配所有 —— Stop / Notification 这两个事件本来也没有 sub-matcher 概念。<code>timeout: 15</code> 秒兜底，超时不影响主流程。</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">路径必须绝对</p>
    hook command 的 cwd 不可预期（取决于当时 Claude 在哪个项目里）。<code>~/.claude/hooks/notify.py</code> 这种波浪号不会展开，必须写成 <code>/Users/&lt;你&gt;/.claude/hooks/notify.py</code>。
  </div>
</div>

<h2 id="4-启用-bark">4. 启用 Bark</h2>
<p>Bark 是 iOS 上一个挺老的推送工具，免费版 key 在 <a href="https://day.app/" target="_blank" rel="noopener">day.app<span class="external-mark" aria-hidden="true">↗</span></a>
 拿。装好 app，主页能看到一段类似 <code>https://api.day.app/abc123xyz/</code> 的链接 —— 中间那段就是设备 key。</p>
<p>把它加到 <code>settings.json</code> 的 <code>env</code> 块（hook 进程会继承）：</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>可选环境变量速查：</p>
<table>
  <thead>
      <tr>
          <th>变量</th>
          <th>默认值</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>BARK_SERVER</code></td>
          <td><code>https://api.day.app</code></td>
          <td>自建服务器改这个</td>
      </tr>
      <tr>
          <td><code>BARK_GROUP</code></td>
          <td><code>ClaudeCode</code></td>
          <td>通知分组（手机折叠用）</td>
      </tr>
      <tr>
          <td><code>BARK_SOUND</code></td>
          <td><code>minuet</code></td>
          <td>铃声名</td>
      </tr>
      <tr>
          <td><code>BARK_STOP_LEVEL</code></td>
          <td><code>passive</code></td>
          <td>任务完成的等级</td>
      </tr>
      <tr>
          <td><code>BARK_NOTIFY_LEVEL</code></td>
          <td><code>timeSensitive</code></td>
          <td>需要确认的等级</td>
      </tr>
      <tr>
          <td><code>CLAUDE_NOTIFY_OFF</code></td>
          <td>(未设)</td>
          <td>设为 <code>1</code> 临时全关</td>
      </tr>
  </tbody>
</table>
<h2 id="5-验证">5. 验证</h2>
<p>设好 <code>BARK_KEY</code> 后，手动跑一发 Notification 模拟：</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>你的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>正常情况下：</p>
<ol>
<li>桌面右上角弹一条 macOS 通知</li>
<li>手机 Bark 通知到达，分组是 <code>ClaudeCode</code>，级别是 <code>timeSensitive</code></li>
</ol>
<p>如果只想看本地通知不打扰手机，把 <code>BARK_KEY</code> 临时去掉就行。</p>
<h2 id="6-几个坑">6. 几个坑</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 通知权限</p>
    <code>osascript display notification</code> 弹的通知归属到<strong>当前终端 app</strong>（Terminal / iTerm / Ghostty / Warp）。如果完全没声没影：系统设置 → 通知 → 找到对应终端 → 允许通知。
  </div>
</div>

<p><strong>hook 不生效？</strong> Claude Code 的 settings watcher 只盯着会话启动时已经存在的目录。如果 <code>.claude/</code> 是会话开始后才创建的，settings 修改可能不会被 reload。打开一次 <code>/hooks</code> 菜单（会强制 reload），或者重启 Claude Code session。</p>
<p><strong><code>chmod</code> 被 deny 怎么办？</strong> 我的 settings 里 <code>permissions.deny</code> 包含 <code>Bash(chmod:*)</code>，所以脚本不能 <code>chmod +x</code>。解法是 hook command 直接用 <code>python3 path/to/notify.py</code> 调用，不依赖文件可执行位 —— 反而更显式，少一处隐式状态。</p>
<p><strong>通知正文太长？</strong> 脚本里 <code>MAX_BODY_LEN = 200</code>，截到 199 字符 + <code>…</code>。macOS 通知中心本身也会截断，但提前截能避免在 Bark 上看到一串 JSON 转义崩溃的字符。</p>
<p><strong>想再省一点？</strong> Stop 事件每回合都触发，会很吵。两个解法：</p>
<ul>
<li>在 Bark 那条线把 <code>BARK_STOP_LEVEL</code> 已经设成 <code>passive</code> —— 只入推送列表，不响。</li>
<li>想本地也不响：在 <code>send_mac_notification</code> 里检查 <code>event == &quot;Stop&quot;</code> 时不发 osascript 就行（5 行改动）。</li>
</ul>
<h2 id="7-可拓展方向">7. 可拓展方向</h2>
<p>留给以后自己折腾：</p>
<ul>
<li><strong>Telegram / Lark / Slack</strong> —— 把 <code>send_bark_push</code> 改名 <code>send_remote_push</code>，按 <code>REMOTE_KIND</code> 环境变量分发。Bark 只是其中一种。</li>
<li><strong>细化 Notification 文案</strong> —— 当前直接拿模型给的 message。可以判断 <code>tool_name</code>（如果将来 hook payload 加进来）做更精准的标题。</li>
<li><strong>聚合连发</strong> —— 短时间内多次 Stop 合并成一条（debounce 5s）。当前每次都响。</li>
<li><strong>统计</strong> —— 把每次 hook 触发记一行到 <code>~/.claude/hooks/notify.log</code>，看一周下来跑了多少回合、几次需要确认。这些数据其实挺有意思。</li>
</ul>
<hr>
<p>整套配置改一次就一直在了。每次模型响完 / 卡住等输入，桌面右上角&quot;叮&quot;一声，手机口袋里也跟着震一下 —— 长任务跑起来终于可以放心去倒杯水。</p>
]]></content:encoded></item></channel></rss>