<?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>Fri, 08 May 2026 12:39:33 +0000</lastBuildDate><atom:link href="https://me.125520.xyz/tags/%E5%B7%A5%E5%85%B7/index.xml" rel="self" type="application/rss+xml"/><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>