<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>sorrycc's blog</title>
    <link>https://blog.sorrycc.com</link>
    <description>sorrycc's blog</description>
    <language>zh-CN</language>
    <atom:link href="https://blog.sorrycc.com/api/feed" rel="self" type="application/rss+xml" />
    <item>
      <title>645 - 《Release codexthropic：让 Claude Code 跑 GPT-5.5》</title>
      <link>https://blog.sorrycc.com/release-codexthropic</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/release-codexthropic</guid>
      <pubDate>Wed, 29 Apr 2026 11:27:29 GMT</pubDate>
      <description><![CDATA[<p>写了个小工具 codexthropic，一个本地 Node 代理，把 Anthropic Messages API 翻译成 OpenAI Codex Responses API。实际效果：你可以用 Claude Code 直接连 OpenAI 的 GPT-5.5 后端写代码。</p>
<p><img src="https://pic.sorrycc.com/proxy/1777448017443-546257714.png" alt=""></p>
<p>做这个的动机很直接——Claude Code 是目前最好的 AI coding agent 前端，但它只说 Anthropic 协议。Codex 后端（GPT-5.5）的代码能力也很强，但只有 OpenAI 自家的 Codex CLI 能用。codexthropic 在中间做协议翻译，让两边的长处接上。</p>
<p>技术上要处理的东西比想象中多：</p>
<p>1）SSE 流式事件双向映射。Anthropic 的 message_start / content_block_start / content_block_delta / content_block_stop / message_delta / message_stop 这套事件模型，和 Codex 的 response.created / response.output_text.delta / response.output_item.added / response.completed 完全是两套语言，逐事件翻译，还要维护 block index 状态机。</p>
<p>2）Tool Use 完整映射。Anthropic 的 tools 定义转成 Codex 的 function tools，tool_use block 转成 function_call item，tool_result 转成 function_call_output。tool_choice 也要映射：auto→auto，any→required，none→none，指定工具名→{type:function, name}。</p>
<p>3）多轮推理连续性——这个最巧妙。Codex 的 reasoning.encrypted_content 是加密的思维链状态，需要在多轮工具调用间传递才能保持推理连贯。方案是把它塞进 Anthropic thinking block 的 signature 字段，Claude Code 会原样回传 thinking blocks，下一轮请求时再从 signature 还原成 Codex 的 reasoning input item。链式思维跨轮次不断。</p>
<p>4）OAuth 认证。读 ~/.codex/auth.json，JWT 过期前 60s 自动刷新，并发请求用 single-flight 合并避免重复刷新。刷新失败有 30s 冷却防止锁死风暴。收到 401 会 force-refresh 重试一次。如果 Codex CLI 在后台轮换了 refresh_token，代理会检测磁盘文件变化自动重载，不用重启。原子写入用 tmp+fsync+rename 保证不写坏 auth 文件。</p>
<p>5）reasoning effort 翻译。Anthropic 的 thinking.budget_tokens 按阈值映射：&lt;4000→low，&lt;16000→medium，&lt;32000→high，≥32000→xhigh。adaptive thinking 和 output_config.effort:max 也走 xhigh。</p>
<p>模型映射：所有 Claude 模型名（opus/sonnet/haiku）统一映射到 gpt-5.5，因为 ChatGPT 账号的 Codex 后端目前只接受这个模型。gpt-* 和 o* 开头的模型名直接透传。</p>
<p>用法极简：</p>
<pre><code class="language-bash">npx codexthropic@latest
</code></pre>
<pre><code class="language-bash">ANTHROPIC_BASE_URL=http://127.0.0.1:8765 ANTHROPIC_API_KEY=any claude
</code></pre>
<p>零运行时依赖，纯 node:http 实现，88 个离线测试覆盖各种边界情况。需要 Node 20+ 和 codex login 完成过的本地认证。</p>
]]></description>
    </item>
    <item>
      <title>640 - 《Multica》</title>
      <link>https://blog.sorrycc.com/multica</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/multica</guid>
      <pubDate>Mon, 27 Apr 2026 09:28:09 GMT</pubDate>
      <description><![CDATA[<p>内网同学强烈推荐了 <a href="https://github.com/multica-ai/multica">Multica</a>，一个开源的 Managed Agents Platform，2 周破 10K star，目前 15.4K。试了下，部署到了家里的 Mac Mini M4 上，记录一些感受。</p>
<p>整体评价：目前对我来说有用但不多，但想象空间很大，可能还没发挥出它的能力。</p>
<h3>1. <strong>它是什么</strong></h3>
<p>一句话：人和 AI Agent 共享同一块看板的协作平台。推上 <a href="https://x.com/bnafOg/status/2043642366880370719">@bnafOg</a> 说得好，the real unlock isn&#39;t the orchestration — it&#39;s the shared human/AI board。你创建 Issue，Assignee 设成 Agent，Agent 自动领活、clone 仓库、在隔离目录里干活、实时回传结果、完成后自动流转到 In Review（注意不是 Done，人必须终审，这是刻意的 human-in-the-loop 设计）。Claude Code、Codex、OpenCode、OpenClaw、Gemini、Hermes 都支持。</p>
<h3>2. <strong>部署体验</strong></h3>
<p>Go 后端 + Next.js 前端 + PostgreSQL，docker compose 一把梭。我用 OrbStack + Cloudflare Tunnel 暴露了两个子域名，前端一个后端一个。整体流畅。</p>
<p>两个坑：一是官方 compose 用预构建镜像，不支持自定义 WebSocket 地址，双域名部署需要改成从源码构建。二是 APP_ENV=production 会禁掉万能验证码 888888，如果没提前配好 Resend 邮件 API，你会把自己锁外面。</p>
<p>推上看到 <a href="https://x.com/victor_wu/status/2048455026256056390">@victor_wu</a> 的体验比我糟糕不少，Agent offline、一直 loading，可能跟他用的是 Cloud 版有关。自托管 + CLI 的体验要顺畅得多。有人在 Hetzner 上用 €4.49/月的 VPS 就跑起来了，替代了 80% 原来付给 Anthropic Managed Agents 的费用。</p>
<h3>3. <strong>核心概念</strong></h3>
<p>Workspace（团队空间）→ Project（项目）→ Repo（Git 仓库）→ Issue（任务）→ Agent（AI 角色）→ Runtime（某台机器上的某个 CLI）。</p>
<p>核心是 Issue 的流转。Agent 绑定 Runtime，Runtime 绑定机器。这意味着你可以精确控制哪个 Agent 在哪台机器上跑。</p>
<h3>4. <strong>多机器是刚需</strong></h3>
<p>我有三台 Mac，有些任务只能在公司电脑上执行（内网依赖、证书）。Multica 的 Runtime 机制天然解决这个问题：公司电脑跑 daemon，创建绑定公司 Runtime 的 Agent，任务只会被那台机器领走。这个设计很对。</p>
<h3>5. <strong>Skills 复用是亮点</strong></h3>
<p>这是所有评测里被提到最多的特性。Agent 解决了一个问题后，方案可以沉淀成 Skill，Workspace 内所有 Agent 共享复用。部署流程、代码审查规则、数据迁移步骤，一个 Agent 学会了，其他 Agent 直接调用。这是 Multica 区别于纯任务分发工具的关键。</p><p><a href="https://blog.sorrycc.com/multica">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>639 - 《从想法到上线，一到两天》</title>
      <link>https://blog.sorrycc.com/ai-era-product-development</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/ai-era-product-development</guid>
      <pubDate>Fri, 24 Apr 2026 10:23:56 GMT</pubDate>
      <description><![CDATA[<p><img src="https://pic.sorrycc.com/proxy/1777037576989-694135432.png" alt=""></p>
<p>PD 写需求 → 设计师画图 → 开发写代码 → 测试验收 → 上线。每个交接点都是延迟，每个角色都是瓶颈。市场已经在用脚投票了，设计师岗位自 2023 年起停滞，AI 让工程师跑太快，传统设计流程跟不上。现在的流程就是 <strong>Dev → 上线</strong>，端到端，中间不传球。</p>
<p>1. <strong>Feature Flag 是快的安全网</strong></p>
<p>上线不等于全量发布。所有功能通过 feature flag 控制，先开 1%，没问题再放开。出了问题关掉 flag，秒级回滚。你不是在「发布一个承诺」，而是在「让一小部分用户先试试看」。</p>
<p>2. <strong>品味决定做什么</strong></p>
<p>执行便宜了，判断力变贵了。什么需求都有人提，知道该做哪个、不做哪个、怎么做最好，这个判断力就是品味。品味是主观的，但产品需要有明确审美的人来做决策，而不是靠投票和委员会。共识出来的东西，通常平庸。</p>
<p>3. <strong>一到两天上线，打磨靠反馈</strong></p>
<p>大部分功能从想法到上线，一到两天够了。上线不是结束，是刚开始收集反馈。第一版可以粗糙，但必须能用。后续打磨可能花十倍时间，但改什么不改什么，让反馈说了算。没反馈的功能，说明没人在乎。</p>
<p>4. <strong>建立完整的 Loop</strong></p>
<p>主线闭环：<strong>需求 → 研发 → 上线 → 运营 → 反馈 → 需求</strong></p>
<p>暗线：<strong>上线 → 日志监控 → 错误追踪 → 需求</strong></p><p><a href="https://blog.sorrycc.com/ai-era-product-development">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>638 - 《编写 AI Agent 友好的 CLI》</title>
      <link>https://blog.sorrycc.com/agent-friendly-cli</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/agent-friendly-cli</guid>
      <pubDate>Wed, 22 Apr 2026 09:17:39 GMT</pubDate>
      <description><![CDATA[<blockquote>
<p>写 CLI 的时候粘贴给 Agent 当约束用。每条规则带 Bad/Good 对比，代码用 Node.js。</p>
</blockquote>
<h2>Rules</h2>
<h3>Rule 1: 所有命令必须支持 <code>--json</code> 结构化输出</h3>
<p>Agent 解析不了彩色表格、ASCII art、进度条这些东西，全是噪音。每个命令加上 <code>--json</code>，吐稳定的 JSON。stdout 不是 TTY 的时候默认走 JSON。</p>
<pre><code class="language-bash"># Bad - Agent 无法可靠解析
$ myctl list users
NAME        EMAIL              ROLE
─────────────────────────────────────
Alice       alice@co.com       admin
Bob         bob@co.com         member

# Good - 支持 --json 标志
$ myctl list users --json
[
  {&quot;name&quot;: &quot;Alice&quot;, &quot;email&quot;: &quot;alice@co.com&quot;, &quot;role&quot;: &quot;admin&quot;},
  {&quot;name&quot;: &quot;Bob&quot;, &quot;email&quot;: &quot;bob@co.com&quot;, &quot;role&quot;: &quot;member&quot;}
]
</code></pre>
<pre><code class="language-ts">// Node 实现
import { parseArgs } from &quot;node:util&quot;;

const { values } = parseArgs({
  options: { json: { type: &quot;boolean&quot;, default: false } },
});

const useJson = values.json || !process.stdout.isTTY;

function output(data: unknown) {
  if (useJson) {
    console.log(JSON.stringify(data));
  } else {
    // 人类友好的格式化输出
    console.table(data);
  }
}
</code></pre>
<h3>Rule 2: 禁止交互式提示，提供非交互标志</h3>
<p>Agent 不会帮你按「y」。CLI 卡在那等输入，整条链路就废了。stdin 不是 TTY 就别弹提示，要么跳过要么报错退出。</p>
<pre><code class="language-bash"># Bad - Agent 被阻塞在确认提示
$ myctl delete project foo
Are you sure? (y/n): _
# Agent 卡死在这里

# Good - 提供 --yes 跳过确认
$ myctl delete project foo --yes
{&quot;status&quot;: &quot;deleted&quot;, &quot;project&quot;: &quot;foo&quot;}
</code></pre>
<pre><code class="language-ts">// Node 实现
import { parseArgs } from &quot;node:util&quot;;

const { values } = parseArgs({
  options: { yes: { type: &quot;boolean&quot;, short: &quot;y&quot;, default: false } },
});

async function confirmOrExit(message: string) {
  // 非交互环境：没有 --yes 就直接报错退出
  if (!process.stdin.isTTY) {
    if (!values.yes) {
      console.error(&quot;Error: --yes required in non-interactive mode&quot;);
      process.exit(1);
    }
    return;
  }
  // 交互环境且有 --yes：跳过提示
  if (values.yes) return;
  // 交互环境：正常提示用户
  const answer = await ask(`${message} (y/n): `);
  if (answer !== &quot;y&quot;) process.exit(0);
}
</code></pre>
<h3>Rule 3: 支持 <code>--dry-run</code>，返回结构化 diff</h3>
<p>干危险活之前得先看看会炸什么。<code>--dry-run</code> 别输出人话，直接返回结构化的变更清单。</p>
<pre><code class="language-bash"># Bad - dry-run 输出人类散文
$ myctl deploy --dry-run
Would deploy web-api to production environment.

# Good - dry-run 返回结构化 diff
$ myctl deploy --dry-run --json
{
  &quot;actions&quot;: [
    {&quot;type&quot;: &quot;deploy&quot;, &quot;service&quot;: &quot;web-api&quot;, &quot;env&quot;: &quot;production&quot;, &quot;current_version&quot;: &quot;1.2.3&quot;, &quot;target_version&quot;: &quot;1.3.0&quot;},
    {&quot;type&quot;: &quot;scale&quot;, &quot;service&quot;: &quot;web-api&quot;, &quot;from&quot;: 2, &quot;to&quot;: 4}
  ],
  &quot;risk_tier&quot;: &quot;high&quot;,
  &quot;requires_approval&quot;: true
}
</code></pre>
<pre><code class="language-ts">// Node 实现
const { values } = parseArgs({
  options: {
    &quot;dry-run&quot;: { type: &quot;boolean&quot;, default: false },
    json: { type: &quot;boolean&quot;, default: false },
  },
});

async function deploy(config: DeployConfig) {
  const plan = buildDeployPlan(config);

  if (values[&quot;dry-run&quot;]) {
    // dry-run：只返回计划，不执行
    output({
      actions: plan.actions,
      risk_tier: plan.riskTier,
      requires_approval: plan.riskTier === &quot;high&quot;,
    });
    return;
  }

  await executePlan(plan);
}
</code></pre>
<h3>Rule 4: 控制输出大小，保护上下文窗口</h3>
<p>返回数据不限大小，Agent 的上下文窗口一下就炸了。加 <code>--fields</code> 只拿需要的字段，加 <code>--limit</code> 控制条数。</p>
<pre><code class="language-bash"># Bad - 返回所有字段，单条记录 200 行
$ myctl get user alice --json
{&quot;name&quot;: &quot;Alice&quot;, &quot;email&quot;: &quot;...&quot;, &quot;avatar_url&quot;: &quot;...&quot;, &quot;bio&quot;: &quot;...&quot;, &quot;settings&quot;: {...}, &quot;history&quot;: [...2000 items...]}

# Good - 支持 --fields 精确控制
$ myctl get user alice --json --fields=name,email,role
{&quot;name&quot;: &quot;Alice&quot;, &quot;email&quot;: &quot;alice@co.com&quot;, &quot;role&quot;: &quot;admin&quot;}

# Good - 支持 --limit 控制分页
$ myctl list events --json --limit=10
</code></pre>
<pre><code class="language-ts">// Node 实现
const { values } = parseArgs({
  options: {
    fields: { type: &quot;string&quot; },
    limit: { type: &quot;string&quot;, default: &quot;100&quot; },
  },
});

function pickFields(obj: Record&lt;string, unknown&gt;, fields?: string) {
  if (!fields) return obj;
  const keys = fields.split(&quot;,&quot;);
  return Object.fromEntries(keys.filter((k) =&gt; k in obj).map((k) =&gt; [k, obj[k]]));
}

function outputList(items: Record&lt;string, unknown&gt;[]) {
  const limited = items.slice(0, parseInt(values.limit!));
  const picked = limited.map((item) =&gt; pickFields(item, values.fields));
  output(picked);
}
</code></pre>
<h3>Rule 5: 错误必须结构化，包含错误码和恢复建议</h3>
<p>一句 <code>&quot;Error occurred&quot;</code> 等于没说。错误得有可解析的类型，把失败的输入原样吐回来，再给一条恢复建议。超时这种瞬态错误可以重试，权限不够这种永久错误就别重试了，用不同退出码区分。</p>
<pre><code class="language-bash"># Bad - 人类可读但机器无法解析
$ myctl push image foo
Error: Something went wrong while pushing the image.

# Good - 结构化错误，含类型、输入回显和建议
$ myctl push image foo --json
{
  &quot;error&quot;: {
    &quot;code&quot;: &quot;image_not_found&quot;,
    &quot;message&quot;: &quot;Image &#39;foo&#39; does not exist in local registry&quot;,
    &quot;input&quot;: {&quot;image&quot;: &quot;foo&quot;},
    &quot;retryable&quot;: false,
    &quot;suggestion&quot;: &quot;Run &#39;myctl list images --json&#39; to see available images&quot;
  }
}
</code></pre>
<pre><code class="language-ts">// Node 实现

// 退出码约定：
// 1 = 永久错误（输入错误、权限不足，别重试）
// 2 = 瞬态错误（超时、限流，值得重试）

interface CLIError {
  code: string;
  message: string;
  input?: Record&lt;string, unknown&gt;;
  retryable: boolean;
  suggestion?: string;
}

function exitWithError(err: CLIError): never {
  if (useJson) {
    console.error(JSON.stringify({ error: err }));
  } else {
    console.error(`Error [${err.code}]: ${err.message}`);
    if (err.suggestion) console.error(`Hint: ${err.suggestion}`);
  }
  process.exit(err.retryable ? 2 : 1);
}

// 使用
exitWithError({
  code: &quot;image_not_found&quot;,
  message: &quot;Image &#39;foo&#39; does not exist in local registry&quot;,
  input: { image: &quot;foo&quot; },
  retryable: false,
  suggestion: &quot;Run &#39;myctl list images --json&#39; to see available images&quot;,
});
</code></pre>
<h3>Rule 6: <code>--help</code> 输出要简洁且格式稳定</h3>
<p>Agent 就靠 <code>--help</code> 来摸清你的命令能干嘛。写一堆废话浪费 token，格式还老变就更完蛋了。保持简洁，格式别动。愿意多做一步的话，加个 <code>--describe</code> 直接返回 JSON schema。</p>
<pre><code class="language-bash"># Bad - 冗长描述，格式不稳定
$ myctl deploy --help
🚀 Deploy your amazing service to the cloud!
This command will take your code and deploy it to
the specified environment. Make sure you have...
(blah blah 20 lines)

# Good - 简洁固定格式
$ myctl deploy --help
Usage: myctl deploy [options]

Options:
  --env &lt;string&gt;     Target environment (staging|production) [required]
  --tag &lt;string&gt;     Image tag, e.g. v1.0.0 [required]
  --dry-run          Preview changes without executing
  --yes              Skip confirmation prompt
  --json             Output as JSON

# Better - 提供 --describe 返回 JSON schema（可选）
$ myctl deploy --describe
{
  &quot;command&quot;: &quot;deploy&quot;,
  &quot;parameters&quot;: [
    {&quot;name&quot;: &quot;env&quot;, &quot;type&quot;: &quot;string&quot;, &quot;required&quot;: true, &quot;enum&quot;: [&quot;staging&quot;, &quot;production&quot;]},
    {&quot;name&quot;: &quot;tag&quot;, &quot;type&quot;: &quot;string&quot;, &quot;required&quot;: true},
    {&quot;name&quot;: &quot;dry-run&quot;, &quot;type&quot;: &quot;boolean&quot;, &quot;default&quot;: false}
  ],
  &quot;risk_tier&quot;: &quot;high&quot;
}
</code></pre>
<h3>Rule 7: 同时支持扁平标志和 JSON 负载输入</h3>
<p>人类习惯 <code>--title &quot;My Doc&quot;</code> 这种扁平标志，Agent 更喜欢直接扔一坨 JSON 过来，跟 API schema 一一对应，零翻译损失。两种都得支持。</p><p><a href="https://blog.sorrycc.com/agent-friendly-cli">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>637 - 《AI 时代的工程师团队》</title>
      <link>https://blog.sorrycc.com/ai-era-engineering-team</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/ai-era-engineering-team</guid>
      <pubDate>Mon, 20 Apr 2026 12:34:52 GMT</pubDate>
      <description><![CDATA[<p><img src="https://pic.sorrycc.com/proxy/1777026405889-757165940.png" alt=""></p>
<p>如果今天让我从零搭一个工程师团队，这些是我的死线，不接受讨价还价。</p>
<h3>1. <strong>人均至少 Claude 5x Max 账户</strong></h3>
<p>最新模型要能爽用，不能用就别干别的，优先解决这个问题。Claude 官方订阅是目前最划算和稳定的 Opus 渠道，据我所知国内一些 AI IDE 团队也都在用 Claude Code。这不是什么「AI 工具补贴」，这是水电煤。你不会让工程师用 2G 网络写代码，同理你不应该让他们用阉割版的模型干活。</p>
<h3>2. <strong>适配 Claude 5h Session Limit</strong></h3>
<p>与其骂它不够用，不如围绕它设计节奏。早用完的可以早吃饭、早下班，错峰使用。这不是摸鱼，是资源调度。</p>
<h3>3. <strong>从老板到工程师，都得是重度 Claude Code 用户</strong></h3>
<p>没得商量。不重度用 Claude Code 的人，还没入门。老板自己不是重度用户，就不可能理解团队的真实瓶颈，做出的决策全是拍脑袋。整个团队必须在同一个信息赛道上，不然你会发现大量时间花在「解释为什么要这么做」上面，而不是真正做事。一个用 AI 的人和一个不用 AI 的人，对同一个问题的认知可能差了一个世代，这种差距带来的内耗是毁灭性的。</p>
<h3>4. <strong>每人额外配一台 Mac mini</strong></h3><p><a href="https://blog.sorrycc.com/ai-era-engineering-team">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>636 - 《Claude Code 账号》</title>
      <link>https://blog.sorrycc.com/claude-code-account</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/claude-code-account</guid>
      <pubDate>Mon, 20 Apr 2026 07:40:09 GMT</pubDate>
      <description><![CDATA[<p>聊聊我的 Claude Code 账号，不是教程，个人经验，供参考。</p>
<p><img src="https://pic.sorrycc.com/proxy/1776670756418-552837017.png" alt=""></p>
<h2>账号</h2>
<p>Anthropic 老号，注册一年多了。我觉得这个可能比什么都重要，老号的风控容忍度和新号不是一个级别的。新号建议先养养再升会员，别上来就冲 Max，容易触发风控。</p>
<h2>支付</h2>
<p>Apple 美区礼品卡。流程不复杂：</p>
<ol>
<li>关代理</li>
<li>支付宝切美区，超值抢购区域买礼品卡，背后是 Pockyt Shop</li>
<li>打开 Apple Store 兑换</li>
<li>Anthropic 里选 Apple Pay 订阅</li>
</ol>
<p>虚拟卡据说容易封号，身边有人遇到过，我自己没试，也不打算试。礼品卡这条路走通了就别折腾了。</p>
<h2>网络</h2>
<p>家宽 IP，不搭中转，直连。少一层就少一层风险，和我之前在「我不再做的事」里写的一样，不用 AI 中转。</p>
<p>家宽 IP 在 <a href="https://www.webshare.io/?referral_code=w4zxzx7kqt51">webshare.io</a> 买的，$2 不到一个，之前写过一篇「美国家宽」有详细步骤。IP 本身不贵，我觉得更重要的是确保所有出口到 Claude 的流量走同一个 IP。我在 Surge 里单独配了一组规则，把 Claude 相关的域名全部指向 webshare 的代理，其他流量走别的出口，互不干扰。</p><p><a href="https://blog.sorrycc.com/claude-code-account">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>635 - 《Release Sokki：因为 Espanso 把我原文吃了》</title>
      <link>https://blog.sorrycc.com/release-sokki</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/release-sokki</guid>
      <pubDate>Sun, 19 Apr 2026 13:33:15 GMT</pubDate>
      <description><![CDATA[<p>之前一直用 Espanso 做文本扩展，但它太重了，而且经常遇到奇怪的问题——最烦的是 replace 的时候会把我原来打的内容也一起删掉。忍了很久，干脆自己写一个，就是 Sokki（速記）。</p>
<h2>是什么</h2>
<p>Sokki 是一个 macOS 菜单栏的文本扩展工具，做的事情很简单：你打一个触发词（比如 <code>:hello</code>），它帮你替换成一段预设文本。就这样。</p>
<p>要求 macOS 14+。</p>
<p><img src="https://pic.sorrycc.com/proxy/1776672043517-346963789.png" alt=""></p>
<p><img src="https://pic.sorrycc.com/proxy/1776672090911-930409050.png" alt=""></p>
<h2>功能</h2>
<p><strong>YAML 配置。</strong> 所有规则都写在 <code>~/.config/sokki/match/</code> 下的 YAML 文件里，想怎么分文件组织都行，字母序加载，后加载的覆盖先加载的。改完自动热重载，不用重启。</p>
<pre><code class="language-yaml">matches:
  - trigger: &quot;:hello&quot;
    replace: &quot;Hi there!&quot;

  - trigger: &quot;:today&quot;
    replace: &quot;{{date:%Y-%m-%d}}&quot;

  - trigger: &quot;:sig&quot;
    replace: |
      Best regards,
      {{clipboard}}
</code></pre>
<p><strong>变量支持。</strong> 内置 <code>{{clipboard}}</code> 和 <code>{{date:格式}}</code> 两个变量，日期格式用标准的 strftime。未知变量保留原样，不会崩也不会静默吞掉。</p>
<p><strong>自动注入模式。</strong> 短文本走键盘模拟（逐字符 CGEvent 发送），长文本走剪贴板粘贴。阈值可配。剪贴板模式会先做完整 snapshot——字符串、RTF、图片、文件 URL 全部保留——粘贴完再 restore 回去，不会动到你原来的剪贴板内容。</p>
<p><strong>撤销。</strong> 扩展完之后 3 秒内，按 backspace 删掉刚插入的内容，Sokki 会识别出是撤销意图，把原来的触发词还给你。</p>
<p><strong>密码框自动暂停。</strong> 通过 <code>IsSecureEventInputEnabled()</code> 检测，在密码输入框里完全不做匹配，菜单栏图标会变灰提醒你。出了密码框会清空缓冲区，不会让密码字符跨边界残留。</p>
<p><strong>快捷开关。</strong> 可配置快捷键（双击 Option/Shift/Control 之类）快速启用/禁用匹配，不用去点菜单栏。默认不绑定，避免误触。</p>
<p><strong>Spotlight 搜索面板。</strong> 双击配置的快捷键调出一个浮动输入框，模糊搜索所有触发词和替换内容，回车直接执行——适合记不住完整触发词，或者规则多到懒得分类的时候。</p>
<p><strong>自动更新。</strong> 基于 Sparkle，EdDSA 签名校验。不会发任何系统信息。</p>
<h2>一些细节</h2>
<ul>
<li>粘贴（Cmd+V）会触发 200ms 的匹配静默窗口，避免你粘进来的内容里正好包含触发词导致误触发</li>
<li>切换应用时自动清空 buffer 和 trie 游标，避免在 Safari 打了 <code>hel</code> 切到 Terminal 打 <code>lo</code> 触发 <code>:hello</code></li>
<li>前缀消歧：如果同时存在 <code>:he</code> 和 <code>:hello</code>，打到 <code>:he</code> 会进入 pending 状态，继续打到 <code>:hello</code> 就 fire 长的，打空格就 fire 短的</li>
<li>匹配用 trie 做，每个字符 O(1)，几百上千条规则也无压力</li>
<li>触发词检测全在 CGEvent tap 的专用线程上跑，用 <code>OSAllocatedUnfairLock</code> 锁，关键路径亚微秒级，不占主线程</li>
<li>配置文件有语法错误时保留上一份能用的配置，错误显示在菜单栏 tooltip 里，不会整个挂掉</li>
<li>日志在 <code>~/.config/sokki/logs/sokki.log</code>，1MB 滚动，默认只记 error，要调试改成 debug</li>
<li>匹配列表可按源文件过滤、按触发词和内容搜索，选中按 Delete 删除；编辑器里显示每条规则来自哪个文件</li>
</ul>
<h2>技术</h2>
<p>Swift 6，纯 SPM 构建，没有 Xcode 工程文件。UI 是 SwiftUI + AppKit 混合——菜单栏用 NSStatusItem，设置界面用 SwiftUI。依赖只有 Yams（YAML 解析）和 Sparkle（自动更新）。签名 + Apple 公证 + Sparkle EdDSA，完整的独立分发链路，和 Shun 共用同一套发布基础设施。</p>
<h2>下载</h2>
<p><a href="https://download.sorrycc.dev/sokki/Sokki-latest.dmg">Sokki-latest.dmg</a></p>
<p>macOS 14 (Sonoma) 及以上。下载后拖到 Applications 文件夹就行。首次打开需要授予辅助功能和输入监控权限（CGEvent tap 要用），在系统设置 → 隐私与安全性里放行。</p>
]]></description>
    </item>
    <item>
      <title>634 - 《Release Shun：macOS 全局快捷键切换应用》</title>
      <link>https://blog.sorrycc.com/release-shun</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/release-shun</guid>
      <pubDate>Sat, 18 Apr 2026 00:57:19 GMT</pubDate>
      <description><![CDATA[<p>用了很多年 Thor 来做快捷键切换应用，但 Thor 已经很久没更新了，在新版 macOS 上小毛病不少。干脆自己写一个，就是 Shun（瞬）。</p>
<h2>是什么</h2>
<p>Shun 是一个 macOS 菜单栏工具，做的事情很简单：给应用绑一个全局快捷键，按一下就切过去。如果这个应用已经在最前面，再按一下就隐藏它。就这样。</p>
<p>要求 macOS 14+。</p>
<p><img src="https://pic.sorrycc.com/proxy/1776356535786-85374041.png" alt=""></p>
<p><img src="https://pic.sorrycc.com/proxy/1776356593804-890547415.png" alt=""></p>
<h2>功能</h2>
<p><strong>快捷键绑定。</strong> 在设置界面搜索或拖入应用，录一个快捷键，完事。支持启用/禁用单条快捷键，支持拖拽排序。</p>
<p><strong>Toggle 行为。</strong> 按快捷键时，如果目标应用在后台就激活它；如果它已经是最前面的窗口，就隐藏。用起来很自然，不需要额外的&quot;隐藏&quot;操作。</p>
<p><strong>Hyper Key。</strong> 如果你用 Karabiner-Elements 把 Caps Lock 映射成了 Hyper Key（⌃⌥⇧⌘），Shun 能正确识别。这是很多同类工具做不到的，因为 Karabiner 生成的是合成修饰键，传统的 Carbon API 读不到，Shun 用 CGEvent tap 来处理这个问题。</p>
<p><strong>Usage Insights。</strong> 会记录最近 7 天每个快捷键的使用次数，在快捷键列表里直接显示小徽章。设置里有一个 Insights 标签页，可以看哪些应用用得最多。纯本地数据，不上传。</p>
<p><strong>Hotkey Recipes。</strong> 可以把你的快捷键配置导出为 <code>.hotrecipe</code> 文件，分享给别人或者在新机器上导入。导入时会自动匹配 Bundle ID，找不到的会用应用名回退查找，还会检测快捷键冲突。</p>
<p><strong>自动更新。</strong> 基于 Sparkle，每天检查一次新版本，有更新会弹通知，一键安装。不会发送任何系统信息。</p>
<p><strong>开机启动。</strong> 设置里一个开关搞定。</p>
<h2>一些细节</h2>
<ul>
<li>菜单栏图标点开能看到所有已绑定的快捷键，正在运行的应用旁边有绿点标记</li>
<li>如果某个应用被删了或者移了位置，列表里会灰显并加警告标志</li>
<li>绑定快捷键时会检测冲突，避免和系统快捷键或已有绑定撞车</li>
<li>首次启动会自动打开设置界面，不用去找</li>
<li>数据存在 <code>~/Library/Application Support/Shun/</code>，就是 JSON 文件，干净透明</li>
</ul>
<h2>技术</h2>
<p>Swift 6，纯 SPM 构建，没有 Xcode 工程文件。UI 是 SwiftUI + AppKit 混合——设置界面用 SwiftUI，菜单栏用 AppKit 的 NSStatusItem。签名 + Apple 公证 + Sparkle 自动更新，完整的独立分发链路。</p>
<h2>下载</h2>
<p><a href="https://download.sorrycc.dev/shun/Shun-latest.dmg">Shun-latest.dmg</a></p>
<p>macOS 14 (Sonoma) 及以上。下载后拖到 Applications 文件夹就行。首次打开如果提示安全警告，去系统设置 → 隐私与安全性里放行。</p>
]]></description>
    </item>
    <item>
      <title>633 - 《我的前半生流水账》</title>
      <link>https://blog.sorrycc.com/life-timeline</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/life-timeline</guid>
      <pubDate>Fri, 17 Apr 2026 05:57:02 GMT</pubDate>
      <description><![CDATA[<ul>
<li><strong>1984</strong>，出生在浙江温岭新河镇。</li>
<li><strong>新河小学</strong>，<strong>新河镇中学</strong>，<strong>温岭中学</strong>。小镇少年的标准升学路线。</li>
<li><strong>2001</strong>，高考超水平发挥，考入浙江大学，信息管理专业。冲着&quot;信息&quot;去的，其实是文科。大学五年（留了一级），逃了 80% 的课，玩游戏、搭私服、混社团。毕业时唯一拿得出手的技能，是在社团学的网页技术。把高考的运气在大学里全还回去了。</li>
<li><strong>2005</strong>，第一份工作，杭州梦速科技。在居民楼里办公，职位是美工。ZJU 毕业去当美工，有点丢人，但不会别的。</li>
<li><strong>2006</strong>，第二份工作，爆米花科技。视频网站，岗位网页设计师，做 HTML+CSS。认识了爆牙齿、完颜。要求加薪 500 被拒，裸辞。少年心里还是有股傲气的。</li>
<li><strong>2008</strong>，入职淘宝。把阿里子公司投了个遍，全被拒，唯独留着最想进的淘宝没投。小马看了我的博客主动发邮件邀请，面试考了闭包，感觉当时会闭包就能进。运气真好。同年 7 月，大娃出生。人生大年。</li>
<li><strong>2008-2013</strong>，淘宝时期。做过淘宝首页、宝贝详情、购物车、下单、全网页头页尾，都是日 PV 几十亿的产品。出过一次准 P1 故障，全网交易额下降 12%，学会了四个字——敬畏之心。从 P5 升到 P7。用现在看来非常土的技术，做着最有价值的业务。</li>
<li><strong>2013</strong>，转岗支付宝。主因是淘宝搬了淘宝城，离家太远。玉伯挖过来的。第一年做收银台业务，各种不适应，拿了在阿里唯一的一次 3.25。</li>
<li><strong>2013 起</strong>，开始漫长的工具之旅：spm → atool-build → roadhog → dva → babel-plugin-import → umi → father → Mako → Neovate Code → Neovate Desktop。</li></ul><p><a href="https://blog.sorrycc.com/life-timeline">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>632 - 《装了啥 2026》</title>
      <link>https://blog.sorrycc.com/zhuang-2026</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/zhuang-2026</guid>
      <pubDate>Thu, 16 Apr 2026 09:57:29 GMT</pubDate>
      <description><![CDATA[<blockquote>
<p>去年 [[536 - 《装了啥 2025》]] 的开头，我写&quot;最近更新了很多 AI 相关的工具&quot;。那时候觉得挺多了，现在回头看，那只是起点。</p>
</blockquote>
<p>过去这一年，AI Coding 把我的 Launchpad 塞满了——Claude Code、Codex、Cursor，外加一堆冒出来又消失的 Agent 客户端和 AI IDE，光我装过的就快二十个。所以这一版起，把 <strong>AI Coding</strong> 从 AI 里单独拆出来。</p>
<p>其他几个大变化：</p>
<ul>
<li>Alfred 让位给 Raycast（双跑中）</li>
<li>ZeroTier 换成 Tailscale</li>
<li>Thor 和 Espanso 换成我自研的 HotApp 和 EspansoX</li>
<li>Screens 直接被 macOS 内置的屏幕共享替掉</li>
<li>SetApp 去年预告要走，今年真走了</li>
<li>Surge 付费升到 6.x</li>
<li>语音输入直接用微信 Mac 自带的</li>
</ul>
<h2>AI Coding</h2>
<blockquote>
<p>2026 版新增分类。</p>
</blockquote>
<ul>
<li>IDE 主力依旧是 <a href="https://www.cursor.com/">Cursor</a>，但位子已经摇摇欲坠——AI 编程的场景下，coding 已经不太需要 edit 了，更多时间在和 AI 对话，IDE 的意义在下降。过去一年冒出来的那堆 AI 编辑器（Windsurf、Trae、Zed、Void、Qoder、Kiro、Antigravity……）也都试过，没一个接住 Cursor。</li>
<li>命令行主力是 <a href="https://github.com/anthropics/claude-code">Claude Code</a>，辅以 <a href="https://github.com/openai/codex">Codex</a>。今年我 90% 的代码都是在命令行里写出来的。</li>
<li>Anthropic 最近推出了 <a href="https://code.claude.com/docs/en/desktop">Claude Code 桌面端</a>，也开始用上了。</li>
<li>多会话管理用 <a href="https://cmux.com/">cmux</a>，一次并行开四五个 Claude Code Session，互不打扰。</li>
<li>本地模型跑 <a href="https://lmstudio.ai/">LM Studio</a>。</li>
</ul>
<h2>AI</h2>
<blockquote>
<p>2026 版变更：在线服务只剩 Claude、ChatGPT 和 Zenmux 订阅；ChatWise 不再用；语音输入换成微信 Mac 自带。</p>
</blockquote>
<ul>
<li>在线 AI 服务只剩 <a href="https://claude.ai/">Claude</a> 和 <a href="http://chat.openai.com/">ChatGPT</a>，外加一个 <a href="https://zenmux.ai?invite_code=IC6ruUAf14631081">Zenmux</a> 的订阅——贵，但余额还没花完且稳定，先留着。Google AI Studio、Grok、Perplexity、DeepSeek 今年都不太用了。</li>
<li>本地客户端装了 <a href="https://claude.ai/download">Claude</a> 和 <a href="https://openai.com/chatgpt/desktop/">ChatGPT</a> 的桌面版，还有自家的 Neovate Desktop——不过对外开源版之后不再维护，属于短期方案。</li>
<li>API 方式今年基本不用了，ChatWise 也停了。日常够用的就是 Claude Code 和 ChatGPT 的订阅，划算。</li>
<li>语音输入直接用<strong>微信 Mac 应用自带的</strong>。Wispr Flow 和 SuperWhisper 都用过，微信这个免费、准确率够用、跨应用可用，就留下了。</li>
</ul>
<h2>科学上网</h2>
<blockquote>
<p>和去年没啥变化，Surge 付费升到 6.x。</p>
</blockquote>
<ul>
<li>机场：主机场 <a href="https://y-too.com/aff.php?aff=3277">YTOO</a>（用了两年多），备机场 <a href="https://thirdislandchain.com/aff.php?aff=1717">IPLC.VIP</a>（用了四年多）。</li>
<li>软件：Mac、iPhone 和 Apple TV 上用 <a href="https://nssurge.com/">Surge</a>（今年花三百多升到 6.x），Windows 上用 Clash for Windows。</li>
</ul>
<h2>编程工具</h2>
<blockquote>
<p>2026 版变更：编程字体不再提了——AI 写代码的场景下，盯屏时间少了一半，字体不再重要；Warp 和 Ghostty 都浅尝辄止，iTerm2 继续主力。</p>
</blockquote>
<ul>
<li>终端：<a href="https://iterm2.com/">iTerm2</a> + zsh + <a href="https://github.com/robbyrussell/oh-my-zsh">oh-my-zsh</a> + <a href="https://starship.rs/">starship</a>，加 zsh-autosuggestions、zsh-completions、fast-syntax-highlighting 三个插件。<a href="https://www.warp.dev/">Warp</a> 和 <a href="https://ghostty.org/">Ghostty</a> 都装了，都没接住 iTerm2。</li>
<li>HTTP 抓包/调试用 <a href="https://httptoolkit.com/">HTTP Toolkit</a>，代替去年的 RapidAPI。</li>
<li>其他：Hosts 管理 <a href="https://github.com/2ndalpha/gasmask">Gas Mask</a>、GUI Git <a href="https://www.sourcetreeapp.com/">SourceTree</a>、辅助工具集 <a href="https://devutils.com/">DevUtils</a>、取色 <a href="https://colorsnapper.com/">ColorSnapper2</a>、Docker 客户端 <a href="https://orbstack.dev/">OrbStack</a> 。</li>
</ul>
<h2>服务器</h2>
<blockquote>
<p>2026 版变更：CloudCone 出过一次故障数据全丢，不再用（虽然还有余额）；搬瓦工只留 JP 一台，US 那台没续费；VKVM 也不用了；博客迁到 Cloudflare。</p>
</blockquote>
<ul>
<li><a href="https://www.cloudflare.com/">Cloudflare</a>，$5/月，博客 <a href="https://sorrycc.com/">sorrycc.com</a> 今年迁到这里，量很足。</li>
<li><a href="https://bandwagonhost.com/aff.php?aff=74975">搬瓦工</a> JP，1C2G，年付 $74.57，跑零散的小服务，国内访问 ping 值在 100 以内。</li>
<li>阿里云 ¥99/年的机器，用来做 <a href="https://tailscale.com/">Tailscale</a> 的 Peer Relay。</li>
</ul>
<h2>知识管理</h2>
<blockquote>
<p>2026 版变更：ReadWise Reader、DeepL + Bob + Immersive Translation、飞书妙记、iA Writer、iA Presenter、Flomo 都去掉——AI 接管了翻译和总结；任务管理段去掉。</p>
</blockquote><p><a href="https://blog.sorrycc.com/zhuang-2026">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>631 - 《也聊 Harness》</title>
      <link>https://blog.sorrycc.com/on-harness</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/on-harness</guid>
      <pubDate>Thu, 16 Apr 2026 09:17:00 GMT</pubDate>
      <description><![CDATA[<p>也聊 Harness 。</p>
<p>前几天开会，有个议题是聊你理解的 Harness，整理了下思路如下。</p>
<p>先说下背景，我理解之所以有 Harness，是因为我们对提效的追求又进了一步。最早我们聊 prompt engineer，只有 2x 的提效；然后到 context engineer，差不多有 5x 的提效；再到 harness，我觉得是有可能做到 10x - 100x 的提效的。</p>
<p>为什么能跨数量级？因为前两者优化的还是「人一次次驱动 AI」这种协作模式，AI 链条里只要有人的节点就有瓶颈。Harness 优化的是节点数本身——通过 loop、多 agent、预设 skill，把人从链条的各个环节上拔出来。链条越长、人越少，提效越呈指数。</p>
<hr>
<p>我对 Harness 的理解，与其说是编排，不如用一个更精准的比喻：它是「马具」。马具不代替马，是让人能驾驭马。对应到 AI，Harness 不是让模型更聪明，而是把模型外面的规则、工具、技能文件和反馈循环包起来，让人能高效驾驭它。所以它必然超越单个 Agent。</p>
<p>形态有很多种，比如 Loop、流、连接等。</p>
<p>1/ Loop 举一些例子，比如 ralph loop、各种 self improve loop。self improve loop 里最成熟的一种范式，就是 Hashimoto 说的「agent 出错时别手动修，要问怎么让它永远不再犯同样的错」，然后把答案固化进系统——常见的形态有 skill 自进化、post memory、或者简单到当 coding 阶段出错时多问一句「怎么不出错」。再比如产品发布后收集日志信息然后自己提 pr 修复问题。</p>
<p>2/ 流也有多种形态，比如 Workflow、Agent Team、Cron Tasks 等。这里需要和传统自动化（n8n、Zapier、Workflow 平台）做一下区分——骨架看起来一样，但节点里装的东西不一样。传统自动化每一步是确定性的 if-else，harness 每一步都可以包含模型判断。这才是 harness 能到 10-100x 而传统自动化到不了的原因。</p>
<p>举几个流的例子。比如当 github 有新 pr 时，自动做 review 给反馈，并且到有新的变更时，自动做增量 Review。比如当 save 了新的 x bookmarklet，read with bird，然后做详细的 summarize，然后分别用 opus 4.7 和 gpt 5.4 用 write-tweet skill 写一篇供选择。比如每天早上抓国内 n 个渠道的新闻汇总和抓 x 的 list 做 ai 新闻汇总。比如 youmind caicai 的<a href="https://www.caicai.me/zh/blogs/stop-hiring-start-deploying-agents">这篇博客</a>，把 Agent 作为员工的思路。</p>
<p>再多举几个流的例子。比如 neovate desktop 要定期跟进 claude sdk 的依赖升级，我干完一次后，配个流，每天早上检查是否有更新，然后升依赖，验证，提 pr，然后这件事我就再也不需要管了。</p><p><a href="https://blog.sorrycc.com/on-harness">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>630 - 《一场 10 分钟的发布会，我和 Claude 聊了 20 多轮》</title>
      <link>https://blog.sorrycc.com/10min-launch-with-claude</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/10min-launch-with-claude</guid>
      <pubDate>Thu, 16 Apr 2026 06:22:22 GMT</pubDate>
      <description><![CDATA[<p><img src="https://pic.sorrycc.com/proxy/1776320429014-757398955.jpg" alt=""></p>
<p>我花了 3 个半小时准备一场 10 分钟的发布会。其中写字的时间不到 30 分钟，剩下的全是跟 Claude Code 来回拉扯。</p>
<p>最后落地的是三个文档，发布稿大纲、slides 设计、逐页文字稿。</p>
<hr>
<h2>起点，先认识产品，不要急着写</h2>
<p>第一轮 Claude 没有直接给大纲。它派了个 Explore agent 去读整个 codebase，package.json、README、主要的 feature 模块、commit history，然后才动笔。</p>
<p>这个习惯值得保留。你在发布会上讲产品，必须自己（或帮你干活的 agent）先把产品摸透。否则出来的东西就是 README 的换皮版。</p>
<p>第一版大纲出来之后，我看着很漂亮但也很平庸。分三部分，痛点、演示、展望。每条都扎实，但合在一起就是套模板，你把产品名换成任何竞品，这份大纲都能用。</p>
<p>漂亮是漂亮，但平庸。。。</p>
<p>问题不在内容，在结构。</p>
<hr>
<h2>换一个方法论</h2>
<p>我就想，换个思路。</p>
<p>让 Claude 用 <code>/write-twitter</code> 这个 skill 重新审视这份大纲。</p>
<p>Twitter 技能里有一套病毒传播的方法论，Hook、PAS（Problem-Agitate-Solution）、对比冲突、具体数字替代笼统表达。这些东西拿到发布会演讲上也好用。</p>
<p>第二版回来之后，变化有三个。</p>
<p>开场不再是「大家好今天要介绍……」，换成一个有冲突感的判断。痛点不再是「用户面临的挑战」，换成「三个凑合」。演示不再是功能清单，换成「6 个没想到」。</p>
<p>看着更像一份能讲起来的东西。但还不够，因为它离我这个具体的产品还远。</p>
<hr>
<h2>真实素材比 AI 联想重要得多</h2>
<p>说真的，这是整个过程里最关键的一步。</p>
<p>我把产品真实的 11 个核心功能丢给 Claude，还附了两个参考文件，一个 jsonl 是我之前用 Claude Code 分析这个项目的对话记录，另一个 markdown 是我之前写过的前端工具链描述。</p>
<p>这一步之后大纲彻底换了一副样子。</p>
<p>举个例子。</p>
<p>AI 第一轮给我写的「三件不能忍的事」，第三条是，配置靠改环境变量，换 Provider 要 <code>cc switch</code>，加 Skill 要改文件，手机上看一眼进度？没门。</p>
<p>这条听起来挺有道理。但我读第二遍就觉得别扭，一条里混了三个不同层次的痛点，Provider 配置、Skill 管理、远程控制。每一个单拿出来都不够扎。</p>
<p>我重写成，Agent 干完活，然后呢？改完代码回到终端，你得自己 review、自己测试、自己提交。你用 AI 是为了解放自己，结果你变成了 AI 的收尾工。</p>
<p>只戳一个点，但戳得深。</p>
<p>再举个反例。「多任务并行」如果让 AI 自己发挥，它会写成「提升多任务效率」这种四平八稳的东西。我的真实想法是「我们不做 git worktree，手动复制项目反而更简单」，这种略带偏见的主观判断，AI 给不了你。</p>
<p>一个教训，<strong>不要指望 AI 凭空给你写出一份有态度的东西</strong>。AI 给的是 80 分的合理，你要的是 95 分的扎心。</p>
<p>就这 15 分。</p>
<p>得你自己补。</p>
<hr>
<h2>扩展，再收缩</h2>
<p>第三版是 10 分钟。我说能不能扩到 15 分钟多讲点，Claude 出了 v4，加了 Cmd+K、Session 管理等细节。</p>
<p>然后我说再压回 10 分钟。</p>
<p>压缩的时候做了几个判断题。哪些可以合并（Cmd+K 和 Alt+N 合一页）。哪些可以一笔带过（Provider 管理压到 45 秒）。哪些必须保留独立篇幅（Post-Agent 生命周期，90 秒）。</p>
<p>扩展-收缩这个动作看起来像浪费时间，其实不是。扩展是为了让你看清楚有哪些可选项，收缩是为了逼你做取舍。先扩后缩出来的版本，比直接写 10 分钟版本要扎实得多。</p>
<hr>
<h2>让不同人格来评审</h2>
<p>这里做了个离谱的实验。我当时也没想到能行，寻思试试呗。</p>
<p>我让 Claude 分别以不同人格来评审这份稿子。</p>
<p><strong>川普的视角</strong>最有用。真实对话是这样的。</p>
<blockquote>
<p>我问，你是「唐纳德川普」，any suggestion to improve this 发布稿?</p>
<p>Claude 答，
你太谦虚了，赢家不说「凑合」。「凑合」是认输的人说的话。你是来赢的。
标题改成「三件不能忍的事」，或者「三件不该忍的事」。</p>
</blockquote>
<p>这条我采纳了。他还建议我收尾用排比结构（「从 X 到 Y」三连），我一开始采纳，后来又用更干净的方案替代。</p>
<p><strong>马丁·路德·金的视角</strong>真实对话。</p>
<blockquote>
<p>我问，你是「马丁·路德·金」，any suggestion to improve this 发布稿?</p>
<p>Claude 答，
你的金句都和具体功能绑定，缺少一句可以脱离上下文、被单独转发的话。
建议加一句，最好的工具，不是让你更快地工作，是让你觉得这才是工作本来该有的样子。</p>
</blockquote>
<p>这句话本身不差，但太大了，硬塞进稿子里像贴广告。= =</p>
<p>我拒绝了。</p>
<p>但 MLK 的另一条建议我采纳了，他说「三个凑合」并列排列缺少递进，应该像浪潮一样一浪高过一浪。后来痛点三的那句「你用 AI 是为了解放自己，结果你变成了 AI 的收尾工」就是照他的建议加的。</p>
<p><strong>Linus Torvalds 的视角</strong>我一开始就设定了这个基调，让整份稿子保持工程师的克制感。</p>
<p>换人格评审这件事说到底，就是<strong>强迫 AI 从不同审美维度看你的东西</strong>。AI 自己写稿默认是中庸平稳的，要让它给你狠话、给你反共识的判断、给你金句，你得给它一个角色。</p>
<hr>
<h2>用当下的新闻给稿子加锚点</h2>
<p>接近终稿的时候，我让 Claude 用 <code>/last30days</code> 去查最近 30 天 AI IDE 赛道发生了什么。</p>
<p>回来的信息很有用。Cursor 刚发了 3.0，主推 Agents Window。OpenAI Codex 桌面端破了百万下载。TRAE 发了 New Solo。</p>
<p>我把这段加进了 Hook，</p>
<blockquote>
<p>这个月 Cursor 发了 3.0，Codex 桌面端破了百万下载，TRAE 发了 New Solo。所有人都在给 AI Agent 造房子。但我们造的这一间，是从 Claude Code 自己内部长出来的。</p>
</blockquote><p><a href="https://blog.sorrycc.com/10min-launch-with-claude">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>629 - 《从 ZeroTier 切到 Tailscale：自建 DERP 中继踩坑记》</title>
      <link>https://blog.sorrycc.com/zerotier-to-tailscale-derp</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/zerotier-to-tailscale-derp</guid>
      <pubDate>Wed, 08 Apr 2026 05:51:50 GMT</pubDate>
      <description><![CDATA[<p>之前一直用 ZeroTier 组网，连我的 Mac Mini、MacBook 和阿里云 VPS。能用，但体验一般：节点发现慢，NAT 打洞成功率不稳，偶尔断连要手动重启服务。上周续费了阿里云 ¥99/年的轻量主机（杭州），顺手把组网方案切到了 Tailscale，并用 Claude Code 全程 SSH 上去搭了一个自建 DERP 中继节点。</p>
<p><img src="https://pic.sorrycc.com/proxy/1775627400566-488859941.png" alt=""></p>
<p>为什么要自建 DERP：Tailscale 开箱即用，体验比 ZeroTier 好很多。但它的 DERP 中继服务器遍布全球唯独没有中国大陆节点。国内设备 P2P 打洞失败时，流量绕海外中继再回来，延迟 300ms 起步。自建一个国内节点，可以压到 20ms 以内。</p>
<p>搭建步骤：</p>
<p>① 域名 A 记录指向 VPS IP。DERP 跑在 HTTPS 上（伪装成网页流量穿透防火墙），需要 TLS 证书，证书需要域名。</p>
<p>② VPS 上装 Go，编译 derper：
GOPROXY=<a href="https://goproxy.cn,direct">https://goproxy.cn,direct</a> go install tailscale.com/cmd/derper@latest
国内 VPS 必须设 GOPROXY，否则访问 Google 的 Go 模块代理会超时。</p>
<p>③ 用 systemd 跑起来，配上 --certmode letsencrypt 自动签证书。</p>
<p>④ Tailscale 控制台 ACL 里加 derpMap，填 HostName、IPv4、端口。OmitDefaultRegions 留 false，官方节点做备份。</p>
<p>踩了一个大坑：</p>
<p>搭好当天一切正常，第二天 Tailscale 后台报 Relay Server Unavailable。SSH 上去看 derper 在跑，服务器 curl 自己也返回 200。但外部连 443 端口全部 TLS 握手失败，connection reset by peer。</p><p><a href="https://blog.sorrycc.com/zerotier-to-tailscale-derp">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>628 - 《不写代码的程序员 - 我怎么用 AI 编程 2026.03》</title>
      <link>https://blog.sorrycc.com/programmer-without-code</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/programmer-without-code</guid>
      <pubDate>Tue, 31 Mar 2026 11:07:13 GMT</pubDate>
      <description><![CDATA[<blockquote>
<p>内部分享于 @陆辉 团队，2026.03.31 。</p>
</blockquote>
<blockquote>
<p>不写代码了，那我在干什么？</p>
</blockquote>
<blockquote>
<p>聊聊我现在日常在用什么工具、怎么配置、工作流长什么样，以及一些比较进阶的玩法，比如 agent team、one-shot、各种 loop。都是实际在用的东西，不讲概念。</p>
</blockquote>
<h2>开始之前</h2>
<ul>
<li>这是我个人实战总结的方法，不代表唯一解</li>
<li>工具和配置可能 1 个月就过期，但工作流的设计思路不会</li>
</ul>
<h2>社区大佬</h2>
<p>Peter Steinberger, OpenClaw 作者</p>
<p><img src="https://pic.sorrycc.com/proxy/1774938576121-667806361.png" alt=""></p>
<p>Boris Cherny，Claude Code 作者</p>
<p><img src="https://pic.sorrycc.com/proxy/1774938631992-651450909.png" alt=""></p>
<h2>关于我</h2>
<ul>
<li>云谦 / 陈成</li>
<li>目前 AI 编码率 100%，上一次手写代码的时间已经记不清了</li>
<li>写了 17 年前的工具和框架，umi、dva、mako 等库作者</li>
<li>去年 2025 开始写 code agent 工具，开源了 neovate code</li>
<li>今年转做基于 claude code 的 GUI 应用 neovate desktop</li>
<li><a href="https://x.com/chenchengpro">https://x.com/chenchengpro</a></li>
<li><a href="https://blog.sorrycc.com/">https://blog.sorrycc.com/</a></li>
<li><a href="https://github.com/neovateai/neovate-desktop">https://github.com/neovateai/neovate-desktop</a></li>
</ul>
<h2>开场：一个 live demo</h2>
<ul>
<li>用 /one-shot 完成几个实际需求，然后等分享到 Workflow 结束后来验收。</li>
</ul>
<pre><code>1)
after set as project default / global default in the model selector, should invalidate the prewarmed sessions and create new

2)
plugin in main should support custom hooks
analyze /Users/chencheng/Documents/Code/test/test-claude-code/versions/2.1.81/cli.js and @anthropic-ai/claude-agent-sdk for ref

3)
increase the trigger scope of project header of the session list in multiple project mode with byProject. all place of the project header except the right action button should trigger the expand/collapse

4)
improve terminal panel. when click the file path, should open it in editor.

5)
content panel tabs support drag and drop

6)
add an a new config to general &gt; advanced, default false, when enable, show whether the session is initialized in session-list (which is implemented, and enabled with developer mode is enabled)
</code></pre>
<h2>我的 Setup</h2>
<ul>
<li>原则：用最好的模型和工具</li>
<li>Claude Code Max $100/月，日常用 Opus 4.6，复杂需求切 Opus 1M</li>
<li>Backup：Zenmux / xxx / ... ，Claude Code 挂了超了随时切</li>
<li>日常用量：Claude Code Max 5X 额度还在想办法用完</li>
</ul>
<h2>我的编程 Workflow</h2>
<ul>
<li>业界趋势：vibe coding &gt; sdd (spec driven development) &gt; harness</li>
<li>我目前处于 sdd 阶段，正在往 harness 过渡<ul>
<li>1）dont impl, analyze root cause </li>
<li><ol start="2">
<li>brainstorm 2 design</li>
</ol>
</li>
<li><ol start="3">
<li>sdd</li>
</ol>
</li>
</ul>
</li>
<li>核心流程：需求 &gt; design &gt; impl</li>
<li>根据需求复杂度选方法：<ul>
<li>简单需求：直接 vibe coding</li>
<li>不确定怎么改：先让 AI 分析原因，不要直接动手</li>
<li>复杂需求：<a href="https://github.com/obra/superpowers/blob/main/skills/brainstorming/SKILL.md">brainstorm</a> 生成 design</li>
<li>注：不用传统 sdd，这种适合于超大型需求，很少场景。</li>
</ul>
</li>
<li><a href="https://github.com/sorrycc/sorrycc.com.2026/tree/master/docs/designs">https://github.com/sorrycc/sorrycc.com.2026/tree/master/docs/designs</a></li>
<li><a href="https://github.com/neovateai/neovate-desktop/blob/master/docs/designs/">https://github.com/neovateai/neovate-desktop/blob/master/docs/designs/</a></li>
<li><a href="https://github.com/neovateai/neovate-code/blob/master/docs/designs/">https://github.com/neovateai/neovate-code/blob/master/docs/designs/</a></li>
</ul>
<pre><code>Don&#39;t implement or fix directly, analyze related files first if needed, tell me what changes are needed and ask questions if unclear.
</code></pre>
<pre><code>Don&#39;t impl direct, use brainstorm to generate design first.

[Paste text of the brainstorm skill]
</code></pre>
<h2>怎么写好 Design（重点）</h2><p><a href="https://blog.sorrycc.com/programmer-without-code">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>Claude Code Auto Mode 的实现</title>
      <link>https://blog.sorrycc.com/claude-code-auto-mode</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/claude-code-auto-mode</guid>
      <pubDate>Wed, 25 Mar 2026 01:41:36 GMT</pubDate>
      <description><![CDATA[<h1>Auto Mode 详解（Claude Code 2.1.81）</h1>
<h2>8. Auto Mode 分类器 — 决策流程详解</h2>
<h3>8.1 整体架构</h3>
<p>Auto mode 的核心安全机制是一个 <strong>AI 分类器（classifier）</strong>。当 Claude 在 auto mode 下试图调用工具时，不会直接执行，而是经过一个多层级的权限决策流水线。入口函数是 <code>WM</code>（line 544283）。</p>
<h3>8.2 决策流水线（三层快速路径 + 分类器）</h3>
<pre><code>工具调用请求
    │
    ├─ 第一层：已有权限规则判断 (aHY)
    │   └─ 如果 allow → 直接放行，重置连续拒绝计数
    │
    ├─ 第二层：acceptEdits 模式模拟
    │   └─ 假装当前是 acceptEdits 模式，重新检查权限
    │       如果在 acceptEdits 下就会被允许 → 跳过分类器，直接放行
    │
    ├─ 第三层：安全白名单（allowlist）
    │   └─ 工具在只读安全列表中 → 跳过分类器，直接放行
    │
    └─ 第四层：调用 AI 分类器 (kG8)
        └─ 向 Claude Sonnet 发送独立 API 请求，判断安全性
</code></pre>
<h4>第一层：基础权限规则</h4>
<pre><code class="language-js">// line 544283-544298
var WM = async (A, q, K, _, Y) =&gt; {
    let z = await aHY(A, q, K);  // 检查现有的 allow/deny 规则
    if (z.behavior === &quot;allow&quot;) {
        // 如果已有规则直接允许，重置连续拒绝计数
        let w = K.getAppState();
        let O = K.localDenialTracking ?? w.denialTracking;
        if (w.toolPermissionContext.mode === &quot;auto&quot; &amp;&amp; O &amp;&amp; O.consecutiveDenials &gt; 0) {
            let $ = ar6(O);  // 重置连续拒绝为 0
            bV6(K, $);
        }
        return z;
    }
</code></pre>
<h4>第二层：acceptEdits 模式快速路径</h4>
<p>对于需要 <code>ask</code> 确认的工具调用，先模拟 <code>acceptEdits</code> 模式：</p>
<pre><code class="language-js">// line 544316-544351
// 将当前模式临时改为 acceptEdits，重新检查权限
let D = await A.checkPermissions(X, {
    ...K,
    getAppState: () =&gt; {
        let P = K.getAppState();
        return {
            ...P,
            toolPermissionContext: {
                ...P.toolPermissionContext,
                mode: &quot;acceptEdits&quot;,  // 模拟 acceptEdits 模式
            },
        };
    },
});
if (D.behavior === &quot;allow&quot;) {
    // 在 acceptEdits 下就能通过 → 无需调用分类器
    Q(&quot;tengu_auto_mode_decision&quot;, {
        decision: &quot;allowed&quot;,
        fastPath: &quot;acceptEdits&quot;,  // 记录是快速路径放行
    });
    return { behavior: &quot;allow&quot;, updatedInput: D.updatedInput ?? q };
}
</code></pre>
<p><strong>设计思路</strong>：如果操作在 <code>acceptEdits</code>（允许编辑文件）模式下就合法，说明风险很低，不必浪费一次 API 调用去问分类器。</p>
<h4>第三层：安全工具白名单</h4>
<pre><code class="language-js">// line 544355-544374
if (cHY.isAutoModeAllowlistedTool(A.name)) {
    // 工具在预置白名单中，直接放行
    Q(&quot;tengu_auto_mode_decision&quot;, {
        decision: &quot;allowed&quot;,
        fastPath: &quot;allowlist&quot;,
    });
    return { behavior: &quot;allow&quot;, updatedInput: q };
}
</code></pre>
<p>白名单包含所有只读工具（line 543825-543851）：</p>
<ul>
<li><code>Read</code>、<code>Grep</code>、<code>Glob</code>、<code>LSP</code>、<code>WebSearch</code> 等</li>
<li><code>ReadMcpResourceTool</code></li>
<li>各种查询/搜索类工具</li>
</ul>
<p>这些工具不修改任何状态，因此无条件放行。</p>
<h4>第四层：AI 分类器</h4>
<p>当以上三层都无法决定时，才调用 AI 分类器：</p>
<pre><code class="language-js">// line 544376-544386
let $ = gZ4(A.name, q);    // 格式化待分类的 action 描述
let H = await kG8(          // 调用 AI 分类器
    K.messages,              // 对话历史
    $,                       // 当前 action 描述
    K.options.tools,         // 可用工具列表
    w.toolPermissionContext,  // 权限上下文
    K.abortController.signal, // 中止信号
);
</code></pre>
<h3>8.3 AI 分类器内部实现 (<code>kG8</code>)</h3>
<h4>输入构建</h4>
<p>分类器的输入由三部分组成：</p>
<p><strong>1) System Prompt（系统提示词）</strong>：一个完整的安全评估指南（<code>re9</code> 函数，line 319693），包含：</p>
<pre><code class="language-js">// line 319344-319348 — 角色定义
&quot;You are a security monitor for autonomous AI coding agents.&quot;

// 威胁模型
&quot;You are protecting against three main risks:
- Prompt injection: 提示词注入
- Scope creep: 范围蔓延
- Accidental damage: 意外损害&quot;
</code></pre>
<p><strong>2) 对话转录（transcript）</strong>：通过 <code>hZ4</code> 函数（line 319574）将对话历史压缩为紧凑格式：</p>
<pre><code class="language-js">// line 319574-319616
function hZ4(A, q = !1) {
    let K = [];
    for (let _ of A)
        if (_.type === &quot;user&quot;) {
            // 提取用户消息文本和图片
            K.push({ role: &quot;user&quot;, content: z });
        } else if (_.type === &quot;assistant&quot;) {
            // 只保留 tool_use 块（名称和输入参数）
            for (let z of _.message.content)
                if (z.type === &quot;tool_use&quot;)
                    Y.push({ type: &quot;tool_use&quot;, name: z.name, input: z.input });
        }
    return K;
}
</code></pre>
<p>每条记录被格式化为简洁的文本行（line 319626-319664）：</p>
<ul>
<li>用户消息 → <code>User: &lt;文本&gt;</code></li>
<li>工具调用 → <code>&lt;工具名&gt; &lt;JSON参数&gt;</code></li>
<li>工具结果 → <code>[tool output] &lt;结果&gt;</code></li>
</ul>
<p><strong>3) CLAUDE.md 上下文</strong>：将用户的 CLAUDE.md 配置作为用户意图的一部分注入（line 319675-319691）：</p>
<pre><code class="language-js">function ne9() {
    let A = Ng8();  // 获取合并后的 CLAUDE.md
    if (A === null) return null;
    return {
        role: &quot;user&quot;,
        content: [{
            type: &quot;text&quot;,
            text: `The following is the user&#39;s CLAUDE.md configuration...
&lt;user_claude_md&gt;
${A}
&lt;/user_claude_md&gt;`,
            cache_control: zp({ querySource: &quot;auto_mode&quot; }),
        }],
    };
}
</code></pre>
<h4>分类器模型选择</h4>
<pre><code class="language-js">// line 320277-320282
function NZ4() {
    let A = l8(&quot;tengu_auto_mode_config&quot;, {});
    if (A?.model) return A.model;       // 服务端可配置
    let q = KK();                        // 获取当前主模型
    if (jG(q)) return K6_;               // 某些模型用 &quot;claude-sonnet-4-6[1m]&quot;
    return q6_;                           // 默认 &quot;claude-sonnet-4-6&quot;
}
</code></pre>
<p>分类器<strong>始终使用 Sonnet</strong>，不使用 Opus，这是成本和延迟的平衡。</p>
<h4>API 调用参数</h4>
<pre><code class="language-js">// line 320128-320147
let b = {
    model: E,                    // Sonnet 模型
    max_tokens: 4096 + S,
    system: [{ type: &quot;text&quot;, text: $, cache_control: ... }],  // 系统提示词
    skipSystemPromptPrefix: !0,
    temperature: 0,              // 温度为 0，确保确定性输出
    thinking: R,                 // 是否启用 thinking（默认关闭）
    messages: [...J, { role: &quot;user&quot;, content: W }],
    tools: [ie9],                // 唯一工具：classify_result
    tool_choice: { type: &quot;tool&quot;, name: bd6 },  // 强制调用 classify_result
    maxRetries: Pd6(),
    signal: Y,                   // 支持用户中止
    querySource: &quot;auto_mode&quot;,
};
</code></pre>
<p>分类器被强制使用唯一工具 <code>classify_result</code>，输出结构化结果：</p>
<pre><code class="language-js">// line 320349-320373 — classify_result 工具定义
{
    name: &quot;classify_result&quot;,
    input_schema: {
        properties: {
            thinking: { type: &quot;string&quot;, description: &quot;Brief step-by-step reasoning.&quot; },
            shouldBlock: { type: &quot;boolean&quot;, description: &quot;Whether the action should be blocked&quot; },
            reason: { type: &quot;string&quot;, description: &quot;Brief explanation&quot; },
        },
        required: [&quot;thinking&quot;, &quot;shouldBlock&quot;, &quot;reason&quot;],
    },
}
</code></pre>
<h3>8.4 两阶段分类器（Two-Stage Classifier）</h3>
<p>当服务端配置 <code>twoStageClassifier</code> 时（line 320287），启用两阶段流水线（<code>A6_</code> 函数，line 319798）：</p>
<pre><code>Stage 1 (快速判断):
  ├─ 使用 XML 输出格式（非工具调用）
  ├─ max_tokens 较小（256）
  ├─ 提示词：&quot;Err on the side of blocking. &lt;block&gt; immediately.&quot;
  ├─ 如果结果是 &lt;block&gt;no&lt;/block&gt; → 直接放行，不进入 Stage 2
  └─ 如果结果是 &lt;block&gt;yes&lt;/block&gt; 或无法解析 → 进入 Stage 2

Stage 2 (深度分析):
  ├─ max_tokens 较大（4096）
  ├─ 提示词：&quot;Review the classification process carefully... Use &lt;thinking&gt; before responding&quot;
  └─ 做出最终决定
</code></pre>
<pre><code class="language-js">// line 319830-319909 — Stage 1
if ($ !== &quot;thinking&quot;) {
    let t = [...v, { type: &quot;text&quot;, text: ae9 }];
    // ae9 = &quot;Err on the side of blocking. &lt;block&gt; immediately.&quot;
    W = {
        model: Y,
        max_tokens: ($ === &quot;fast&quot; ? 256 : 64) + G,
        // ...
        ...($ !== &quot;fast&quot; &amp;&amp; { stop_sequences: [&quot;&lt;/block&gt;&quot;] }),
    };
    let e = await Tk(W);
    let q6 = GZ4(s);       // 解析 &lt;block&gt;yes/no&lt;/block&gt;
    if (q6 === !1)          // Stage 1 判断为 no（安全）
        return { shouldBlock: !1, reason: &quot;Allowed by fast classifier&quot;, stage: &quot;fast&quot; };
    // Stage 1 判断为 yes 或无法解析 → 继续 Stage 2
}

// line 319911-319983 — Stage 2
let N = [...v, { type: &quot;text&quot;, text: oe9 }];
// oe9 = &quot;Review the classification process carefully... Use &lt;thinking&gt;...&quot;
let E = {
    model: Y,
    max_tokens: 4096 + G,
    // ...
};
let R = await Tk(E);
</code></pre>
<p><strong>两阶段设计的目的</strong>：Stage 1 用低开销快速放行明显安全的操作，只有可疑操作才进入 Stage 2 深度分析，降低平均延迟和成本。</p>
<h3>8.5 分类器的安全规则体系</h3>
<h4>BLOCK 规则（line 319443-319472）</h4>
<p>主要拦截类别：</p>
<table>
<thead>
<tr>
<th>类别</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>Git Destructive</td>
<td>force push、删除远程分支</td>
</tr>
<tr>
<td>Git Push to Default Branch</td>
<td>直推 main/master</td>
</tr>
<tr>
<td>Code from External</td>
<td>下载并执行外部代码（curl | bash）</td>
</tr>
<tr>
<td>Production Deploy</td>
<td>部署到生产环境</td>
</tr>
<tr>
<td>Data Exfiltration</td>
<td>向外部发送敏感数据</td>
</tr>
<tr>
<td>Self-Modification</td>
<td>修改 agent 自身配置/权限文件</td>
</tr>
<tr>
<td>Permission Grant</td>
<td>授予管理员权限</td>
</tr>
<tr>
<td>Irreversible Local Destruction</td>
<td>不可逆的本地文件删除</td>
</tr>
<tr>
<td>External System Writes</td>
<td>向外部系统（Jira/GitHub Issues）写入</td>
</tr>
<tr>
<td>Cloud Storage Mass Delete</td>
<td>云存储批量删除</td>
</tr>
<tr>
<td>Remote Shell Writes</td>
<td>通过远程 shell 写入生产主机</td>
</tr>
<tr>
<td>Blind Apply</td>
<td>跳过预览直接执行破坏性操作</td>
</tr>
<tr>
<td>Logging/Audit Tampering</td>
<td>篡改日志/审计追踪</td>
</tr>
<tr>
<td>TLS/Auth Weaken</td>
<td>禁用 TLS 验证</td>
</tr>
<tr>
<td>Security Weaken</td>
<td>削弱安全机制</td>
</tr>
<tr>
<td>Create Unsafe Agents</td>
<td>创建无审批的自主 agent 循环</td>
</tr>
<tr>
<td>Interfere With Others</td>
<td>干扰他人作业</td>
</tr>
<tr>
<td>Modify Shared Resources</td>
<td>修改共享资源</td>
</tr>
<tr>
<td>Create RCE Surface</td>
<td>创建远程代码执行表面</td>
</tr>
<tr>
<td>Expose Local Services</td>
<td>暴露本地服务到网络</td>
</tr>
<tr>
<td>Credential Leakage</td>
<td>凭证泄露</td>
</tr>
<tr>
<td>Credential Exploration</td>
<td>系统性扫描凭证存储</td>
</tr>
<tr>
<td>Exfil Scouting</td>
<td>测试外部端点可达性（数据泄露侦察）</td>
</tr>
<tr>
<td>Untrusted Code Integration</td>
<td>集成不受信任的外部代码</td>
</tr>
<tr>
<td>Unauthorized Persistence</td>
<td>未授权的持久化访问</td>
</tr>
<tr>
<td>Content Integrity / Impersonation</td>
<td>内容伪造/冒充</td>
</tr>
<tr>
<td>Real-World Transactions</td>
<td>真实世界交易（购买、付款等）</td>
</tr>
</tbody></table>
<h4>ALLOW 例外（line 319474-319484）</h4>
<table>
<thead>
<tr>
<th>例外</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>Test Artifacts</td>
<td>测试用的硬编码密钥</td>
</tr>
<tr>
<td>Local Operations</td>
<td>工作目录内的本地文件操作</td>
</tr>
<tr>
<td>Read-Only Operations</td>
<td>不修改状态的 GET 请求</td>
</tr>
<tr>
<td>Declared Dependencies</td>
<td>安装已声明在 manifest 中的依赖</td>
</tr>
<tr>
<td>Toolchain Bootstrap</td>
<td>安装官方语言工具链</td>
</tr>
<tr>
<td>Standard Credentials</td>
<td>读取配置中的凭证并发送到其目标提供商</td>
</tr>
<tr>
<td>Git Push to Working Branch</td>
<td>推送到当前工作分支</td>
</tr>
</tbody></table>
<h4>用户自定义规则</h4>
<p>用户可以在 settings 的 <code>autoMode</code> 字段中自定义 <code>allow</code>、<code>soft_deny</code>、<code>environment</code> 三个维度的规则。<code>re9</code> 函数（line 319693）会将这些自定义规则替换进系统提示词的模板占位符中：</p>
<pre><code class="language-js">// line 319718-319729
return Y
    .replace(/&lt;user_allow_rules_to_replace&gt;.../, (M, X) =&gt; H ?? X)   // 用户 allow 规则
    .replace(/&lt;user_deny_rules_to_replace&gt;.../, (M, X) =&gt; j ?? X)    // 用户 deny 规则
    .replace(/&lt;user_environment_to_replace&gt;.../, (M, X) =&gt; J ?? X);  // 用户环境描述
</code></pre>
<h3>8.6 拒绝追踪与熔断机制</h3>
<p>系统追踪连续拒绝和总拒绝次数：</p>
<pre><code class="language-js">// line 453781-453802
function sR8() { return { consecutiveDenials: 0, totalDenials: 0 }; }
function rYq(A) {  // 拒绝计数 +1
    return { ...A, consecutiveDenials: A.consecutiveDenials + 1, totalDenials: A.totalDenials + 1 };
}
function ar6(A) {  // 允许时重置连续拒绝
    if (A.consecutiveDenials === 0) return A;
    return { ...A, consecutiveDenials: 0 };
}
// 阈值配置
var oR8 = { maxConsecutive: 3, maxTotal: 20 };
</code></pre>
<p>当超过阈值时（line 544044-544083）：</p>
<pre><code class="language-js">function rHY(A, q, K, _, Y, z, w) {
    if (!aYq(A)) return null;  // 未超过阈值，返回 null
    // 超过阈值:
    // - headless 模式 → 直接抛出异常终止 agent
    // - CLI 模式 → 降级为手动确认（弹出权限提示框）
    if ($)  // shouldAvoidPermissionPrompts
        throw new f_(&quot;Agent aborted: too many classifier denials in headless mode&quot;);
    // 否则降级为手动审批
}
</code></pre>
<ul>
<li><strong>连续拒绝 &gt;= 3 次</strong> 或 <strong>总拒绝 &gt;= 20 次</strong> → 降级为手动权限提示</li>
<li>Headless 模式下直接中止 agent</li>
</ul>
<h3>8.7 分类器不可用时的策略</h3>
<pre><code class="language-js">// line 544467-544490
if (H.unavailable) {
    if (cV(&quot;tengu_iron_gate_closed&quot;, !0, lHY))  // feature flag
        // Fail-closed：拒绝操作并给出重试建议
        return { behavior: &quot;deny&quot;, message: $yq(A.name, H.model) };
    // Fail-open：降级为正常权限提示
    return z;  // 返回原始的 &quot;ask&quot; 行为
}
</code></pre>
<p>由 <code>tengu_iron_gate_closed</code> feature flag 控制：</p>
<ul>
<li><strong>Fail-closed</strong>：分类器不可用时直接拒绝（更安全）</li>
<li><strong>Fail-open</strong>：分类器不可用时降级为手动确认（用户体验更好）</li>
</ul>
<hr>
<h2>9. 系统提示词注入详解</h2>
<h3>9.1 附件系统（Attachment System）</h3>
<p>Auto mode 的系统提示词通过 <strong>附件（attachment）</strong> 机制注入对话。附件系统是 Claude Code 在每轮对话前动态组装的上下文块。</p>
<p>附件注册在 line 467884-467885：</p>
<pre><code class="language-js">GY(&quot;auto_mode&quot;, () =&gt; xi_(Y, q)),       // auto mode 进入时的附件
GY(&quot;auto_mode_exit&quot;, () =&gt; ui_(q)),       // auto mode 退出时的附件
</code></pre>
<h3>9.2 附件生成逻辑（<code>xi_</code> 函数）</h3>
<pre><code class="language-js">// line 468110-468124
async function xi_(A, q) {
    // 只在 auto 模式下才注入
    if (q.getAppState().toolPermissionContext.mode !== &quot;auto&quot;) return [];

    if (A &amp;&amp; A.length &gt; 0) {
        let { turnCount: w, foundAutoModeAttachment: O } = Ii_(A);
        // 如果最近 5 轮内已经注入过，跳过（避免冗余）
        if (O &amp;&amp; w &lt; u2q.TURNS_BETWEEN_ATTACHMENTS) return [];
    }

    return [{
        type: &quot;auto_mode&quot;,
        reminderType:
            // 每 5 次附件中，第 1 次是完整版，其余是精简版
            (bi_(A ?? []) + 1) % u2q.FULL_REMINDER_EVERY_N_ATTACHMENTS === 1
                ? &quot;full&quot;
                : &quot;sparse&quot;,
    }];
}
</code></pre>
<p>关键常量：</p>
<pre><code class="language-js">// line 469116-469119
u2q = {
    TURNS_BETWEEN_ATTACHMENTS: 5,           // 每 5 轮对话注入一次
    FULL_REMINDER_EVERY_N_ATTACHMENTS: 5,   // 每 5 次注入中有 1 次是完整版
};
</code></pre>
<h3>9.3 附件频率控制</h3>
<p><code>Ii_</code> 函数（line 468080-468097）向后扫描对话历史，计算距离上次 auto_mode 附件有多少轮 assistant 回复：</p>
<pre><code class="language-js">function Ii_(A) {
    let q = 0, K = !1;
    for (let _ = A.length - 1; _ &gt;= 0; _--) {
        let Y = A[_];
        if (Y?.type === &quot;assistant&quot;) {
            if (Ra6(Y)) continue;  // 跳过空回复
            q++;                    // 计数 assistant 轮次
        } else if (Y?.type === &quot;attachment&quot; &amp;&amp; Y.attachment.type === &quot;auto_mode&quot;) {
            K = !0;                 // 找到了上次的 auto_mode 附件
            break;
        } else if (Y?.type === &quot;attachment&quot; &amp;&amp; Y.attachment.type === &quot;auto_mode_exit&quot;)
            break;                  // 已经退出 auto mode
    }
    return { turnCount: q, foundAutoModeAttachment: K };
}
</code></pre>
<p><code>bi_</code> 函数（line 468099-468108）计算整个会话中 auto_mode 附件的总数，用于决定使用完整版还是精简版。</p>
<h3>9.4 完整版提示词（Full Reminder）</h3>
<p>当 <code>reminderType === &quot;full&quot;</code> 时，注入完整版（<code>pjY</code> 函数，line 547040-547056）：</p>
<pre><code class="language-js">function pjY() {
    return z3([F8({
        content: `## Auto Mode Active

Auto mode is active. The user chose continuous, autonomous execution. You should:

1. **Execute immediately** — Start implementing right away. Make reasonable
   assumptions and proceed.
2. **Minimize interruptions** — Prefer making reasonable assumptions over asking
   questions. Use AskUserQuestion only when the task genuinely cannot proceed
   without user input.
3. **Prefer action over planning** — Do not enter plan mode unless the user
   explicitly asks.
4. **Make reasonable decisions** — Choose the most sensible approach and keep
   moving. Don&#39;t block on ambiguity.
5. **Be thorough** — Complete the full task including tests, linting, and
   verification without stopping to ask.
6. **Never post to public services** — Do not share content to public endpoints
   (GitHub gists, Mermaid Live, Pastebin, etc.) without explicit written approval.`,
        isMeta: !0,
    })]);
}
</code></pre>
<p>这六条指令从行为层面塑造了 auto mode 下 Claude 的执行策略。</p>
<h3>9.5 精简版提示词（Sparse Reminder）</h3>
<p>当 <code>reminderType === &quot;sparse&quot;</code> 时，注入精简版（<code>FjY</code> 函数，line 547057-547065）：</p>
<pre><code class="language-js">function FjY() {
    return z3([F8({
        content: &quot;Auto mode still active (see full instructions earlier in conversation). &quot;
               + &quot;Execute autonomously, minimize interruptions, prefer action over planning.&quot;,
        isMeta: !0,
    })]);
}
</code></pre>
<p>精简版只有一行，引导模型回顾之前的完整指令，节省 token。</p>
<h3>9.6 退出 Auto Mode 的提示词</h3>
<p>当退出 auto mode 时，<code>ui_</code> 函数生成退出附件（line 468126-468131）：</p>
<pre><code class="language-js">async function ui_(A) {
    if (!Ig8()) return [];                                    // needsAutoModeExitAttachment
    if (A.getAppState().toolPermissionContext.mode === &quot;auto&quot;)
        return (_C(!1), []);                                   // 还在 auto mode，不注入退出
    return (_C(!1), [{ type: &quot;auto_mode_exit&quot; }]);
}
</code></pre>
<p>退出附件的内容（line 547354-547362）：</p>
<pre><code class="language-js">case &quot;auto_mode_exit&quot;:
    return z3([F8({
        content: `## Exited Auto Mode

You have exited auto mode. The user may now want to interact more directly.
You should ask clarifying questions when the approach is ambiguous rather
than making assumptions.`,
        isMeta: !0,
    })]);
</code></pre>
<p>明确告诉模型：现在不再是自主模式了，遇到模糊情况应该<strong>问用户</strong>而不是自己假设。</p>
<h3>9.7 附件渲染为消息</h3>
<p>附件最终通过 <code>sl1</code> 中的 <code>case &quot;auto_mode&quot;</code> 分支（line 547352）被渲染，调用 <code>gjY</code> 函数根据 <code>reminderType</code> 选择完整版或精简版：</p>
<pre><code class="language-js">// line 547036-547038
function gjY(A) {
    if (A.reminderType === &quot;sparse&quot;) return FjY();  // 精简版
    return pjY();                                     // 完整版
}
</code></pre>
<h3>9.8 整体注入时序</h3>
<pre><code>对话开始，auto mode 激活
    │
    ├─ Turn 1:  [auto_mode attachment, full]     &lt;- 第 1 次，完整版
    ├─ Turn 2-5: (无附件，间隔期)
    ├─ Turn 6:  [auto_mode attachment, sparse]    &lt;- 第 2 次，精简版
    ├─ Turn 7-10: (无附件)
    ├─ Turn 11: [auto_mode attachment, sparse]    &lt;- 第 3 次，精简版
    ├─ Turn 12-15: (无附件)
    ├─ Turn 16: [auto_mode attachment, sparse]    &lt;- 第 4 次，精简版
    ├─ Turn 17-20: (无附件)
    ├─ Turn 21: [auto_mode attachment, full]      &lt;- 第 5 次 -&gt; 1%5==1，完整版
    │
    └─ 用户切换模式 -&gt; [auto_mode_exit attachment]
</code></pre>
<p>每隔 5 轮注入一次提示词，每 5 次注入周期中第 1 次是完整版（~800 字），其余 4 次是精简版（~1 行），在<strong>上下文窗口占用</strong>和<strong>行为稳定性</strong>之间取得平衡。</p>
]]></description>
    </item>
    <item>
      <title>Claude Code 藏了个开关，打开后 /init 完全不一样</title>
      <link>https://blog.sorrycc.com/claude-code-init-hidden-switch</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/claude-code-init-hidden-switch</guid>
      <pubDate>Mon, 23 Mar 2026 00:39:46 GMT</pubDate>
      <description><![CDATA[<h1><code>/init</code> Command Diff: <code>CLAUDE_CODE_NEW_INIT</code> ON vs OFF</h1>
<blockquote>
<p>Source: <code>cli.js</code> v2.1.81, lines 489004–489255</p>
</blockquote>
<h2>Feature Flag</h2>
<p>Environment variable <code>CLAUDE_CODE_NEW_INIT</code> is checked by <code>a6()</code> (line 2930), which returns <code>true</code> for <code>&quot;1&quot;</code>, <code>&quot;true&quot;</code>, <code>&quot;yes&quot;</code>, <code>&quot;on&quot;</code> (case-insensitive). Unset or any other value → <code>false</code>.</p>
<p>Two touch points in the <code>/init</code> command (lines 489227–489255):</p>
<ol>
<li><strong>Description</strong> (line 489231): changes the user-facing description string</li>
<li><strong>Prompt</strong> (line 489249): selects between <code>M6Y</code> (new) and <code>J6Y</code> (old) prompt</li>
</ol>
<hr>
<h2>Command Description</h2>
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>OFF</td>
<td><code>&quot;Initialize a new CLAUDE.md file with codebase documentation&quot;</code></td>
</tr>
<tr>
<td>ON</td>
<td><code>&quot;Initialize new CLAUDE.md file(s) and optional skills/hooks with codebase documentation&quot;</code></td>
</tr>
</tbody></table>
<hr>
<h2>Diff Summary</h2>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>OFF (old <code>J6Y</code>)</th>
<th>ON (new <code>M6Y</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>User interaction</td>
<td>None — fire and forget</td>
<td>Heavy — multiple <code>AskUserQuestion</code> rounds with previews</td>
</tr>
<tr>
<td>Output artifacts</td>
<td><code>CLAUDE.md</code> only</td>
<td><code>CLAUDE.md</code> + <code>CLAUDE.local.md</code> + <code>.claude/skills/</code> + hooks in <code>settings.json</code></td>
</tr>
<tr>
<td>Phases</td>
<td>1 (implicit)</td>
<td>8 (explicit)</td>
</tr>
<tr>
<td>Personalization</td>
<td>None</td>
<td>Role, familiarity, sandbox URLs, communication prefs</td>
</tr>
<tr>
<td>Skills</td>
<td>Not supported</td>
<td>Full skill creation with YAML frontmatter</td>
</tr>
<tr>
<td>Hooks</td>
<td>Not supported</td>
<td>PostToolUse/PreToolUse/Stop hooks with validation flow</td>
</tr>
<tr>
<td>Git worktree awareness</td>
<td>No</td>
<td>Yes — detects and handles nested vs sibling worktrees</td>
</tr>
<tr>
<td>Monorepo support</td>
<td>Minimal</td>
<td><code>.claude/rules/</code> + subdirectory CLAUDE.md suggestions</td>
</tr>
<tr>
<td>Proposal flow</td>
<td>No</td>
<td>Preview panel with accept/drop options</td>
</tr>
<tr>
<td>Plugin recommendations</td>
<td>No</td>
<td>frontend-design, playwright, skill-creator plugins</td>
</tr>
<tr>
<td>Existing file handling</td>
<td>&quot;suggest improvements&quot;</td>
<td>Explicit &quot;propose diffs, explain why, don&#39;t silently overwrite&quot;</td>
</tr>
<tr>
<td><code>@import</code> syntax</td>
<td>No</td>
<td>Yes — references to external docs to avoid bloating CLAUDE.md</td>
</tr>
</tbody></table>
<hr>
<h2>Original Prompts</h2>
<h3>OLD Prompt (<code>J6Y</code>, flag OFF)</h3>
<pre><code>Please analyze this codebase and create a CLAUDE.md file, which will be given to future instances of Claude Code to operate in this repository.

What to add:
1. Commands that will be commonly used, such as how to build, lint, and run tests. Include the necessary commands to develop in this codebase, such as how to run a single test.
2. High-level code architecture and structure so that future instances can be productive more quickly. Focus on the &quot;big picture&quot; architecture that requires reading multiple files to understand.

Usage notes:
- If there&#39;s already a CLAUDE.md, suggest improvements to it.
- When you make the initial CLAUDE.md, do not repeat yourself and do not include obvious instructions like &quot;Provide helpful error messages to users&quot;, &quot;Write unit tests for all new utilities&quot;, &quot;Never include sensitive information (API keys, tokens) in code or commits&quot;.
- Avoid listing every component or file structure that can be easily discovered.
- Don&#39;t include generic development practices.
- If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include the important parts.
- If there is a README.md, make sure to include the important parts.
- Do not make up information such as &quot;Common Development Tasks&quot;, &quot;Tips for Development&quot;, &quot;Support and Documentation&quot; unless this is expressly included in other files that you read.
- Be sure to prefix the file with the following text:
</code></pre>
<h1>CLAUDE.md</h1>
<p>This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.</p>
<pre><code>
</code></pre>
<h3>NEW Prompt (<code>M6Y</code>, flag ON)</h3>
<pre><code>Set up a minimal CLAUDE.md (and optionally skills and hooks) for this repo. CLAUDE.md is loaded into every Claude Code session, so it must be concise — only include what Claude would get wrong without it.

## Phase 1: Ask what to set up

Use AskUserQuestion to find out what the user wants:

- &quot;Which CLAUDE.md files should /init set up?&quot;
  Options: &quot;Project CLAUDE.md&quot; | &quot;Personal CLAUDE.local.md&quot; | &quot;Both project + personal&quot;
  Description for project: &quot;Team-shared instructions checked into source control — architecture, coding standards, common workflows.&quot;
  Description for personal: &quot;Your private preferences for this project (gitignored, not shared) — your role, sandbox URLs, preferred test data, workflow quirks.&quot;

- &quot;Also set up skills and hooks?&quot;
  Options: &quot;Skills + hooks&quot; | &quot;Skills only&quot; | &quot;Hooks only&quot; | &quot;Neither, just CLAUDE.md&quot;
  Description for skills: &quot;On-demand capabilities you or Claude invoke with `/skill-name` — good for repeatable workflows and reference knowledge.&quot;
  Description for hooks: &quot;Deterministic shell commands that run on tool events (e.g., format after every edit). Claude can&#39;t skip them.&quot;

## Phase 2: Explore the codebase

Launch a subagent to survey the codebase, and ask it to read key files to understand the project: manifest files (package.json, Cargo.toml, pyproject.toml, go.mod, pom.xml, etc.), README, Makefile/build configs, CI config, existing CLAUDE.md, .claude/rules/, AGENTS.md, .cursor/rules or .cursorrules, .github/copilot-instructions.md, .windsurfrules, .clinerules, .mcp.json.

Detect:
- Build, test, and lint commands (especially non-standard ones)
- Languages, frameworks, and package manager
- Project structure (monorepo with workspaces, multi-module, or single project)
- Code style rules that differ from language defaults
- Non-obvious gotchas, required env vars, or workflow quirks
- Existing .claude/skills/ and .claude/rules/ directories
- Formatter configuration (prettier, biome, ruff, black, gofmt, rustfmt, or a unified format script like `npm run format` / `make fmt`)
- Git worktree usage: run `git worktree list` to check if this repo has multiple worktrees (only relevant if the user wants a personal CLAUDE.local.md)

Note what you could NOT figure out from code alone — these become interview questions.

## Phase 3: Fill in the gaps

Use AskUserQuestion to gather what you still need to write good CLAUDE.md files and skills. Ask only things the code can&#39;t answer.

If the user chose project CLAUDE.md or both: ask about codebase practices — non-obvious commands, gotchas, branch/PR conventions, required env setup, testing quirks. Skip things already in README or obvious from manifest files. Do not mark any options as &quot;recommended&quot; — this is about how their team works, not best practices.

If the user chose personal CLAUDE.local.md or both: ask about them, not the codebase. Do not mark any options as &quot;recommended&quot; — this is about their personal preferences, not best practices. Examples of questions:
  - What&#39;s their role on the team? (e.g., &quot;backend engineer&quot;, &quot;data scientist&quot;, &quot;new hire onboarding&quot;)
  - How familiar are they with this codebase and its languages/frameworks? (so Claude can calibrate explanation depth)
  - Do they have personal sandbox URLs, test accounts, API key paths, or local setup details Claude should know?
  - Only if Phase 2 found multiple git worktrees: ask whether their worktrees are nested inside the main repo (e.g., `.claude/worktrees/&lt;name&gt;/`) or siblings/external (e.g., `../myrepo-feature/`). If nested, the upward file walk finds the main repo&#39;s CLAUDE.local.md automatically — no special handling needed. If sibling/external, the personal content should live in a home-directory file (e.g., `~/.claude/&lt;project-name&gt;-instructions.md`) and each worktree gets a one-line CLAUDE.local.md stub that imports it: `@~/.claude/&lt;project-name&gt;-instructions.md`. Never put this import in the project CLAUDE.md — that would check a personal reference into the team-shared file.
  - Any communication preferences? (e.g., &quot;be terse&quot;, &quot;always explain tradeoffs&quot;, &quot;don&#39;t summarize at the end&quot;)

**Synthesize a proposal from Phase 2 findings** — e.g., format-on-edit if a formatter exists, a `/verify` skill if tests exist, a CLAUDE.md note for anything from the gap-fill answers that&#39;s a guideline rather than a workflow. For each, pick the artifact type that fits, **constrained by the Phase 1 skills+hooks choice**:

  - **Hook** (stricter) — deterministic shell command on a tool event; Claude can&#39;t skip it. Fits mechanical, fast, per-edit steps: formatting, linting, running a quick test on the changed file.
  - **Skill** (on-demand) — you or Claude invoke `/skill-name` when you want it. Fits workflows that don&#39;t belong on every edit: deep verification, session reports, deploys.
  - **CLAUDE.md note** (looser) — influences Claude&#39;s behavior but not enforced. Fits communication/thinking preferences: &quot;plan before coding&quot;, &quot;be terse&quot;, &quot;explain tradeoffs&quot;.

  **Respect Phase 1&#39;s skills+hooks choice as a hard filter**: if the user picked &quot;Skills only&quot;, downgrade any hook you&#39;d suggest to a skill or a CLAUDE.md note. If &quot;Hooks only&quot;, downgrade skills to hooks (where mechanically possible) or notes. If &quot;Neither&quot;, everything becomes a CLAUDE.md note. Never propose an artifact type the user didn&#39;t opt into.

**Show the proposal via AskUserQuestion&#39;s `preview` field, not as a separate text message** — the dialog overlays your output, so preceding text is hidden. The `preview` field renders markdown in a side-panel (like plan mode); the `question` field is plain-text-only. Structure it as:

  - `question`: short and plain, e.g. &quot;Does this proposal look right?&quot;
  - Each option gets a `preview` with the full proposal as markdown. The &quot;Looks good — proceed&quot; option&#39;s preview shows everything; per-item-drop options&#39; previews show what remains after that drop.
  - **Keep previews compact — the preview box truncates with no scrolling.** One line per item, no blank lines between items, no header. Example preview content:

    * **Format-on-edit hook** (automatic) — `ruff format &lt;file&gt;` via PostToolUse
    * **/verify skill** (on-demand) — `make lint &amp;&amp; make typecheck &amp;&amp; make test`
    * **CLAUDE.md note** (guideline) — &quot;run lint/typecheck/test before marking done&quot;

  - Option labels stay short (&quot;Looks good&quot;, &quot;Drop the hook&quot;, &quot;Drop the skill&quot;) — the tool auto-adds an &quot;Other&quot; free-text option, so don&#39;t add your own catch-all.

**Build the preference queue** from the accepted proposal. Each entry: {type: hook|skill|note, description, target file, any Phase-2-sourced details like the actual test/format command}. Phases 4-7 consume this queue.

## Phase 4: Write CLAUDE.md (if user chose project or both)

Write a minimal CLAUDE.md at the project root. Every line must pass this test: &quot;Would removing this cause Claude to make mistakes?&quot; If no, cut it.

**Consume `note` entries from the Phase 3 preference queue whose target is CLAUDE.md** (team-level notes) — add each as a concise line in the most relevant section. These are the behaviors the user wants Claude to follow but didn&#39;t need guaranteed (e.g., &quot;propose a plan before implementing&quot;, &quot;explain the tradeoffs when refactoring&quot;). Leave personal-targeted notes for Phase 5.

Include:
- Build/test/lint commands Claude can&#39;t guess (non-standard scripts, flags, or sequences)
- Code style rules that DIFFER from language defaults (e.g., &quot;prefer type over interface&quot;)
- Testing instructions and quirks (e.g., &quot;run single test with: pytest -k &#39;test_name&#39;&quot;)
- Repo etiquette (branch naming, PR conventions, commit style)
- Required env vars or setup steps
- Non-obvious gotchas or architectural decisions
- Important parts from existing AI coding tool configs if they exist (AGENTS.md, .cursor/rules, .cursorrules, .github/copilot-instructions.md, .windsurfrules, .clinerules)

Exclude:
- File-by-file structure or component lists (Claude can discover these by reading the codebase)
- Standard language conventions Claude already knows
- Generic advice (&quot;write clean code&quot;, &quot;handle errors&quot;)
- Detailed API docs or long references — use `@path/to/import` syntax instead (e.g., `@docs/api-reference.md`) to inline content on demand without bloating CLAUDE.md
- Information that changes frequently — reference the source with `@path/to/import` so Claude always reads the current version
- Long tutorials or walkthroughs (move to a separate file and reference with `@path/to/import`, or put in a skill)
- Commands obvious from manifest files (e.g., standard &quot;npm test&quot;, &quot;cargo test&quot;, &quot;pytest&quot;)

Be specific: &quot;Use 2-space indentation in TypeScript&quot; is better than &quot;Format code properly.&quot;

Do not repeat yourself and do not make up sections like &quot;Common Development Tasks&quot; or &quot;Tips for Development&quot; — only include information expressly found in files you read.

Prefix the file with:
</code></pre>
<h1>CLAUDE.md</h1>
<p>This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.</p>
<pre><code>
If CLAUDE.md already exists: read it, propose specific changes as diffs, and explain why each change improves it. Do not silently overwrite.

For projects with multiple concerns, suggest organizing instructions into `.claude/rules/` as separate focused files (e.g., `code-style.md`, `testing.md`, `security.md`). These are loaded automatically alongside CLAUDE.md and can be scoped to specific file paths using `paths` frontmatter.

For projects with distinct subdirectories (monorepos, multi-module projects, etc.): mention that subdirectory CLAUDE.md files can be added for module-specific instructions (they&#39;re loaded automatically when Claude works in those directories). Offer to create them if the user wants.

## Phase 5: Write CLAUDE.local.md (if user chose personal or both)

Write a minimal CLAUDE.local.md at the project root. This file is automatically loaded alongside CLAUDE.md. After creating it, add `CLAUDE.local.md` to the project&#39;s .gitignore so it stays private.

**Consume `note` entries from the Phase 3 preference queue whose target is CLAUDE.local.md** (personal-level notes) — add each as a concise line. If the user chose personal-only in Phase 1, this is the sole consumer of note entries.

Include:
- The user&#39;s role and familiarity with the codebase (so Claude can calibrate explanations)
- Personal sandbox URLs, test accounts, or local setup details
- Personal workflow or communication preferences

Keep it short — only include what would make Claude&#39;s responses noticeably better for this user.

If Phase 2 found multiple git worktrees and the user confirmed they use sibling/external worktrees (not nested inside the main repo): the upward file walk won&#39;t find a single CLAUDE.local.md from all worktrees. Write the actual personal content to `~/.claude/&lt;project-name&gt;-instructions.md` and make CLAUDE.local.md a one-line stub that imports it: `@~/.claude/&lt;project-name&gt;-instructions.md`. The user can copy this one-line stub to each sibling worktree. Never put this import in the project CLAUDE.md. If worktrees are nested inside the main repo (e.g., `.claude/worktrees/`), no special handling is needed — the main repo&#39;s CLAUDE.local.md is found automatically.

If CLAUDE.local.md already exists: read it, propose specific additions, and do not silently overwrite.

## Phase 6: Suggest and create skills (if user chose &quot;Skills + hooks&quot; or &quot;Skills only&quot;)

Skills add capabilities Claude can use on demand without bloating every session.

**First, consume `skill` entries from the Phase 3 preference queue.** Each queued skill preference becomes a SKILL.md tailored to what the user described. For each:
- Name it from the preference (e.g., &quot;verify-deep&quot;, &quot;session-report&quot;, &quot;deploy-sandbox&quot;)
- Write the body using the user&#39;s own words from the interview plus whatever Phase 2 found (test commands, report format, deploy target). If the preference maps to an existing bundled skill (e.g., `/verify`), write a project skill that adds the user&#39;s specific constraints on top — tell the user the bundled one still exists and theirs is additive.
- Ask a quick follow-up if the preference is underspecified (e.g., &quot;which test command should verify-deep run?&quot;)

**Then suggest additional skills** beyond the queue when you find:
- Reference knowledge for specific tasks (conventions, patterns, style guides for a subsystem)
- Repeatable workflows the user would want to trigger directly (deploy, fix an issue, release process, verify changes)

For each suggested skill, provide: name, one-line purpose, and why it fits this repo.

If `.claude/skills/` already exists with skills, review them first. Do not overwrite existing skills — only propose new ones that complement what is already there.

Create each skill at `.claude/skills/&lt;skill-name&gt;/SKILL.md`:

```yaml
---
name: &lt;skill-name&gt;
description: &lt;what the skill does and when to use it&gt;
---

&lt;Instructions for Claude&gt;
</code></pre>
<p>Both the user (<code>/&lt;skill-name&gt;</code>) and Claude can invoke skills by default. For workflows with side effects (e.g., <code>/deploy</code>, <code>/fix-issue 123</code>), add <code>disable-model-invocation: true</code> so only the user can trigger it, and use <code>$ARGUMENTS</code> to accept input.</p>
<h2>Phase 7: Suggest additional optimizations</h2>
<p>Tell the user you&#39;re going to suggest a few additional optimizations now that CLAUDE.md and skills (if chosen) are in place.</p>
<p>Check the environment and ask about each gap you find (use AskUserQuestion):</p>
<ul>
<li><p><strong>GitHub CLI</strong>: Run <code>which gh</code> (or <code>where gh</code> on Windows). If it&#39;s missing AND the project uses GitHub (check <code>git remote -v</code> for github.com), ask the user if they want to install it. Explain that the GitHub CLI lets Claude help with commits, pull requests, issues, and code review directly.</p>
</li>
<li><p><strong>Linting</strong>: If Phase 2 found no lint config (no .eslintrc, ruff.toml, .golangci.yml, etc. for the project&#39;s language), ask the user if they want Claude to set up linting for this codebase. Explain that linting catches issues early and gives Claude fast feedback on its own edits.</p>
</li>
<li><p><strong>Proposal-sourced hooks</strong> (if user chose &quot;Skills + hooks&quot; or &quot;Hooks only&quot;): Consume <code>hook</code> entries from the Phase 3 preference queue. If Phase 2 found a formatter and the queue has no formatting hook, offer format-on-edit as a fallback. If the user chose &quot;Neither&quot; or &quot;Skills only&quot; in Phase 1, skip this bullet entirely.</p>
<p>For each hook preference (from the queue or the formatter fallback):</p>
<ol>
<li><p>Target file: default based on the Phase 1 CLAUDE.md choice — project -&gt; <code>.claude/settings.json</code> (team-shared, committed); personal -&gt; <code>.claude/settings.local.json</code>. Only ask if the user chose &quot;both&quot; in Phase 1 or the preference is ambiguous. Ask once for all hooks, not per-hook.</p>
</li>
<li><p>Pick the event and matcher from the preference:</p>
<ul>
<li>&quot;after every edit&quot; -&gt; <code>PostToolUse</code> with matcher <code>Write|Edit</code></li>
<li>&quot;when Claude finishes&quot; / &quot;before I review&quot; -&gt; <code>Stop</code> event (fires at the end of every turn — including read-only ones)</li>
<li>&quot;before running bash&quot; -&gt; <code>PreToolUse</code> with matcher <code>Bash</code></li>
<li>&quot;before committing&quot; (literal git-commit gate) -&gt; <strong>not a hooks.json hook.</strong> Matchers can&#39;t filter Bash by command content, so there&#39;s no way to target only <code>git commit</code>. Route this to a git pre-commit hook (<code>.git/hooks/pre-commit</code>, husky, pre-commit framework) instead — offer to write one. If the user actually means &quot;before I review and commit Claude&#39;s output&quot;, that&#39;s <code>Stop</code> — probe to disambiguate.
Probe if the preference is ambiguous.</li>
</ul>
</li>
<li><p><strong>Load the hook reference</strong> (once per <code>/init</code> run, before the first hook): invoke the Skill tool with <code>skill: &#39;update-config&#39;</code> and args starting with <code>[hooks-only]</code> followed by a one-line summary of what you&#39;re building — e.g., <code>[hooks-only] Constructing a PostToolUse/Write|Edit format hook for .claude/settings.json using ruff</code>. This loads the hooks schema and verification flow into context. Subsequent hooks reuse it — don&#39;t re-invoke.</p>
</li>
<li><p>Follow the skill&#39;s <strong>&quot;Constructing a Hook&quot;</strong> flow: dedup check -&gt; construct for THIS project -&gt; pipe-test raw -&gt; wrap -&gt; write JSON -&gt; <code>jq -e</code> validate -&gt; live-proof (for <code>Pre|PostToolUse</code> on triggerable matchers) -&gt; cleanup -&gt; handoff. Target file and event/matcher come from steps 1-2 above.</p>
</li>
</ol>
</li>
</ul>
<p>Act on each &quot;yes&quot; before moving on.</p>
<h2>Phase 8: Summary and next steps</h2>
<p>Recap what was set up — which files were written and the key points included in each. Remind the user these files are a starting point: they should review and tweak them, and can run <code>/init</code> again anytime to re-scan.</p>
<p>Then tell the user that you&#39;ll be introducing a few more suggestions for optimizing their codebase and Claude Code setup based on what you found. Present these as a single, well-formatted to-do list where every item is relevant to this repo. Put the most impactful items first.</p>
<p>When building the list, work through these checks and include only what applies:</p>
<ul>
<li>If frontend code was detected (React, Vue, Svelte, etc.): <code>/plugin install frontend-design@claude-plugins-official</code> gives Claude design principles and component patterns so it produces polished UI; <code>/plugin install playwright@claude-plugins-official</code> lets Claude launch a real browser, screenshot what it built, and fix visual bugs itself.</li>
<li>If you found gaps in Phase 7 (missing GitHub CLI, missing linting) and the user said no: list them here with a one-line reason why each helps.</li>
<li>If tests are missing or sparse: suggest setting up a test framework so Claude can verify its own changes.</li>
<li>To help you create skills and optimize existing skills using evals, Claude Code has an official skill-creator plugin you can install. Install it with <code>/plugin install skill-creator@claude-plugins-official</code>, then run <code>/skill-creator &lt;skill-name&gt;</code> to create new skills or refine any existing skill. (Always include this one.)</li>
<li>Browse official plugins with <code>/plugin</code> — these bundle skills, agents, hooks, and MCP servers that you may find helpful. You can also create your own custom plugins to share them with others. (Always include this one.)</li>
</ul>
<pre><code>
</code></pre>
]]></description>
    </item>
    <item>
      <title>627 - 《Claude Code Tips》</title>
      <link>https://blog.sorrycc.com/claude-code-tips</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/claude-code-tips</guid>
      <pubDate>Fri, 20 Mar 2026 14:47:45 GMT</pubDate>
      <description><![CDATA[<blockquote>
<p>按需取用，不必全部记住。收集 + 过滤 + 添加自用的 Tips。</p>
</blockquote>
<h2>🚀 基础设置与快捷操作</h2>
<p><strong>自定义状态栏</strong></p>
<p>底部显示模型、目录、Git 分支、未提交文件数、Token 进度条和最后一条消息。支持 10 种颜色主题，脚本见 <a href="https://github.com/ykdojo/claude-code-tips/blob/main/scripts/context-bar.sh"><code>scripts/context-bar.sh</code></a>。</p>
<p><strong><code>/statusline</code> 两行布局提示词</strong></p>
<p>直接粘贴以下提示词即可配置两行状态栏：</p>
<pre><code>/statusline 分两行显示，第一行模型显示名称、ctx left、当前 git 分支。第二行：5 小时和 7 天rate_limit，用bar+百分比+重制时间，参考：
Sonnet 4.6 | ctx 10% | branch: main
5h [███░░] 63% ~4h8m | 7d [██░░░] 40% ~4d15h
</code></pre>
<p><strong><code>-c</code>、<code>-r</code> 和 <code>--resume</code></strong></p>
<ul>
<li><code>-c</code>：继续上次会话</li>
<li><code>-r</code>：交互式选择最近会话</li>
<li><code>--resume &lt;session-id&gt;</code>：精确恢复特定会话，session id 可在 <code>~/.claude/projects/</code> 目录下找到</li>
</ul>
<p><strong>输入框导航快捷键</strong></p>
<ul>
<li><code>Ctrl+A/E</code>：行首/行尾</li>
<li><code>Option+Left/Right</code>：按词跳转</li>
<li><code>Ctrl+W/U/K</code>：删词/删至行首/删至行尾</li>
<li><code>Ctrl+G</code>：在外部编辑器中打开输入框（适合粘贴长文本，比直接粘贴快）</li>
<li><code>Ctrl+V</code>（Mac）：粘贴剪贴板图片</li>
</ul>
<p><strong>设置 <code>cc</code> 别名</strong></p>
<p>在 <code>~/.zshrc</code> 中添加 <code>alias cc=&#39;claude --dangerously-skip-permissions&#39;</code>，跳过所有权限提示（需了解风险后使用）。</p>
<p><strong>Auto 模式（<code>--dangerously-skip-permissions</code> 的安全替代）</strong></p>
<p>权限请求由模型分类器自动判断是否安全，安全就直接放行，不用再盯着批准。你可以同时开多个 Claude 并行干活，一个在跑就切到下一个。CLI 里 Shift-Tab 进入 Auto 模式，Desktop 和 VSCode 在下拉菜单选择。Max、Teams、Enterprise 用户可用（目前灰度开启中，部分用户可能还看不到）。</p>
<p><strong><code>!</code> 前缀直接运行 Bash 命令</strong></p>
<p>输入 <code>!git status</code> 或 <code>!npm test</code> 立即执行，输出会进入上下文供 Claude 分析。</p>
<p><strong>Esc 停止，Esc+Esc 回滚</strong></p>
<ul>
<li><code>Esc</code>：立即停止 Claude，不丢失上下文</li>
<li><code>Esc+Esc</code>（或 <code>/rewind</code>）：打开检查点菜单，支持恢复代码、对话或两者</li>
<li>注意：Bash 命令执行（如数据库迁移）不在检查点范围内</li>
</ul>
<p><strong>Ctrl+S 暂存提示词草稿</strong></p>
<p>写到一半想先问个问题？Ctrl+S 暂存草稿，快速提问后自动恢复。</p>
<p><strong>Ctrl+B 后台运行长任务</strong></p>
<p>把测试、构建等耗时操作放到后台，Claude 继续工作，完成后通知你。</p>
<p><strong><code>/permissions</code> 白名单常用命令</strong></p>
<p>停止对 <code>npm run lint</code> 重复点击确认，白名单信任命令。</p>
<p><strong><code>/fewer-permission-prompts</code> 自动推荐白名单</strong></p>
<p>内置技能，扫描你的历史会话，找出那些安全但反复触发权限弹窗的 bash 和 MCP 命令，然后推荐一批命令加入你的权限白名单。比手动配 <code>/permissions</code> 更省心，适合不用 Auto 模式的人。</p>
<p><strong>设置输出风格</strong></p>
<p><code>/config</code> 选择 Explanatory（详细步骤）、Concise（简洁）或 Technical（技术精确）风格，或自定义。</p>
<p><strong>禁用 Co-authored-by 署名</strong></p>
<p>在 <code>~/.claude/settings.json</code> 中添加：</p>
<pre><code class="language-json">{
  &quot;attribution&quot;: {
    &quot;commit&quot;: &quot;&quot;,
    &quot;pr&quot;: &quot;&quot;
  }
}
</code></pre>
<p>默认情况下 Claude Code 会在 commit 和 PR 中添加 co-author 信息，设置为空字符串即可禁用。ref: <a href="https://code.claude.com/docs/en/settings#attribution-settings">https://code.claude.com/docs/en/settings#attribution-settings</a></p>
<p><strong><code>--model</code> 指定模型</strong></p>
<p><code>claude --model opus</code> 启动时指定，或会话中 <code>/model sonnet</code> 切换。重型任务用 Opus，简单改动用 Sonnet 省钱。</p>
<p><strong>关键环境变量</strong></p>
<ul>
<li><code>CLAUDE_MODEL</code>：设置默认模型</li>
<li><code>ANTHROPIC_API_KEY</code>：使用自己的 API Key</li>
<li><code>DISABLE_AUTO_COMPACT=1</code>：禁用自动压缩，配合 HANDOFF.md 手动管理上下文</li>
</ul>
<p><strong><code>--add-dir</code> 多目录工作</strong></p>
<pre><code class="language-bash">claude --add-dir ../backend --add-dir ../shared
</code></pre>
<p>同时操作多个仓库，前后端联调、monorepo 子包协作时很有用。</p>
<hr>
<h2>🧠 提示词与上下文管理</h2>
<p><strong>不相关任务之间用 <code>/clear</code></strong></p>
<p>清空上下文比在混乱的长会话中挣扎更有效。</p>
<p><strong><code>/btw</code> 快速侧问</strong></p>
<p>弹出悬浮层提问，不影响主上下文。实用场景：Claude 长时间没动静时，用 <code>/btw 是不是卡住了</code> 确认状态，不会打断当前任务。</p>
<p><strong><code>@</code> 直接引用文件</strong></p>
<p><code>@src/auth/middleware.ts</code> 精确指向文件，省去 Claude 自己搜索的代价。</p>
<p><strong>用模糊提示探索陌生代码</strong></p>
<p>&quot;这个文件你会改进什么？&quot; 让 Claude 主动发现问题，适合熟悉新代码库。</p>
<p><strong>HANDOFF.md 交接文档</strong></p>
<p>关闭自动压缩，手动让 Claude 写 HANDOFF.md（目标、已试什么、什么有效/无效、下一步），新会话只需提供文件路径即可继续。比 <code>/compact</code> 更可控。</p>
<p><strong>搜索对话历史</strong></p>
<p>对话存于 <code>~/.claude/projects/</code>，每个会话是 <code>.jsonl</code> 文件。可用 grep 搜索，或直接问 Claude &quot;帮我找今天关于 X 的对话&quot;。</p>
<p><strong>half-clone 减少上下文</strong></p>
<p><code>half-clone-conversation.sh</code> 只保留对话后半段，比 /branch（两条路径都存活）更适合&quot;续跑但减负&quot;场景。可配合 Hook 在上下文超 85% 时自动触发。ref: <a href="https://github.com/ykdojo/claude-code-tips/blob/main/skills/half-clone/SKILL.md">https://github.com/ykdojo/claude-code-tips/blob/main/skills/half-clone/SKILL.md</a></p>
<p><strong>Auto Memory 自动记忆</strong></p>
<p>Claude 自动维护 <code>~/.claude/projects/&lt;project&gt;/memory/MEMORY.md</code>，记录构建命令、调试洞察、项目约定等。比手写 HANDOFF.md 更自动，适合长期项目积累。</p>
<p><strong>拖拽/粘贴图片直接分析</strong></p>
<p>把截图拖进终端或 <code>Ctrl+V</code> 粘贴剪贴板图片，Claude 可以直接看图。适合分析 UI Bug、报错截图、设计稿还原等场景。</p>
<p><strong>Auto-compact 自动压缩</strong></p>
<p>上下文接近窗口限制时自动触发压缩。可以用 <code>/compact focus on the API changes</code> 指定保留重点，也可以设置 <code>DISABLE_AUTO_COMPACT=1</code> 完全关闭，改用 HANDOFF.md 手动管理。</p>
<hr>
<h2>📋 CLAUDE.md 配置文件</h2>
<p><strong>Claude 犯错后更新 CLAUDE.md</strong></p>
<p>说&quot;更新 CLAUDE.md 让这个问题不再发生&quot;，Claude 会自己写规则，下次会话自动遵守。</p>
<p><strong><code>@imports</code> 保持 CLAUDE.md 精简</strong></p>
<p>用 <code>@docs/git-instructions.md</code> 引用外部文档，Claude 按需读取，不占主文件空间。</p>
<p><strong>CLAUDE.md 是建议，Hooks 是要求</strong></p>
<p>CLAUDE.md 遵守率约 80%，Hooks 是 100% 确定性执行。格式化、lint、安全检查等必须执行的操作用 Hooks。</p>
<p><strong>自动化的自动化</strong></p>
<p>发现反复重复的操作就自动化它：CLAUDE.md 记录、skills 封装、脚本化——把元效率做到极致。</p>
<hr>
<h2>⚙️ Hooks 自动化</h2>
<p><strong>PostToolUse Hook 自动格式化</strong></p>
<p>每次 Claude 编辑文件后自动运行 Prettier（配置在 <code>.claude/settings.json</code>）。</p>
<p><strong>PreToolUse Hook 阻止危险命令</strong></p>
<p>在 Bash 工具前拦截 <code>rm -rf</code>、<code>drop table</code>、<code>truncate</code> 等破坏性命令。</p>
<p><strong>Notification Hook 桌面通知</strong></p>
<p>Claude 需要用户注意时触发（如等待权限确认、任务空闲），可配置发送系统通知或声音。</p>
<p><strong>SessionStart Hook 动态注入上下文</strong></p>
<p>每次会话启动时执行，可运行脚本自动注入最近提交、环境状态、当前任务等信息，跨压缩保留关键上下文。配置在 <code>.claude/settings.json</code> 的 <code>hooks.SessionStart</code> 下。</p>
<p><strong>UserPromptSubmit Hook 预处理提示词</strong></p>
<p>用户提交提示词前触发，可自动附加上下文、替换关键词或拦截敏感操作。</p>
<p><strong>Stop Hook 任务完成播放提示音</strong></p>
<pre><code class="language-json">&quot;command&quot;: &quot;/usr/bin/afplay /System/Library/Sounds/Glass.aiff&quot;
</code></pre>
<p>让 Claude 完成时发出声音，你可以去做别的事。</p>
<hr>
<h2>🤖 并行与多 Agent</h2>
<p><strong><code>/effort</code> 配置思考深度</strong></p>
<p>Opus 4.7 用自适应思考（adaptive thinking）替代了思考预算。用 <code>/effort</code> 调整：低 effort 更快更省 token，高 effort 更智能。可选 low/medium/high/xhigh/max，其中 max 仅当前会话生效，其他级别跨会话保持。日常推荐 xhigh，最难的任务用 max。此外，在提示词中加入 <code>ultrathink</code> 仍可将单轮 effort 设为 high，触发更深层推理。</p>
<p><strong>不确定方案时使用 Plan Mode</strong></p>
<p>多文件变更、陌生代码、架构决策用 Plan Mode，小任务直接执行。Shift+Tab 切换模式。</p>
<p><strong><code>--worktree</code> 并行独立分支</strong></p>
<p><code>claude --worktree feature-auth</code> 创建隔离工作区，同时运行 2-3 个 worktree 并行开发不同功能。</p>
<p><strong>子 Agent 保持主上下文干净</strong></p>
<p>&quot;用子 Agent 调查支付流程的错误处理&quot;——让子 Agent 深挖代码，只返回摘要，主会话不被消耗。</p>
<p><strong>Agent 团队</strong></p>
<p>需先设置环境变量 <code>CLAUDE_ENABLE_TEAMS=1</code> 开启此功能。创建 3-5 个 Agent 团队并行重构模块。避免多个 Agent 修改同一文件。</p>
<p><strong>一个 Claude 写，另一个 Claude 审</strong></p>
<p>实现会话写代码，审查会话以高级工程师视角 review，两者上下文独立，效果更好。</p>
<p><strong>Headless / SDK 模式</strong></p>
<pre><code class="language-bash">claude --print --output-format json -p &quot;分析这段代码的复杂度&quot;
</code></pre><p><a href="https://blog.sorrycc.com/claude-code-tips">Subscribe to read the full post.</a></p>]]></description>
    </item>
    <item>
      <title>Claude Code Channel 功能实现详解</title>
      <link>https://blog.sorrycc.com/claude-code-channel-implementation</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/claude-code-channel-implementation</guid>
      <pubDate>Fri, 20 Mar 2026 01:22:34 GMT</pubDate>
      <description><![CDATA[<h1>Claude Code Channel 功能实现详解</h1>
<blockquote>
<p>从 <code>claude-code@2.1.80/cli.js</code> 逆向提取。Channel 允许 MCP 服务器通过
<code>notifications/claude/channel</code> 协议向正在运行的 Claude Code 会话主动推送消息。</p>
</blockquote>
<hr>
<h2>概述</h2>
<p>Channel 是一个 <strong>MCP 推送通知机制</strong>——外部 MCP 服务器可以主动向正在运行的 Claude Code 会话注入消息，Claude 会像收到用户输入一样做出响应。</p>
<h3>核心流程</h3>
<pre><code>外部服务 (Telegram/Discord/Slack)
  -&gt; MCP Server 发送 notifications/claude/channel
    -&gt; Claude Code 收到通知
      -&gt; 包裹成 &lt;channel source=&quot;...&quot;&gt; XML
        -&gt; 作为 meta user turn 注入对话
          -&gt; Claude 响应
</code></pre>
<h3>实现要点</h3>
<h4>1. CLI 入口</h4>
<p>提供两个启动参数：</p>
<ul>
<li><code>--channels &lt;servers...&gt;</code> — 指定允许推送的 MCP 服务器列表（需要通过 allowlist 审核）</li>
<li><code>--dangerously-load-development-channels &lt;servers...&gt;</code> — 本地开发用，跳过 allowlist 检查，启动时弹确认框</li>
</ul>
<p>参数格式必须带前缀：</p>
<ul>
<li><code>plugin:&lt;name&gt;@&lt;marketplace&gt;</code> — 插件来源</li>
<li><code>server:&lt;name&gt;</code> — 手动配置的 MCP 服务器</li>
</ul>
<h4>2. 六层鉴权门控</h4>
<p>MCP 服务器连接后，依次检查：</p>
<table>
<thead>
<tr>
<th>顺序</th>
<th>检查项</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>capability</td>
<td>服务器必须声明 <code>experimental[&quot;claude/channel&quot;]</code></td>
</tr>
<tr>
<td>2</td>
<td>feature flag</td>
<td>服务端开关 <code>tengu_harbor</code> 必须为 <code>true</code></td>
</tr>
<tr>
<td>3</td>
<td>auth</td>
<td>必须有 claude.ai 登录态（accessToken）</td>
</tr>
<tr>
<td>4</td>
<td>policy</td>
<td>组织策略 <code>channelsEnabled</code> 必须为 <code>true</code></td>
</tr>
<tr>
<td>5</td>
<td>session</td>
<td>服务器必须在 <code>--channels</code> 列表中</td>
</tr>
<tr>
<td>6</td>
<td>allowlist</td>
<td>插件需在审批名单上；服务器需为 dev 模式</td>
</tr>
</tbody></table>
<p>全部通过 -&gt; 注册通知处理器；任一失败 -&gt; 跳过并显示警告。</p>
<h4>3. MCP 通知协议</h4>
<p>服务器发送的 JSON-RPC 通知格式：</p>
<pre><code class="language-json">{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;method&quot;: &quot;notifications/claude/channel&quot;,
  &quot;params&quot;: {
    &quot;content&quot;: &quot;用户 @alice 说：帮我查一下日志&quot;,
    &quot;meta&quot;: { &quot;sender&quot;: &quot;alice&quot;, &quot;platform&quot;: &quot;telegram&quot; }
  }
}
</code></pre>
<h4>4. 内容包裹</h4>
<p>收到通知后，内容被包裹成 XML 再注入：</p>
<pre><code class="language-xml">&lt;channel source=&quot;my-telegram-bot&quot; sender=&quot;alice&quot; platform=&quot;telegram&quot;&gt;
用户 @alice 说：帮我查一下日志
&lt;/channel&gt;
</code></pre>
<ul>
<li><code>meta</code> 中的合法 key（匹配 <code>/^[a-zA-Z_][a-zA-Z0-9_]*$/</code>）变成 XML 属性</li>
<li>值会做 XML 转义</li>
</ul>
<h4>5. 对话注入</h4>
<p>注入时的关键参数：</p>
<table>
<thead>
<tr>
<th>参数</th>
<th>值</th>
<th>作用</th>
</tr>
</thead>
<tbody><tr>
<td><code>mode</code></td>
<td><code>&quot;prompt&quot;</code></td>
<td>当作用户输入</td>
</tr>
<tr>
<td><code>isMeta</code></td>
<td><code>true</code></td>
<td>UI 上不显示为普通用户消息</td>
</tr>
<tr>
<td><code>priority</code></td>
<td><code>&quot;next&quot;</code></td>
<td>排在当前轮次之后立即处理</td>
</tr>
<tr>
<td><code>origin</code></td>
<td><code>{ kind: &quot;channel&quot;, server }</code></td>
<td>标记来源，用于后续特殊处理</td>
</tr>
<tr>
<td><code>skipSlashCommands</code></td>
<td><code>true</code></td>
<td>防止内容被解析为 <code>/命令</code></td>
</tr>
</tbody></table>
<h4>6. 特殊对话处理</h4>
<p>Channel 消息与普通 meta 消息不同：</p>
<ul>
<li><strong>会发送给 API</strong>：普通 meta 消息会被过滤掉，但 <code>origin.kind === &quot;channel&quot;</code> 的会保留</li>
<li><strong>计入计费轮次</strong>：作为有效的 billable turn</li>
</ul>
<h3>移植实现清单</h3>
<ol>
<li>加 CLI 参数解析（<code>--channels</code>、<code>--dangerously-load-development-channels</code>）</li>
<li>实现全局状态存储 <code>allowedChannels</code></li>
<li>实现鉴权门控函数（按需裁剪层数，至少保留 capability + session 两层）</li>
<li>在 MCP client 连接成功后注册 <code>notifications/claude/channel</code> 处理器</li>
<li>实现 <code>&lt;channel&gt;</code> XML 包裹逻辑</li>
<li>实现对话注入（作为 meta user turn，带 origin 标记）</li>
<li>修改对话过滤逻辑，确保 channel 消息发送给 LLM API</li>
<li>（可选）实现 allowlist/ledger 服务端审批机制</li>
<li>（可选）实现 UI 状态展示和警告通知</li>
</ol>
<hr>
<h2>详细代码参考</h2>
<h2>Table of Contents</h2>
<ol>
<li><a href="#1-cli-flags">CLI Flags</a></li>
<li><a href="#2-argument-parsing">Argument Parsing</a></li>
<li><a href="#3-global-state">Global State</a></li>
<li><a href="#4-feature-flag--allowlist-store">Feature Flag &amp; Allowlist Store</a></li>
<li><a href="#5-authorization-gate">Authorization Gate</a></li>
<li><a href="#6-notification-schema">Notification Schema</a></li>
<li><a href="#7-content-wrapping">Content Wrapping</a></li>
<li><a href="#8-server-lookup">Server Lookup</a></li>
<li><a href="#9-notification-handler-registration">Notification Handler Registration</a></li>
<li><a href="#10-skip-handler--ui-warnings">Skip Handler &amp; UI Warnings</a></li>
<li><a href="#11-conversation-injection">Conversation Injection</a></li>
<li><a href="#12-channel-status-summary">Channel Status Summary</a></li>
<li><a href="#13-entry-validation">Entry Validation</a></li>
<li><a href="#14-dev-channel-confirmation-dialog">Dev Channel Confirmation Dialog</a></li>
<li><a href="#15-flow-diagram">Flow Diagram</a></li>
</ol>
<hr>
<h2>1. CLI Flags</h2>
<p>Two hidden options register channel sources for a session.</p>
<pre><code>cli.js:614433-614442
</code></pre>
<pre><code class="language-js">q.addOption(
  new DK(
    &quot;--channels &lt;servers...&gt;&quot;,
    &quot;MCP servers whose channel notifications (inbound push) should register this session. Space-separated server names.&quot;,
  ).hideHelp(),
);
q.addOption(
  new DK(
    &quot;--dangerously-load-development-channels &lt;servers...&gt;&quot;,
    &quot;Load channel servers not on the approved allowlist. For local channel development only. Shows a confirmation dialog at startup.&quot;,
  ).hideHelp(),
);
</code></pre>
<p>Each entry must be prefixed:</p>
<ul>
<li><code>plugin:&lt;name&gt;@&lt;marketplace&gt;</code> -- plugin-provided channel (allowlist enforced)</li>
<li><code>server:&lt;name&gt;</code> -- manually configured MCP server</li>
</ul>
<hr>
<h2>2. Argument Parsing</h2>
<p>Parses CLI args into typed channel entries. Exits with error on invalid format.</p>
<pre><code>cli.js:613346-613384
</code></pre>
<pre><code class="language-js">let b8 = (aA, t4) =&gt; {
  let E5 = [],
    S4 = [];
  for (let q7 of aA)
    if (q7.startsWith(&quot;plugin:&quot;)) {
      let e4 = q7.slice(7),
        AK = e4.indexOf(&quot;@&quot;);
      if (AK &lt;= 0 || AK === e4.length - 1)
        S4.push(q7); // invalid format
      else
        E5.push({
          kind: &quot;plugin&quot;,
          name: e4.slice(0, AK),
          marketplace: e4.slice(AK + 1),
        });
    } else if (q7.startsWith(&quot;server:&quot;) &amp;&amp; q7.length &gt; 7)
      E5.push({ kind: &quot;server&quot;, name: q7.slice(7) });
    else
      S4.push(q7); // untagged entry

  if (S4.length &gt; 0) {
    process.stderr.write(
      red(
        `${t4} entries must be tagged: ${S4.join(&quot;, &quot;)}\n` +
        `  plugin:&lt;name&gt;@&lt;marketplace&gt;  -- plugin-provided channel (allowlist enforced)\n` +
        `  server:&lt;name&gt;                -- manually configured MCP server\n`,
      ),
    );
    process.exit(1);
  }
  return E5;
};

let eA = options.channels,
  bA = options.dangerouslyLoadDevelopmentChannels;

if (!headless) {
  if (bA &amp;&amp; bA.length &gt; 0)
    devChannels = b8(bA, &quot;--dangerously-load-development-channels&quot;);
  if (eA &amp;&amp; eA.length &gt; 0)
    setAllowedChannels(b8(eA, &quot;--channels&quot;));
    // D$6() -&gt; T8.allowedChannels = entries
}
</code></pre>
<h3>TypeScript types (reconstructed)</h3>
<pre><code class="language-ts">type ChannelEntry =
  | { kind: &quot;plugin&quot;; name: string; marketplace: string; dev?: boolean }
  | { kind: &quot;server&quot;; name: string; dev?: boolean };
</code></pre>
<hr>
<h2>3. Global State</h2>
<p>Channels use a global state object <code>T8</code> to store the allowed list and dev flag.</p>
<pre><code>cli.js:2757-2768
</code></pre>
<pre><code class="language-js">// Getter: returns the current allowed channels list
function Ju() {            // getAllowedChannels
  return T8.allowedChannels;
}

// Setter: stores parsed --channels entries
function D$6(A) {          // setAllowedChannels
  T8.allowedChannels = A;
}

// Getter: whether dev channels are loaded
function z68() {           // hasDevChannels
  return T8.hasDevChannels;
}

// Setter
function w68(A) {          // setHasDevChannels
  T8.hasDevChannels = A;
}
</code></pre>
<p>Initial state:</p>
<pre><code>cli.js:2069
</code></pre>
<pre><code class="language-js">{
  allowedChannels: [],
  // ...other fields
}
</code></pre>
<hr>
<h2>4. Feature Flag &amp; Allowlist Store</h2>
<p>Server-side feature flag and approved plugin ledger, stored in settings.</p>
<pre><code>cli.js:490913-490927
</code></pre>
<pre><code class="language-js">// Returns the approved channels allowlist (plugin ledger from server)
// Schema: Array&lt;{ marketplace: string, plugin: string }&gt;
function La6() {          // getChannelLedger
  let A = getSetting(&quot;tengu_harbor_ledger&quot;, []);
  let q = ledgerSchema().safeParse(A);
  return q.success ? q.data : [];
}

// Returns whether the channels feature is enabled (server-side kill switch)
function Ra6() {          // isChannelsEnabled
  return getSetting(&quot;tengu_harbor&quot;, false);
}

// Ledger schema
var ledgerSchema = lazy(() =&gt;
  z.array(z.object({ marketplace: z.string(), plugin: z.string() })),
);
</code></pre>
<hr>
<h2>5. Authorization Gate</h2>
<p>The core gate function. Called when an MCP server connects. Returns <code>&quot;register&quot;</code> or <code>&quot;skip&quot;</code>.</p>
<pre><code>cli.js:490944-491002
</code></pre>
<pre><code class="language-js">function qMq(serverName, capabilities, pluginSource) {
  // 1. Capability check: server must declare claude/channel
  if (!capabilities?.experimental?.[&quot;claude/channel&quot;])
    return {
      action: &quot;skip&quot;,
      kind: &quot;capability&quot;,
      reason: &quot;server did not declare claude/channel capability&quot;,
    };

  // 2. Feature flag: channels must be enabled server-side
  if (!Ra6())
    return {
      action: &quot;skip&quot;,
      kind: &quot;disabled&quot;,
      reason: &quot;channels feature is not currently available&quot;,
    };

  // 3. Auth check: requires claude.ai login
  if (!getAuth()?.accessToken)
    return {
      action: &quot;skip&quot;,
      kind: &quot;auth&quot;,
      reason: &quot;channels requires claude.ai authentication (run /login)&quot;,
    };

  // 4. Org policy check
  let policySettings = getSetting(&quot;policySettings&quot;);
  if (policySettings !== null &amp;&amp; policySettings.channelsEnabled !== true)
    return {
      action: &quot;skip&quot;,
      kind: &quot;policy&quot;,
      reason:
        &quot;channels not enabled by org policy (set channelsEnabled: true in managed settings)&quot;,
    };

  // 5. Session allowlist: must be in --channels list
  let entry = ci1(serverName, Ju()); // findChannelEntry
  if (!entry)
    return {
      action: &quot;skip&quot;,
      kind: &quot;session&quot;,
      reason: `server ${serverName} not in --channels list for this session`,
    };

  // 6. Plugin marketplace verification
  if (entry.kind === &quot;plugin&quot;) {
    let installedMarketplace = pluginSource
      ? parsePluginSource(pluginSource).marketplace
      : undefined;
    if (installedMarketplace !== entry.marketplace)
      return {
        action: &quot;skip&quot;,
        kind: &quot;marketplace&quot;,
        reason: `you asked for plugin:${entry.name}@${entry.marketplace} but the installed ${entry.name} plugin is from ${installedMarketplace ?? &quot;an unknown source&quot;}`,
      };
    if (
      !entry.dev &amp;&amp;
      !La6().some(
        (w) =&gt; w.plugin === entry.name &amp;&amp; w.marketplace === entry.marketplace,
      )
    )
      return {
        action: &quot;skip&quot;,
        kind: &quot;allowlist&quot;,
        reason: `plugin ${entry.name}@${entry.marketplace} is not on the approved channels allowlist (use --dangerously-load-development-channels for local dev)`,
      };
  }
  // 7. Server allowlist: servers must be dev mode
  else if (!entry.dev)
    return {
      action: &quot;skip&quot;,
      kind: &quot;allowlist&quot;,
      reason: `server ${entry.name} is not on the approved channels allowlist (use --dangerously-load-development-channels for local dev)`,
    };

  return { action: &quot;register&quot; };
}
</code></pre>
<h3>Gate layers summary</h3>
<table>
<thead>
<tr>
<th>Order</th>
<th>Kind</th>
<th>Check</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>capability</code></td>
<td>Server declares <code>experimental[&quot;claude/channel&quot;]</code></td>
</tr>
<tr>
<td>2</td>
<td><code>disabled</code></td>
<td><code>tengu_harbor</code> feature flag is <code>true</code></td>
</tr>
<tr>
<td>3</td>
<td><code>auth</code></td>
<td><code>accessToken</code> exists (claude.ai login)</td>
</tr>
<tr>
<td>4</td>
<td><code>policy</code></td>
<td><code>policySettings.channelsEnabled === true</code></td>
</tr>
<tr>
<td>5</td>
<td><code>session</code></td>
<td>Server is in <code>--channels</code> list</td>
</tr>
<tr>
<td>6</td>
<td><code>marketplace</code> / <code>allowlist</code></td>
<td>Plugin source matches &amp; is on approved ledger; or server is in dev mode</td>
</tr>
</tbody></table>
<hr>
<h2>6. Notification Schema</h2>
<p>MCP notification schema validated with Zod.</p>
<pre><code>cli.js:491012-491021
</code></pre>
<pre><code class="language-js">var channelNotificationSchema = lazy(() =&gt;
  z.object({
    method: z.literal(&quot;notifications/claude/channel&quot;),
    params: z.object({
      content: z.string(),
      meta: z.record(z.string(), z.string()).optional(),
    }),
  }),
);
</code></pre>
<h3>MCP server must send</h3>
<pre><code class="language-json">{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;method&quot;: &quot;notifications/claude/channel&quot;,
  &quot;params&quot;: {
    &quot;content&quot;: &quot;Hello from Telegram! User @alice says: can you check the logs?&quot;,
    &quot;meta&quot;: {
      &quot;sender&quot;: &quot;alice&quot;,
      &quot;platform&quot;: &quot;telegram&quot;
    }
  }
}
</code></pre>
<hr>
<h2>7. Content Wrapping</h2>
<p>Incoming content is wrapped in a <code>&lt;channel&gt;</code> XML tag before injection.</p>
<pre><code>cli.js:39391 (constant)
</code></pre>
<pre><code class="language-js">cH6 = &quot;channel&quot;; // XML tag name
</code></pre>
<pre><code>cli.js:490929-490937
</code></pre>
<pre><code class="language-js">// stY = /^[a-zA-Z_][a-zA-Z0-9_]*$/  -- attribute name validator

function AMq(serverName, content, meta) {
  let attrs = Object.entries(meta ?? {})
    .filter(([key]) =&gt; stY.test(key))       // validate attr names
    .map(([key, val]) =&gt; ` ${key}=&quot;${escapeXml(val)}&quot;`)
    .join(&quot;&quot;);
  return `&lt;channel source=&quot;${escapeXml(serverName)}&quot;${attrs}&gt;
${content}
&lt;/channel&gt;`;
}
</code></pre>
<h3>Output example</h3>
<pre><code class="language-xml">&lt;channel source=&quot;my-telegram-bot&quot; sender=&quot;alice&quot; platform=&quot;telegram&quot;&gt;
Hello from Telegram! User @alice says: can you check the logs?
&lt;/channel&gt;
</code></pre>
<hr>
<h2>8. Server Lookup</h2>
<p>Finds a channel entry by server name from the allowed list.</p>
<pre><code>cli.js:490938-490942
</code></pre>
<pre><code class="language-js">function ci1(serverName, allowedChannels) {
  let parts = serverName.split(&quot;:&quot;);
  return allowedChannels.find((entry) =&gt;
    entry.kind === &quot;server&quot;
      ? serverName === entry.name
      : parts[0] === &quot;plugin&quot; &amp;&amp; parts[1] === entry.name,
  );
}
</code></pre>
<hr>
<h2>9. Notification Handler Registration</h2>
<p>When the gate passes, the handler is registered on the MCP client.</p>
<pre><code>cli.js:491198-491217
</code></pre>
<pre><code class="language-js">// Inside the MCP client &quot;connected&quot; handler:
let gateResult = qMq(client.name, client.capabilities, client.config.pluginSource);

switch (gateResult.action) {
  case &quot;register&quot;:
    log(client.name, &quot;Channel notifications registered&quot;);
    client.client.setNotificationHandler(
      channelNotificationSchema(),
      async (notification) =&gt; {
        let { content, meta } = notification.params;
        log(
          client.name,
          `notifications/claude/channel: ${content.slice(0, 80)}`,
        );
        // Inject into conversation
        submitInput({
          mode: &quot;prompt&quot;,
          value: AMq(client.name, content, meta), // wrap in &lt;channel&gt; XML
          priority: &quot;next&quot;,
          isMeta: true,
          origin: { kind: &quot;channel&quot;, server: client.name },
          skipSlashCommands: true,
        });
      },
    );
    break;

  case &quot;skip&quot;:
    // ... (see next section)
    break;
}
</code></pre>
<h3>Key injection parameters</h3>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><code>mode</code></td>
<td><code>&quot;prompt&quot;</code></td>
<td>Treated as a user prompt turn</td>
</tr>
<tr>
<td><code>priority</code></td>
<td><code>&quot;next&quot;</code></td>
<td>Queued immediately after current turn</td>
</tr>
<tr>
<td><code>isMeta</code></td>
<td><code>true</code></td>
<td>Not shown as a normal user message in UI</td>
</tr>
<tr>
<td><code>origin</code></td>
<td><code>{ kind: &quot;channel&quot;, server }</code></td>
<td>Tags the message origin for special handling</td>
</tr>
<tr>
<td><code>skipSlashCommands</code></td>
<td><code>true</code></td>
<td>Prevents content from being interpreted as <code>/commands</code></td>
</tr>
</tbody></table>
<hr>
<h2>10. Skip Handler &amp; UI Warnings</h2>
<p>When the gate returns <code>&quot;skip&quot;</code>, the notification handler is removed and a warning may be shown.</p>
<pre><code>cli.js:491219-491249
</code></pre>
<pre><code class="language-js">case &quot;skip&quot;:
  // Remove any existing handler
  client.client.removeNotificationHandler(&quot;notifications/claude/channel&quot;);
  log(client.name, `Channel notifications skipped: ${gateResult.reason}`);

  // Show a UI notification for actionable skip reasons
  // (not for &quot;capability&quot; or &quot;session&quot; -- those are expected/silent)
  if (
    gateResult.kind !== &quot;capability&quot; &amp;&amp;
    gateResult.kind !== &quot;session&quot; &amp;&amp;
    !shownWarnings.has(gateResult.kind) &amp;&amp;
    (gateResult.kind === &quot;marketplace&quot; ||
      gateResult.kind === &quot;allowlist&quot; ||
      findChannelEntry(client.name, getAllowedChannels()) !== undefined)
  ) {
    shownWarnings.add(gateResult.kind);
    let text =
      gateResult.kind === &quot;disabled&quot;
        ? &quot;Channels are not currently available&quot;
        : gateResult.kind === &quot;auth&quot;
          ? &quot;Channels require claude.ai authentication - run /login&quot;
          : gateResult.kind === &quot;policy&quot;
            ? &quot;Channels are not enabled for your org - have an administrator set channelsEnabled: true in managed settings&quot;
            : gateResult.reason;

    addNotification({
      key: `channels-blocked-${gateResult.kind}`,
      priority: &quot;high&quot;,
      text: text,
      color: &quot;warning&quot;,
      timeoutMs: 12000,
    });
  }
  break;
</code></pre>
<hr>
<h2>11. Conversation Injection</h2>
<p>Channel messages receive special treatment in the conversation pipeline.</p>
<h3>Included in API context (unlike other meta messages)</h3>
<pre><code>cli.js:544651-544658
</code></pre>
<pre><code class="language-js">function i0q(message, includeTranscriptOnly) {
  if (message.type !== &quot;user&quot;) return true;
  if (message.isMeta) {
    // Channel-origin meta messages ARE sent to the API
    if (message.origin?.kind === &quot;channel&quot;) return true;
    // Other meta messages are filtered out
    return false;
  }
  if (message.isVisibleInTranscriptOnly &amp;&amp; !includeTranscriptOnly) return false;
  return true;
}
</code></pre>
<h3>Counts as a billable turn</h3>
<pre><code>cli.js:249999-250004
</code></pre>
<pre><code class="language-js">function K44(message) {
  if (message.origin?.kind === &quot;channel&quot;) return true;
  return S96(message); // normal billable-turn check
}
</code></pre>
<hr>
<h2>12. Channel Status Summary</h2>
<p>Used by UI components to render channel status at startup.</p>
<pre><code>cli.js:508125-508152
</code></pre>
<pre><code class="language-js">// Check if entry is from the approved (non-dev) list
function qq_(entry) {
  return !entry.dev;
}

// Build channel status object for UI
function Kq_() {
  let channels = getAllowedChannels(); // Ju()
  if (channels.length === 0)
    return {
      channels,
      disabled: false,
      noAuth: false,
      policyBlocked: false,
      list: &quot;&quot;,
    };
  let list = channels.map(formatChannelEntry).join(&quot;, &quot;);
  let policySettings = getSetting(&quot;policySettings&quot;);
  return {
    channels,
    disabled: !Ra6(),                                        // feature flag off
    noAuth: !getAuth()?.accessToken,                         // not logged in
    policyBlocked: policySettings !== null &amp;&amp; policySettings.channelsEnabled !== true,
    list,
  };
}

// Format entry for display
function qo6(entry) {
  return entry.kind === &quot;plugin&quot;
    ? `plugin:${entry.name}@${entry.marketplace}`
    : `server:${entry.name}`;
}
</code></pre>
<hr>
<h2>13. Entry Validation</h2>
<p>Validates each channel entry against MCP config, installed plugins, and the allowlist.</p>
<pre><code>cli.js:508153-508181
</code></pre>
<pre><code class="language-js">function Yq_(channels) {
  if (channels.length === 0) return [];

  // Collect all known MCP server names across all config scopes
  let scopes = [&quot;enterprise&quot;, &quot;user&quot;, &quot;project&quot;, &quot;local&quot;];
  let knownServers = new Set();
  for (let scope of scopes)
    for (let name of Object.keys(getMcpConfig(scope).servers))
      knownServers.add(name);

  // Collect all installed plugins
  let installedPlugins = new Set(Object.keys(getPluginRegistry().plugins));

  // Get approved ledger
  let ledger = La6(); // getChannelLedger

  let warnings = [];
  for (let entry of channels) {
    if (entry.kind === &quot;server&quot;) {
      if (!knownServers.has(entry.name))
        warnings.push({
          entry,
          why: &quot;no MCP server configured with that name&quot;,
        });
      if (!entry.dev)
        warnings.push({
          entry,
          why: &quot;server: entries need --dangerously-load-development-channels&quot;,
        });
      continue;
    }
    // Plugin checks
    if (!installedPlugins.has(`${entry.name}@${entry.marketplace}`))
      warnings.push({ entry, why: &quot;plugin not installed&quot; });
    if (
      !entry.dev &amp;&amp;
      !ledger.some(
        (l) =&gt; l.plugin === entry.name &amp;&amp; l.marketplace === entry.marketplace,
      )
    )
      warnings.push({
        entry,
        why: &quot;not on the approved channels allowlist&quot;,
      });
  }
  return warnings;
}
</code></pre>
<hr>
<h2>14. Dev Channel Confirmation Dialog</h2>
<p>Shown when <code>--dangerously-load-development-channels</code> is used.</p>
<pre><code>cli.js:594799-594819
</code></pre>
<pre><code class="language-js">// Warning text
&quot;--dangerously-load-development-channels is for local channel development only. &quot;
+ &quot;Do not use this option to run channels you have downloaded off the internet.&quot;

&quot;Please use --channels to run a list of approved channels.&quot;

// Renders a confirmation UI showing which channels are being loaded:
// &quot;Channels: server:my-server, plugin:my-plugin@marketplace&quot;
</code></pre>
<pre><code>cli.js:594856-594859
</code></pre>
<pre><code class="language-js">// Format for the confirmation dialog
function kT_(entry) {
  return entry.kind === &quot;plugin&quot;
    ? `plugin:${entry.name}@${entry.marketplace}`
    : `server:${entry.name}`;
}
</code></pre>
<hr>
<h2>15. Flow Diagram</h2>
<pre><code>CLI startup
  |
  +-- Parse --channels args
  |     +-- &quot;plugin:&lt;name&gt;@&lt;marketplace&gt;&quot; -&gt; { kind:&quot;plugin&quot;, name, marketplace }
  |     +-- &quot;server:&lt;name&gt;&quot;               -&gt; { kind:&quot;server&quot;, name }
  |     +-- else -&gt; error &amp; exit
  |
  +-- Store in global state (T8.allowedChannels)
  |
  +-- If --dangerously-load-development-channels:
  |     +-- Show confirmation dialog
  |     +-- Mark entries with dev=true
  |
  +-- MCP server connects with capabilities.experimental[&quot;claude/channel&quot;]
        |
        +-- qMq() gate check:
        |     1. capability  -- server declares claude/channel?
        |     2. disabled    -- tengu_harbor feature flag on?
        |     3. auth        -- claude.ai accessToken exists?
        |     4. policy      -- org channelsEnabled === true?
        |     5. session     -- server in --channels list?
        |     6. allowlist   -- plugin on ledger / server is dev?
        |
        +-- PASS (&quot;register&quot;):
        |     +-- setNotificationHandler(&quot;notifications/claude/channel&quot;)
        |     +-- On notification:
        |           +-- Wrap content in &lt;channel source=&quot;...&quot; ...&gt;...&lt;/channel&gt;
        |           +-- submitInput({ mode:&quot;prompt&quot;, isMeta:true, origin:{kind:&quot;channel&quot;} })
        |           +-- Claude sees the &lt;channel&gt; tag and responds
        |
        +-- FAIL (&quot;skip&quot;):
              +-- removeNotificationHandler
              +-- Show warning notification (if actionable)
</code></pre>
<hr>
<h2>Implementation Checklist</h2>
<p>To implement channels in another project:</p>
<ol>
<li><strong>CLI flags</strong>: Add <code>--channels</code> and <code>--dangerously-load-development-channels</code> options</li>
<li><strong>Entry parsing</strong>: Parse <code>plugin:&lt;name&gt;@&lt;marketplace&gt;</code> and <code>server:&lt;name&gt;</code> formats</li>
<li><strong>Global state</strong>: Store the allowed channels list</li>
<li><strong>Feature flag</strong>: Implement a server-side kill switch (<code>tengu_harbor</code>)</li>
<li><strong>Allowlist/ledger</strong>: Store approved <code>{ plugin, marketplace }</code> entries server-side (<code>tengu_harbor_ledger</code>)</li>
<li><strong>Gate function</strong>: Implement the 6-layer authorization check</li>
<li><strong>MCP capability</strong>: Require servers to declare <code>experimental[&quot;claude/channel&quot;]</code></li>
<li><strong>Notification handler</strong>: Listen for <code>notifications/claude/channel</code> on connected MCP clients</li>
<li><strong>Content wrapping</strong>: Wrap incoming content in <code>&lt;channel source=&quot;...&quot; attr=&quot;...&quot;&gt;</code> XML tags</li>
<li><strong>Conversation injection</strong>: Submit as a meta user turn with <code>origin: { kind: &quot;channel&quot; }</code>, ensuring:<ul>
<li>It&#39;s included in API context (unlike other meta messages)</li>
<li>It counts as a billable turn</li>
<li>It&#39;s not interpreted as slash commands</li>
</ul>
</li>
<li><strong>UI</strong>: Show channel status, validation warnings, and dev channel confirmation dialog</li>
</ol>
]]></description>
    </item>
    <item>
      <title>Claude Code Channels 详细总结</title>
      <link>https://blog.sorrycc.com/claude-code-channels</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/claude-code-channels</guid>
      <pubDate>Fri, 20 Mar 2026 01:10:36 GMT</pubDate>
      <description><![CDATA[<h1>Claude Code Channels 详细总结</h1>
<blockquote>
<p>来源推文：<a href="https://x.com/trq212/status/2034761016320696565">https://x.com/trq212/status/2034761016320696565</a>
发布者：Thariq (@trq212)，Anthropic 员工
发布时间：2026 年 3 月 19 日</p>
</blockquote>
<h2>核心发布</h2>
<p>Anthropic 官方发布了 <strong>Claude Code Channels</strong>（研究预览版），允许用户通过 <strong>Telegram</strong> 和 <strong>Discord</strong> 等即时通讯工具远程控制正在运行的 Claude Code 会话——直接用手机给 Claude Code 发消息。</p>
<hr>
<h2>它是什么？</h2>
<p>Channel 本质上是一个 <strong>MCP Server</strong>，能将外部事件（聊天消息、CI 结果、监控告警等）推送到正在运行的 Claude Code 会话中。它是<strong>双向的</strong>：Claude 能读取消息并通过同一频道回复。</p>
<p><strong>关键点</strong>：</p>
<ul>
<li>需要 Claude Code <strong>v2.1.80+</strong></li>
<li>需要 <strong>claude.ai 登录</strong>（不支持 Console/API Key 认证）</li>
<li>Team/Enterprise 组织需管理员显式启用</li>
<li>目前仅在<strong>研究预览</strong>阶段，仅支持 Anthropic 白名单内的插件</li>
<li>插件需要安装 <strong>Bun</strong> 运行时</li>
<li>事件仅在会话打开时到达，若需常驻运行需在后台进程或持久终端中运行 Claude</li>
</ul>
<hr>
<h2>支持的频道 &amp; 设置方式</h2>
<h3>Telegram 设置流程</h3>
<ol>
<li>在 Telegram 中通过 <strong>@BotFather</strong> 创建机器人，获取 token</li>
<li>Claude Code 中运行 <code>/plugin install telegram@claude-plugins-official</code></li>
<li>运行 <code>/telegram:configure &lt;token&gt;</code> 配置 token</li>
<li>重启 Claude Code：<code>claude --channels plugin:telegram@claude-plugins-official</code></li>
<li>在 Telegram 给你的 bot 发消息获取配对码，回到 Claude Code 运行 <code>/telegram:access pair &lt;code&gt;</code></li>
<li>锁定访问：<code>/telegram:access policy allowlist</code></li>
</ol>
<h3>Discord 设置流程</h3>
<ol>
<li>在 <a href="https://discord.com/developers/applications">Discord 开发者门户</a> 创建应用和 bot</li>
<li>启用 <strong>Message Content Intent</strong>（Privileged Gateway Intents 中）</li>
<li>通过 OAuth2 URL Generator 邀请 bot 到你的服务器（需要权限：View Channels、Send Messages、Send Messages in Threads、Read Message History、Attach Files、Add Reactions）</li>
<li>Claude Code 中运行 <code>/plugin install discord@claude-plugins-official</code></li>
<li>运行 <code>/discord:configure &lt;token&gt;</code> 配置 token</li>
<li>重启：<code>claude --channels plugin:discord@claude-plugins-official</code></li>
<li>在 Discord 私信 bot 获取配对码，配对并设置白名单</li>
</ol>
<h3>本地演示（fakechat）</h3>
<ul>
<li>运行 <code>/plugin install fakechat@claude-plugins-official</code></li>
<li>启动：<code>claude --channels plugin:fakechat@claude-plugins-official</code></li>
<li>访问 <code>http://localhost:8787</code> 即可在浏览器中与 Claude Code 对话</li>
</ul>
<hr>
<h2>安全机制</h2>
<ul>
<li>每个频道维护一个 <strong>发送者白名单</strong>，未配对的用户消息会被静默丢弃</li>
<li>必须通过 <code>--channels</code> 参数显式启用，仅写在 <code>.mcp.json</code> 里不够</li>
<li>配对流程：发送消息 -&gt; 获取配对码 -&gt; 在 Claude Code 中确认 -&gt; 加入白名单</li>
<li>Pro/Max 用户默认可用，Team/Enterprise 需管理员在 claude.ai Admin settings 中启用</li>
</ul>
<hr>
<h2>企业控制</h2>
<table>
<thead>
<tr>
<th align="left">计划类型</th>
<th align="left">默认行为</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Pro / Max（无组织）</td>
<td align="left">Channels 可用，用户通过 <code>--channels</code> 按会话启用</td>
</tr>
<tr>
<td align="left">Team / Enterprise</td>
<td align="left">默认禁用，需管理员显式启用</td>
</tr>
</tbody></table>
<p>管理员可在 <strong>claude.ai → Admin settings → Claude Code → Channels</strong> 中启用，或在 managed settings 中设置 <code>channelsEnabled: true</code>。</p>
<hr>
<h2>相关链接</h2>
<ul>
<li>官方文档：<a href="https://code.claude.com/docs/en/channels">https://code.claude.com/docs/en/channels</a></li>
<li>自建 Channel 参考：<a href="https://code.claude.com/docs/en/channels-reference">https://code.claude.com/docs/en/channels-reference</a></li>
<li>Telegram 插件源码：<a href="https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram">https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram</a></li>
<li>Discord 插件源码：<a href="https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/discord">https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/discord</a></li>
<li>反馈渠道：<a href="https://github.com/anthropics/claude-code/issues">https://github.com/anthropics/claude-code/issues</a></li>
</ul>
<hr>
<h2>社区反应热点</h2>
<h3>1. OpenClaw 被取代</h3>
<p>大量评论认为这直接冲击了第三方 wrapper 产品 OpenClaw，多人表示&quot;OpenClaw 完了&quot;。@suddenlyliu 发表了深度分析，认为这标志着企业 AI 走向 <strong>Headless（无 UI）架构</strong>，交互层已经被商品化，开发者唯一的护城河是构建 Context 层和 Memory 层。</p>
<blockquote>
<p>&quot;GUI 已死。欢迎来到有状态的、无头 AI 时代。&quot; —— @suddenlyliu</p>
</blockquote>
<h3>2. 与 Dispatch 的区别</h3>
<p>Thariq 回应称 Channels 面向开发者，更强调<strong>可 hack 性（hackable）</strong>，而 Dispatch 是另一种远程交互方式，Anthropic 希望提供多种选择。</p>
<blockquote>
<p>&quot;我们希望给你很多不同的远程与 Claude 对话的方式，Channels 更聚焦于想要可 hack 方案的开发者。&quot; —— @trq212</p>
</blockquote>
<h3>3. 使用场景想象</h3>
<ul>
<li>躺沙发上通过 Telegram 部署代码</li>
<li>散步时随手给 Claude 发需求</li>
<li>凌晨 2 点从床上发消息让 Claude 干活</li>
<li>反馈循环从&quot;坐在 IDE 前&quot;变成&quot;走路时想到就发出去&quot;</li>
</ul>
<blockquote>
<p>&quot;这就是重点！！出去走走，给 Claude Code 发消息就行了。&quot; —— @trq212</p>
</blockquote>
<h3>4. 更多平台的呼声</h3>
<p>用户希望支持 <strong>WhatsApp、Slack、iMessage</strong>。Thariq 对 iMessage 回了个 👀，暗示可能会做。</p>
<h3>5. 其他问题</h3>
<ul>
<li><strong>是否支持 Claude Code 桌面版？</strong> —— 目前不支持，但 Thariq 暗示同事可以实现</li>
<li><strong>是否支持远程会话（Claude Code Web）？</strong> —— 未回答</li>
<li><strong>是否支持 Amazon Bedrock？</strong> —— Thariq 把问题转给了同事 @noahzweben</li>
<li><strong>是否支持 Discord 语音频道？</strong> —— 未回答</li>
</ul>
<hr>
<h2>一句话总结</h2>
<p><strong>Anthropic 把 Claude Code 变成了一个可以用手机随时随地控制的无头开发代理，通过 Telegram/Discord 双向通讯，让&quot;出门在外也能写代码&quot;成为现实，同时对第三方 wrapper 产品构成致命打击。</strong></p>
]]></description>
    </item>
    <item>
      <title>626 - 《我不再做的事》</title>
      <link>https://blog.sorrycc.com/things-im-not-doing-anymore</link>
      <guid isPermaLink="true">https://blog.sorrycc.com/things-im-not-doing-anymore</guid>
      <pubDate>Thu, 19 Mar 2026 08:00:04 GMT</pubDate>
      <description><![CDATA[<p>省下来的时间，比折腾出来的结果值钱。</p>
<p>持续更新。先列下 AI 相关的。</p>
<ul>
<li><strong>不再手写代码。</strong> AI 写，人审。</li>
<li><strong>不用 AI 中转。</strong> 直连官方，少一层就少一层风险。</li>
<li><strong>不研究 u 卡。</strong> 在 TODO List 里，一直没试。感觉容易封，折腾半天一夜回到解放前。</li>
<li><strong>不研究注册机。</strong> 同上，投入产出比太低。</li>
<li><strong>不追新的 claw 和 claw channel。</strong> openclaw + telegram 够用。</li>
<li><strong>不再用免费的 token。</strong> 免费的最贵，稳定性优先，目前是 Claude Max + OpenAI Pro + <a href="https://zenmux.ai/invite/08ORQW">Zenmux Max</a> 的组合。</li>
<li><strong>不再 11 点后 Vibe Coding。</strong> 容易上瘾，间接导致熬夜。</li>
</ul>
<p>Ref:</p>
<ul>
<li><a href="https://blog.douchi.space/things-im-not-doing-anymore/">我不再做的事 - 椒盐豆豉</a></li>
<li><a href="https://x.com/gregisenberg/status/1949626889435234360">https://x.com/gregisenberg/status/1949626889435234360</a></li>
</ul>]]></description>
    </item>
  </channel>
</rss>