跳到主要内容
返回ClaudeCode
工具教程

12|16 小时 Claude Code:settings.json、输出风格与 Hooks

星核工具箱编辑部约 51 分钟阅读

31 · settings.json:用户级 / 项目级配置

这里有件特别容易栽的蠢事,踩过一次就再难忘。

刚开始正经用 Claude Code 时,常见的操作是在某个项目的 .claude/settings.json 里塞一行 defaultMode: "auto",想让它一进项目就自动放行、别老问。改完没反应。第一反应是写错了字段名,对着官方文档逐字核对三遍,一个字母都没错。再怀疑是 JSON 格式坏了,拿在线校验器跑一遍,合法得很。折腾大概二十分钟,甚至开始怀疑是不是 Claude Code 这个版本有 bug。

后来才在文档角落里看到那句话:defaultMode 设成 "auto" 时,写在项目设置里会被直接忽略——这是官方故意拦的,防止某个仓库偷偷给自己开自动模式(这点第 20 篇也提过)。那行配置语法全对、文件没坏,纯粹是写错了「楼层」。挪到用户级的 ~/.claude/settings.json 里,秒生效。

说这个坑是想让你记住一件事:settings.json 的坑,九成不在「怎么写」,而在「写在哪一层、哪一层压哪一层」。今天就把这套「楼层规则」掰开揉碎,让你下次配置时心里有数——这条该放主目录还是放项目里,一眼就知道。

看完这一篇,你会拿到:

  • 一句话讲明白 settings.json 是什么、它跟 CLAUDE.md 到底分什么工
  • 三个层级(用户级 / 项目级 / 本地级)各自的文件位置、影响范围、该放什么
  • 一张「谁压谁」的优先级表,外加一个最反直觉的例外(数组是合并不是覆盖)
  • 几个你最常会动的配置项(modelpermissionsenvhooksstatusLine)分别干嘛、放哪层
  • 一个能照着跑、给了预期输出的实战:写一份配置 → 用 /status 验证它真生效

01 先搞懂:settings.json 是什么,跟 CLAUDE.md 分什么工

先给结论:settings.json 是 Claude Code 的「行为开关总成」——用 JSON 格式管权限、环境变量、默认模型、Hook、状态栏这些工具行为;它跟 CLAUDE.md 是两套东西,一个管「怎么干活」,一个管「记住什么」。

很多人一上来就把这俩搞混。你前面写了一路 CLAUDE.md(第 18 篇),又配了权限规则(第 20 篇),接下来还要配 Hook——这些东西最终都落在 settings.json 这个文件里,但它跟 CLAUDE.md 装的是完全不同的内容。

类比:公司的项目档案柜 vs 你工位上的电闸盒。 CLAUDE.md 像档案柜里那本《项目说明书》——写的是「我们用 pnpm 不用 npm」「提交前先跑测试」这类给人(给 Claude)看的、自然语言的约定,它每会话被读进去当背景。settings.json 不一样,它是工位墙上那个电闸盒——里面是一个个明确的开关:这个工具准不准用、默认跑哪个模型、改完文件自动触发哪段脚本。档案柜是「讲给它听的规矩」,电闸盒是「替它定死的机器行为」

官方把它的定位说得很干脆:

settings.json 文件是通过分层设置配置 Claude Code 的官方机制。

注意「官方机制」和「分层」这两个词,正好是这一篇的两条主线:它是配置 Claude Code 的正规入口(不是临时在命令行敲参数),而且是分好几层叠起来的(用户级、项目级、本地级)。

落到你会遇到的真实场景,settings.json 管的就是这几类事:

  • 「这个项目里,rm -rf 这种命令给我拦死」——写 permissions.deny
  • 「这个项目默认用 Sonnet 就行,别老用 Opus 烧额度」——写 model
  • 「每次它改完文件,自动跑一遍格式化」——写 hooks
  • 「我想让终端底下那行状态栏显示当前 git 分支」——写 statusLine

这些都不是「讲给 Claude 听」的话,而是实打实改变它运行行为的开关。这就是 settings.json 跟 CLAUDE.md 的根本分工。

💡 一句话总结:CLAUDE.md 是「讲给 Claude 听的自然语言规矩」,settings.json 是「替它定死机器行为的开关总成」——前者管记住什么,后者管怎么干活,两套东西别混着用。


02 三个层级:主目录、项目里、还是只在你这台机器

settings.json 最该先搞懂的,不是有哪些字段,而是它有三个层级,同一个文件名摆在三个不同位置,作用范围天差地别。开头那个坑,根子就在没分清层级。

类比:贴通知的三种地方。 同一条「下班记得关空调」的通知,贴在公司大门口(全公司每个项目都看得到)、贴在这间办公室门上(只有这个项目的人看得到、还登记进了办公室公约)、还是贴在你自己显示器边上的便利贴(只有你看、别人不知道)——范围完全不同。settings.json 的三个层级就是这三种「贴法」。

官方定义的三层,看这张表(外加最高的 Managed 层,企业 IT 专用,咱们小白基本碰不到,下一节单独提一句):

层级文件位置影响谁进 git 吗该放什么
用户级(User)~/.claude/settings.json你,跨你所有项目否(在你主目录)个人偏好:你惯用的模型、主题、跨项目都想要的工具
项目级(Project).claude/settings.json这个仓库的所有协作者(提交到 git 共享)团队约定:权限规则、Hook、共享的 MCP
本地级(Local).claude/settings.local.json你,仅在这个仓库否(自动 gitignored)个人覆盖、带凭据的实验性配置

三层的取舍,记住这三句就够:

  • 「我所有项目都想要」→ 用户级~/.claude/settings.json)。比如「我习惯默认用 Sonnet」「我的状态栏脚本」,配一次,开任何项目都在。
  • 「全队都该有、还得跟着仓库走」→ 项目级.claude/settings.json)。它被提交进 git,队友拉下来就有同一套配置,这是「配置即代码」。
  • 「只我自己、这个项目专属、不想进版本库」→ 本地级.claude/settings.local.json)。

这里有个特别贴心的细节,官方明说了:当你创建 .claude/settings.local.json 时,Claude Code 会自动帮你把它加进 git 忽略

Claude Code 将在创建 .claude/settings.local.json 时配置 git 以忽略它。

为啥这么设计?想想看:本地级就是放「私人物品」的——你个人的实验配置、可能带点凭据的东西,这些本来就不该上交到版本库污染队友。官方替你把这道防线焊死了,省得你哪天手滑 git add . 把私人配置怼上去。这也呼应了第 21 篇讲的安全主线:敏感东西,从源头就别让它有机会进 git

实战里怎么分:一个真实项目的三层分布

光记定义不够,看一个真实项目里这三层各放了啥,立刻就有体感:

  • 用户级~/.claude/settings.json):那套自定义状态栏脚本、默认模型偏好。这些跟具体项目无关,是「走到哪带到哪」的个人习惯。
  • 项目级.claude/settings.json):一组 permissions.deny(禁 curl、禁读 .env)、一个「提交前自动跑 lint」的 hook。这些是全队的底线和卡点,必须进 git 让每个协作者拉下来都有。
  • 本地级.claude/settings.local.json):个人临时多放行的几条命令(团队没必要知道)、一个还在试验、没成熟到能分享给队友的 hook。

判断一条配置该放哪层,脑子里过一个问题链就够:「这条只我自己要 → 用户级或本地级;全队都要 → 项目级」,再细分一步:「全项目通用、还跟着我走 → 用户级;就这个项目、还不想进 git → 本地级」

一个反方向的错很常见:图省事把项目专属的权限规则全堆进了用户级——结果换到另一个项目,那些规则全跟过来了,在不相干的项目里平白多出一堆莫名其妙的放行。这才能明白:「这条配置该跟着我走,还是该跟着项目走」,是分层的第一判断。跟着项目走的,老老实实放项目里。

💡 一句话总结:三层一句话区分——跨所有项目放用户级 ~/.claude/settings.json、全队共享放项目级 .claude/settings.json(进 git)、私人覆盖放本地级 .claude/settings.local.json(自动 gitignored);判断口诀「跟着我走 vs 跟着项目走」。


03 谁压谁:优先级,外加一个最反直觉的例外

三层都能写同一个字段。那问题来了:用户级说用 Opus、项目级说用 Sonnet,到底听谁的? 这就是「优先级(precedence)」要管的事,也是 settings.json 最容易绕晕人的地方。

先给官方的优先级排序,从高到低(高的压低的):

优先级层级说人话
1(最高)Managed(企业 IT 部署)公司锁死的策略,谁都改不动
2命令行参数--settings 等)你启动时临时拍的板,只管这一次会话
3本地级 .claude/settings.local.json你在这个项目的私人覆盖
4项目级 .claude/settings.json团队共享的项目设置
5(最低)用户级 ~/.claude/settings.json你的全局默认,没人覆盖时才生效

这个「从高到低」的压制关系,画成一张图你会更有体感——高层像盖在低层上的纸,挡住下面写了同字段的部分

Claude Code settings 五层优先级栈:Managed > 命令行 > 本地 > 项目 > 用户

这张图自上而下就是优先级从高到低:上面的层覆盖下面的层(仅限单值字段)。换句话说,越靠上的越「临时、具体」,越靠下的越「全局、兜底」——只有当上面所有层都没碰某个字段时,最下面的用户级默认才轮到生效。

一句话记牢这个顺序:越「具体到当下」的越大,越「全局兜底」的越小。命令行参数(就这一次)压本地(就这项目、就你),本地压项目(全队),项目压用户(全局)。官方给的例子最直观:

例如,如果您的用户设置允许 Bash(npm run *),但项目的共享设置拒绝它,则项目设置优先,命令被阻止。

也就是说——你在主目录给自己开的绿灯,进了某个项目可能被项目设置一票否决。这恰恰印证了上一篇结尾埋的那个悬念:同一条配置,写在你主目录和写在项目里,效果可能正好相反。开头那个 defaultMode: "auto" 的坑,本质就是栽在没吃透这套层级语义上。

Managed 层,小白一句话带过就行。 它是企业 IT 通过 MDM、注册表或服务器统一下发的策略,优先级最高、用户和项目都覆盖不了,专门给公司强制执行安全合规用的(比如「全公司禁止 curl」)。你自己一个人用、或者小团队协作,基本碰不到它——知道有这么个「天花板层」存在就够了,真在受管控的公司环境里再去翻官方的 server-managed-settings 那页。

那个最反直觉的例外:数组是「合并」,不是「覆盖」

上面说的「谁压谁」,针对的是单个值(像 model 这种,你写一个值我写一个值,高层那个赢)。但有一类配置完全不按这个来,新手十有八九会栽——数组类配置(比如 permissions.allow / deny)是跨层「合并」的,不是覆盖

啥意思?看官方原话:

数组设置跨作用域合并。 当相同的数组值设置出现在多个作用域中时,数组被连接和去重,而不是替换。

翻成大白话:你的权限规则不会被项目设置「整个换掉」,而是两边的规则「拼到一起」一块生效

举个例子你立刻懂:

场景直觉(错的)实际(对的)
用户级 allow: ["Bash(npm run *)"],项目级 allow: ["Bash(git diff *)"]项目级优先级高,所以只剩 git diff两条都生效npm run *git diff * 都被允许

这跟单值字段的「高层覆盖低层」是两套逻辑,一定要分开记:

  • 单值字段(如 modeldefaultMode):高优先级层整个盖掉低层。
  • 数组字段(如 permissions.allow / denyenv 里的多个变量也类似拼装):各层拼起来去重,谁都不会抹掉谁。

这个点很容易绊人:以为在项目级写了一组 deny,就把用户级那组宽松的 allow 顶掉了,结果发现用户级那些 allow 还在生效——因为它俩是合并的,不是替换的。理解了这条,你才不会「以为关了某个口子、其实它还从另一层敞着」。

💡 一句话总结:优先级口诀「越具体到当下的越大」(命令行>本地>项目>用户,Managed 封顶);但数组类配置(尤其权限规则)是跨层合并去重、不是覆盖——这是最容易踩的反直觉点。


04 最常动的几个配置项:放哪层、干嘛用

层级和优先级理清了,来看你实际最常会去动的几个字段。settings.json 官方支持的键有上百个,但 90% 的人日常碰的就这么几个。我挑出来,每个讲清「干嘛的、放哪层最合适」。

先放一份麻雀虽小五脏俱全的示例,让你对长相有个整体印象(这是官方示例的精简版):

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "model": "claude-sonnet-4-6",
  "permissions": {
    "allow": ["Bash(npm run test *)"],
    "deny": ["Bash(curl *)", "Read(./.env)"]
  },
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1"
  }
}

开头那行 $schema 强烈建议你加上。它指向官方的 JSON 架构,加了之后在 VS Code、Cursor 这类编辑器里写配置会有自动补全和实时校验——字段名敲错、值类型不对,编辑器当场给你标红。开头那对着文档逐字核对字段名的二十分钟,要是当初加了这行 $schema,编辑器早就提示出来了。官方原话:

将其添加到您的 settings.json 可在 VS Code、Cursor 和任何其他支持 JSON 架构验证的编辑器中启用自动完成和内联验证。

下面逐个拆这几个高频字段:

model:默认用哪个模型

model 决定这一层默认跑哪个模型。值填模型 ID(像 "claude-sonnet-4-6")。

  • 放哪层:看你的需求。「我个人就爱用某个模型」→ 用户级;「这个项目大家统一用 Sonnet 省额度」→ 项目级。
  • 一个要注意的点model 跟大多数字段不同,它在会话启动时只读一次,改了要么重启、要么会话里用 /model 现切。--model 启动参数和 ANTHROPIC_MODEL 环境变量都能临时盖过它(模型相关详见第 5 篇)。

这个字段有个很顺手的用法:给不同项目配不同的默认模型。比如一个文档类项目,活儿都不重,就在它的项目级 settings.json 里写死 model 用相对轻量的型号;另一个核心代码项目则不设、保持用户级那个更强的默认。这么一分,进哪个项目就自动用哪个档位,不用每次手动 /model——既不浪费强模型的额度在简单活儿上,也不在硬骨头项目上将就。这正是「项目级压用户级」这条优先级的实用价值:项目级给这个项目定制,用户级兜底其余所有项目

permissions:准不准用某个工具 / 命令

这是第 20 篇专门讲过的——allow(放行)、ask(每次问)、deny(拦死)三种规则,按工具、按命令精确控权。

  • 放哪层:团队都该守的安全底线(比如「禁 curl」「禁读 .env」)→ 项目级,进 git 让全队都有;你个人图省事想多放行几条 → 本地级或用户级。
  • 记住第 03 节那条permissions 是数组,跨层合并——别指望在某一层写 deny 就能把别层的 allow 顶掉。

env:给会话注入环境变量

env 里写的键值对,会作为环境变量应用到每个会话以及 Claude Code 跑起来的子进程

类比:进车间前统一发的工牌和装备。 不管今天谁来上工,一进这个车间(会话)就自动配齐这套环境——env 就是这套「入场标配」,你在这儿声明的变量,会话里跑的每条命令、每个子进程都带着它。

  • 典型用途:打开遥测(CLAUDE_CODE_ENABLE_TELEMETRY)、给某个工具链塞个固定变量。
  • 放哪层:项目专属的环境(比如这个项目要连的某个服务地址)→ 项目级;你全局都想要的 → 用户级。

hooks:在固定时机自动跑脚本

hooks 是上一篇反复提到、第 33 篇要专门拆的那个「事件触发自动动作」的入口——它就配在 settings.json 里。比如「每次改完文件自动跑格式化」「每次会话开始打个招呼」。

  • 放哪层:团队都该跑的卡点(如「提交前自动 lint」)→ 项目级;你个人的自动化习惯 → 用户级。
  • 具体怎么写、能监听哪些事件,这里先知道「它的家在 settings.json」就行,第 33 篇展开。

statusLine:自定义底部状态栏

第 14 篇讲过界面底下那行「状态行」。statusLine 让你自定义它显示什么——比如塞个脚本进去,实时显示当前 git 分支、当前模型、token 用量。

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh"
  }
}
  • 放哪层:状态栏是个人视觉偏好,绝大多数人放用户级~/.claude/settings.json),配一次所有项目通用。

我把这几个常用字段「放哪层」的经验汇成一张表,纠结时直接查:

配置项干嘛的我的默认放法
model默认模型个人偏好→用户级;项目统一→项目级
permissions工具 / 命令准入安全底线→项目级(进 git)
env注入环境变量项目专属→项目级;全局→用户级
hooks事件触发自动动作团队卡点→项目级;个人习惯→用户级
statusLine自定义状态栏个人视觉→用户级

一个容易踩的混淆:不是所有配置都住在 settings.json 里

这点很多人不知道,往往是被报错教育才记住的。Claude Code 还有另一个配置文件 ~/.claude.json(注意,是主目录下的 .claude.json,跟 ~/.claude/settings.json 不是一个东西)。它装的是另一类东西:你的登录会话、用户/本地作用域的 MCP server 配置(第 22 篇提过 MCP 配置存这儿)、每个项目的信任状态、各种缓存。

关键的坑在于——有少数配置项官方规定就只能放 ~/.claude.json,你要是把它们写进 settings.json,会直接触发架构校验错误。官方点名的几个比如:autoConnectIde(外部终端自动连 IDE)、teammateDefaultModel(队友默认模型)这类。

典型的撞坑场景是想配「外部终端自动连 VS Code」,顺手写进了 settings.json,结果 $schema 校验当场标红、/status 也报错。翻文档才知道这字段的家在 ~/.claude.json所以记住:settings.json 管「行为开关」,~/.claude.json 管「会话状态 / MCP / 缓存」这类幕后数据——绝大多数时候你只碰前者,但偶尔有字段「死活写不进 settings.json」时,先想想它是不是该去 ~/.claude.json

💡 一句话总结:高频字段就这几个——model(默认模型)、permissions(控权)、env(环境变量)、hooks(自动动作)、statusLine(状态栏);「全队该有」放项目级进 git、「个人偏好」放用户级,加上 $schema 让编辑器替你查错;注意少数字段(如 autoConnectIde)住在 ~/.claude.json 而非 settings.json


05 在哪编辑、改完啥时候生效、怎么确认它真的读到了

字段会写了,还有三个特别实际的问题没解决:在哪改、改完要不要重启、怎么知道它真生效了。开头折腾的二十分钟,一半时间就耗在「不确定它到底读没读到这份配置」上。

在哪编辑:直接改文件,或者用 /config

两条路:

  1. 直接拿编辑器改那个 JSON 文件——按第 02 节的位置找到对应层级的文件改就行。本项目 CLAUDE.md 也建议「改文件优先用清晰的编辑」,比临时敲命令更可控。
  2. 会话里敲 /config——官方提供的交互式设置界面,能看状态、改一部分常用开关(主题、详细输出这类)。

一个容易误会的点官方专门澄清过:/config 里那个 Config 选项卡不是settings.json 文件内容的完整视图,它只是「主题、详细输出」等少数固定开关的编辑器。别指望在 /config 里看到你写的每一条配置——完整的还得去文件里看。

改完啥时候生效:大部分热加载,两个例外要重启

这是个好消息——Claude Code 会盯着你的设置文件,改了大多数键会在运行中的会话里直接生效,不用重启。官方明说:

Claude Code 监视您的设置文件,并在它们更改时重新加载它们……这包括 permissionshooks 和凭证助手。

但有两个例外,它俩是「启动时只读一次」的,改完得重启(或用对应命令现切)才认:

字段改完怎么生效
model重启,或会话里用 /model 现切
outputStyle(输出样式,下一篇讲)重启,或 /clear 后重建

记这条的实际意义:你改了 permissionshooks存盘就生效,接着用;但你改了 model 发现没变化,别急着怀疑写错了——它就是要重启才认。开头那个坑要是发生在 permissions 上,本来存盘就能验出来,偏偏撞上了「层级被忽略」的特例,才多绕了那么久。

怎么确认它真读到了:/status 看「Setting sources」

这是最关键的一招,专治「我不确定它到底加载了哪份配置」。会话里敲 /status,里面有一行 Setting sources列出当前会话实际加载了哪几层设置——比如 User settingsProject local settings(具体标签名以实际界面为准)。

官方对它的说明很实在:

Setting sources 行确认正在读取哪些源……仅当该源至少加载一个键时,该层才出现在列表中,因此空列表意味着未找到任何设置源。

这话信息量很大,拆开看:

  • 你写的那层出现在列表里 = 这份文件被成功读到了。
  • 你写的那层没出现 = Claude Code 压根没找到 / 没读到它(多半是路径放错了,比如把 .claude/settings.json 写成了 settings.json)。
  • 文件要是有语法错误(JSON 坏了、值不合法),/status直接报错给你看,省得你瞎猜。

所以以后配完一份 settings.json,第一件事就是敲 /status 确认那层真在列表里——这一步要是早知道,开头那二十分钟的弯路能省成两分钟。

配置不生效?照这张表排查

把这一篇所有「坑」收拢成一张排查表。下次你写的配置「没反应」,别急着怀疑语法,按这个顺序自查,基本一查一个准:

现象❌ 别先怀疑✅ 先查这个
改完完全没反应字段名写错了?/status 看你那层在不在 Setting sources 里——不在就是路径放错了
model 改了不变配置坏了?model 重启才生效(或 /model 现切),存盘不够
defaultMode: "auto" 不起作用拼写错了?auto项目/本地级被忽略,得放用户级(第 03 节)
写了 deny 但某命令还能跑deny 没写对?权限跨层合并,多半别层有条 allow 没抹掉(第 03 节)
某字段死活写不进 settings.jsonJSON 格式错了?它可能住在 ~/.claude.json(如 autoConnectIde,第 04 节)

你看这张表里,真正属于「语法写错」的几乎没有——绝大多数「不生效」都是层级、生效时机、合并语义这几个概念没吃透。这也正是开头那个坑最想传给你的一句话:settings.json 难的从来不是怎么写,是搞清它那套分层规则

💡 一句话总结:改文件或用 /config(后者只管少数开关);permissions/hooks 存盘热加载、model/outputStyle 要重启;配完务必 /status 看「Setting sources」那行确认你那层真被读到了;不生效先查层级别查语法。


06 动手:写一份用户级 + 项目级配置,用 /status 验证它真生效

光看不练假把式。下面带你亲手写两层配置、再用 /status 验证它们真被加载——把这一篇的「写文件 → 分层 → 验证」整条链路跑通一遍。全程用最小示例,不依赖你已有的复杂环境。

第一步:建个练手项目(在终端)

mkdir settings-demo && cd settings-demo

第二步:写一份项目级配置

settings-demo/.claude/settings.json 里贴入下面这份(放行测试命令、拦死 curl)。第一行 $schema 让你的编辑器顺手做校验:

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": ["Bash(npm run test *)"],
    "deny": ["Bash(curl *)"]
  }
}

第三步:写一份本地级配置

再在 settings-demo/.claude/settings.local.json 里贴入一条只属于你、不进 git 的私人放行:

{
  "permissions": {
    "allow": ["Bash(git status *)"]
  }
}

预期:这两个文件都建好后,你这个项目里就同时有了「项目级」和「本地级」两层配置。注意——按第 02 节说的,settings.local.json 会被自动 gitignore(下一步验证)。

第四步:确认本地级真被 git 忽略了

git init -q && git status --short

预期:输出里能看到 .claude/settings.json(项目级,待加入版本库),但看不到 .claude/settings.local.json——它被自动忽略了。看到这个差异 = 第 02 节那条「本地级自动 gitignored」在你这儿验证成功。

第五步:进会话,用 /status 验证两层都被读到

claude

进去后敲:

/status

预期:在弹出的状态信息里找到 Setting sources 那一行,它应该列出 Project local settings(可能还有你早先配过的 User settings),具体标签名以实际界面为准。看到你写的层都在列表里 = 那份配置被成功加载了。如果某一层没出现,回第 02 节核对文件路径有没有放错。

第六步:再用 /permissions 交叉确认规则生效

/permissions

预期:能看到刚写的规则——npm run test *git status * 在允许列表里、curl * 在拒绝列表里。特别注意git status * 来自本地级、另外两条来自项目级,但它们全都在生效——这正是第 03 节讲的「权限规则跨层合并、不是覆盖」的活例子。

跑通这六步,你就把 settings.json 最核心的能力亲手验了一遍:分层写、自动隔离私人配置、用 /status 确认加载、用 /permissions 看合并效果。以后配任何一层、任何字段,本质都是这套流程。

💡 一句话总结:动手就练「项目级 + 本地级两份配置 → git status 验本地级被忽略 → /status 看两层都加载 → /permissions 看规则合并」——亲手跑通这条链路,分层和合并这两个最绕的点立刻就通了。


07 小结

这一篇我们把 Claude Code 那块「总配电盘」settings.json 从里到外理清了——从「它是什么」到「分几层、谁压谁、改完怎么验」,把配置这件事规规矩矩安顿明白

把核心要点串起来回顾:

你想搞清的事答案一句话关键点
它跟 CLAUDE.md 啥关系两套东西CLAUDE.md 管「记住什么」,settings.json 管「怎么干活」
有几层、放哪三层用户级 ~/.claude/、项目级 .claude/(进 git)、本地级 .claude/settings.local.json(自动 gitignore)
冲突听谁的越具体越大命令行>本地>项目>用户,Managed 封顶
最反直觉的点数组合并权限规则等数组跨层拼起来去重,不是覆盖
常动哪几个字段五个model/permissions/env/hooks/statusLine
改完怎么验/status看「Setting sources」那行确认你那层真被读到

你现在应该能: 分清 settings.json 和 CLAUDE.md 各管什么、知道一条配置该放用户级还是项目级、看懂「谁压谁」的优先级以及「数组合并不是覆盖」这个反直觉例外、认识 model/permissions/env/hooks/statusLine 这几个高频字段,并且配完会用 /status 确认它真生效。这套「分层配置」的能力,是你把 Claude Code 从「装好能用」调成「贴合你和团队工作流」的那把扳手。

开头那二十分钟的弯路,根子就一句话——没分清「写在哪一层」。这一篇之后,你能少走一大圈:配置不生效时,先别怀疑语法,先想「我是不是放错了楼层」,再敲个 /status 看一眼


下一篇 32「输出样式(Output Styles)」——你刚在 settings.json 里见过一个特殊的 outputStyle 字段,还记得它是「启动时只读一次、要重启才生效」的两个例外之一吗?下一篇就专门讲它:怎么调 Claude 的「说话风格和系统提示」,让它从默认的「干活助手」切换成更适合讲解、教学或别的场景的样子。想想看:同一个 Claude,换一套输出样式,给你的回答风格能差出多远?


32 · 输出样式(Output Styles):换一档「节目」,不换主持人

都说「想让 Claude 听话,就往 CLAUDE.md 里堆」——堆约定、堆规矩、堆它该记住的一切。

说句实话,这话只对了一半。CLAUDE.md 确实是装「项目背景」的好地方(详见第 18 篇),但有一类需求往里塞,是塞错抽屉了

哪一类?「我希望它每次回应的口吻 / 角色 / 格式都变一变」这类。 比如:我想让它每次都先画个图再解释、我想让它一边写代码一边教我为什么这么写、我干脆想拿它当个写作助手而不是程序员。这些需求你写进 CLAUDE.md,它会时灵时不灵——因为 CLAUDE.md 是「附在系统提示之后的一条用户消息」,是给 Claude 看的「请求」,不是改它「本性」的开关。真正改「Claude 怎么说话」的开关,叫 output styles。

这么说吧:CLAUDE.md 是递给新员工的项目资料,output styles 是直接重写他的岗位说明——「你这个岗,是埋头编程的工程师,还是边干边讲的导师」。这一篇就讲清这个开关怎么拨。

举个能让你立刻有感觉的场景:让一个完全不懂代码的人拿 Claude Code 改简历。开着默认的工程师人格,它动不动就想「帮你把这段拆成函数」「要不要加个测试」——满脑子工程思维,跟改简历这事儿完全不在一个频道。这不是 CLAUDE.md 能解决的问题,这就是 output styles 存在的原因。

看完这一篇,你会拿到:

  • 一句话讲明白 output styles 改的到底是什么(剧透:是「怎么回应」,不是「知道什么」)
  • Claude Code 自带的几种内置样式(默认、Proactive、Explanatory、Learning)各是干嘛的、什么时候切
  • 怎么切换样式——以及一个版本变更的坑:老的 /output-style 命令已经没了,现在该怎么切
  • 自定义一个属于你自己的输出样式:Markdown 文件怎么写、那几个 frontmatter 字段各管什么、keep-coding-instructions 这个开关别按错
  • output styles 和 CLAUDE.md、--append-system-prompt、Subagent、Skill 到底差在哪,一张表理清,再不混

01 先搞懂:它改的是「怎么回应」,不是「知道什么」

先把这一篇最核心的一句话钉死,后面全建在它上面:

输出样式改变 Claude 的响应方式,而不是 Claude 知道什么。

这是官方文档的原话。它的意思是:output styles 不会给 Claude 灌进任何关于你项目的新知识,它动的是 Claude 的系统提示(system prompt,即每次会话开头就装进去、定义 Claude「是谁、该怎么干活」的那段底层指令)——给它设定角色、语气和输出格式

类比:同一位主持人,换一档节目就换一套台风。 还是这个人,业务能力没变(嘴皮子、知识储备都是他自己的);但他主持《新闻联播》是一套沉稳腔,主持少儿节目是另一套蹦蹦跳跳的语调,主持教学频道又会放慢节奏、边讲边停下来提问。换的是「这档节目要求他怎么说话」,不是把他换成另一个人。 output styles 就是给 Claude「换节目」——模型还是那个模型,能力一点没动,变的是它跟你对话时的角色、口吻和格式。

那它什么时候派上用场?官方给的判断很实在:

当你在每个回合中不断重新提示相同的语音或格式时,或者当你希望 Claude 充当软件工程师以外的角色时,请使用一个。

(原文如此,这里的「语音」指回应口吻 / 格式。)

翻成大白话,就两种信号:

  • 「同一句要求我每轮都得重打一遍」——比如你每次都得叮嘱「解释的时候先给我画个流程图」,打到第五遍就该想:这不该每轮手打,该固化成一个样式。
  • 「我想让它干的根本不是写代码这件事」——比如拿它当写作助手、当数据分析师。Claude Code 默认那套系统提示是为「高效完成软件工程任务」调的,你让它写小说,那套「先限定改动范围、写好注释、验证工作」的工程指令反而碍事。

回到开篇那个改简历的场景——这正是第二种信号的典型案例:Claude Code 整个默认人格是为软件工程调的,你让它帮人改简历,那套「限定改动范围、写好注释、验证工作」的工程指令全成了干扰。这种「让它别当工程师」的活,靠在 CLAUDE.md 里写一堆「请你忘掉你是程序员」是解决不了的——CLAUDE.md 是附在后面的一条请求,output style 才是改它岗位说明的那把刀。

💡 一句话总结:output styles 动的是 Claude 的系统提示——给它换角色、换语气、换格式,改的是「怎么回应」,不是「知道什么」;两种信号该想起它:同一句话每轮重打、或想让它干编程以外的活。


02 四种内置样式:默认之外,还有三档现成的

不用你自己写,Claude Code 已经内置了几种样式,开箱就能切。先把这几档认全。

默认(Default)样式,就是你前面三十一篇一直在用的那个——它的系统提示是为「高效完成软件工程任务」专门调好的。埋头干活、改动克制、该验证就验证,这是 Claude Code 的「本职岗位」。没特殊需求,就用它。

除了默认,官方还内置了三档额外样式,我挨个用一句话讲清它「跟默认比,多了点啥」:

Proactive(主动型)——更敢自己拿主意、倾向动手而非先规划。 官方的说法是:它会「立即执行,做出合理的假设而不是暂停进行常规决策,并倾向于行动而非规划」。说白了就是少跟你来回确认、少在那儿列计划,遇到常规小决策自己拍板往前推。

这里有个容易混的点,官方专门点了:Proactive 提供的是比自动模式更强的「自主执行指导」,但它无需更改你的权限模式就能用——所以工具运行前你照样会看到权限提示。权限模式(第 20 篇讲的「实习生动手前问不问你」)管的是「要不要拦着问你」,Proactive 管的是「它的行事风格多激进」,两码事,别当成一个。

Explanatory(讲解型)——边干活边给你「知识点」。 它在帮你完成工程任务的同时,会插入一段段教育性的「Insights(洞见)」,帮你理解「为什么这么实现、这个代码库是什么模式」。适合你想顺便搞懂代码、而不只是要个结果的时候。

Learning(学习型)——协作式边学边做,还会留作业给你。 这是最特别的一档。它不光像 Explanatory 那样分享洞见,还会在代码里留 TODO(human) 标记,要求你自己动手写一小段、战略性的代码片段。等于 Claude 把脚手架搭好,关键的几行留给你填——逼着你真上手,而不是全程看它表演。

把这三档落到一句真实的话里,你会更有体感:

  • 同一个需求「给这个列表加个分页」,Proactive 下它大概率直接动手改、少问你「要不要先列个方案」;
  • 同一个需求,Explanatory 下它边改边告诉你「这里为什么用游标分页而不是 offset、这个项目其它地方也是这么写的」;
  • 同一个需求,Learning 下它会把外层框架写好,在最关键那个函数体里留一行 // TODO(human): 在这里实现游标的解析,把笔交到你手上。

四档并排,对照看立刻清楚谁该上场:

内置样式跟默认比,核心区别什么时候切它响应会更长吗
Default(默认)——正常软件工程,绝大多数时候基准
Proactive更敢自己拿主意、少确认、倾向动手你嫌它来回问、想让它放开手脚干不一定
Explanatory边干边插「Insights」讲解想顺便搞懂实现思路和代码库模式更长(设计如此)
Learning讲解 + 留 TODO(human) 让你自己写学习场景,想边做边练手更长(设计如此)

最后这列「响应会更长吗」要单独拎出来提醒:官方明确说,Explanatory 和 Learning 在设计上就比 Default 产出更长的响应——因为要插讲解、要留作业。长响应意味着更多输出 token(关于 token 怎么计费,见第 06 篇)。所以这俩别一直挂着当默认用,想学的时候切上,学完切回 Default,省 token 也省得每次都被一堆讲解刷屏。

一个简单的用法路线:日常干活就用 Default;遇到一个不熟的开源仓库、想边让它改边搞懂它的架构,切 Explanatory;真要静下心学某个新框架、不想光看它写、想自己也敲两行,才切 Learning。Proactive 用得最少的人,往往是更习惯它动手前停一下让人看一眼的——这纯属个人偏好,你嫌它墨迹,完全可以反过来。

💡 一句话总结:内置四档——Default 埋头干活、Proactive 放开手脚少确认、Explanatory 边干边讲、Learning 边讲边留作业给你;后两档响应更长更费 token,按需切、用完切回默认。


03 怎么切换:那个 /output-style 命令,已经没了

这一节得先泼盆冷水,因为这里藏着一个版本变更的真坑——很多老教程、老视频还在教的命令,现在敲下去是无效的。

你在别处大概率会看到这么个说法:「用 /output-style 命令切换样式」。这个独立命令已经被移除了。 官方文档写得清清楚楚:

独立的 /output-style 命令在 v2.1.73 中已弃用,在 v2.1.91 中被移除。使用 /config 或直接编辑 outputStyle 设置。

所以,现在切换样式有两条正路,我都给你写清。

路子一:/config 菜单里点(推荐,直观)

在 Claude 会话里敲 /config,在弹出的菜单里找到输出样式(Output Styles)那一项,选你要的样式。官方原话:

运行 /config 并选择输出样式从菜单中选择一种样式。你的选择会保存到本地项目级别.claude/settings.local.json

注意这句末尾的落点——你在菜单里选的样式,会被写进当前项目的 .claude/settings.local.json。这正好接上一篇 settings.json 的知识:settings.local.json项目级、且只属于你个人、不进版本控制的那一份(第 31 篇讲过它的定位)。换句话说,你切的样式默认只影响你自己、只在这个项目里生效,不会因为提交代码把它强加给队友。

路子二:直接编辑 outputStyle 字段

不想点菜单,也可以手写。在 settings 文件里加一个 outputStyle 字段就行:

{
  "outputStyle": "Explanatory"
}

值就填样式的名字(内置的填 ExplanatoryLearningProactive,或自定义样式的名字)。这个字段写在哪个 settings 文件里,就决定了它的作用范围——想全局默认就写进用户级 ~/.claude/settings.json,想这个项目专属就写进项目级。谁压谁的优先级规则,跟第 31 篇讲的完全一致,这里不重复。

一个必须记住的生效时机

无论哪条路,切完都有个「什么时候才算数」的讲究。官方点得很准:

输出样式是系统提示的一部分,Claude Code 在会话开始时读取一次。更改将在 /clear 或新会话后生效。

类比:节目的台本,是开播前发到主持人手里的。 节目都开播半小时了,你临时塞张新台本进去,这一期是改不动的——得等下一期开播才生效。output style 一样:它是系统提示的一部分,Claude 在会话开始时只读一次。你中途切了样式,当前这段对话不会立刻变身,得 /clear 清一下(第 19 篇讲过 /clear 是「收拾干净台面重开」)或者干脆开个新会话,新样式才真正上场。

很多人头一回切样式就栽在这儿:在 /config 里把样式改成了 Explanatory,回到对话发现它还是闷头干活、半句讲解没有,当场以为「这功能坏了」。折腾半天才反应过来——/clear,旧的系统提示还在这段会话里挂着呢/clear 一下,讲解立刻就出来了。这个坑你记着,能省下当时那十分钟的抓狂。

💡 一句话总结:老的 /output-style 命令已被移除,现在切样式走两条路——/config 菜单里选(存进 settings.local.json)或直接编辑 outputStyle 字段;切完记得 /clear 或开新会话才生效,别像我一样以为它坏了。


04 自定义一个属于自己的样式:一个 Markdown 文件搞定

内置四档不够用?你完全可以自己写一个。好消息是,门槛低得出奇——一个 Markdown 文件就是一个输出样式

官方把结构说得很干脆:

自定义输出样式是一个 Markdown 文件:frontmatter 用于元数据,然后是要添加到系统提示的说明。

拆开就两部分:头部的 frontmatter(用 --- 包起来的元数据)+ 下面的正文(你想追加进系统提示的指令)。下面分三步带你建一个。

第一步:把文件存到对的地方

跟前面学过的扩展点(Skill、Subagent)一样,输出样式也分三个存放级别,存在哪决定了它在哪些项目能选到:

级别存放目录谁能用到
用户级~/.claude/output-styles你所有项目都能选
项目级.claude/output-styles(项目根目录下)仅当前项目,可随项目提交、共享给队友
托管策略级托管设置目录内的 .claude/output-styles由组织统一下发

这个「项目级 vs 用户级」的逻辑,跟第 31 篇讲 settings、第 25 篇讲记忆是同一套思路:自己天天用的、跨项目通用的,放用户级(~/.claude/output-styles);某个项目专属、还想让协作者也能用上的,放项目级(.claude/output-styles),它能跟着 git 走。

还有一条命名规则得记住:

文件名成为样式名称,除非你在 frontmatter 中设置 name

也就是说,你存一个 code-reviewer.md,它的样式名默认就叫 code-reviewer;除非你在 frontmatter 里另写了 name,那就以 name 为准。

第二步:写 frontmatter 和正文

来看官方给的这个例子——一个「每次解释都先画图」的样式。我先原样放上来,再逐字段拆:

---
name: Diagrams first
description: Lead every explanation with a diagram
keep-coding-instructions: true
---

When explaining code, architecture, or data flow, start with a Mermaid diagram showing the structure, then explain in prose.

## Diagram conventions

Use `flowchart TD` for control flow and `sequenceDiagram` for request paths. Keep diagrams under 15 nodes.

上半部分 --- 之间是 frontmatter,下半部分是正文指令(「解释代码、架构或数据流时,先给一个展示结构的 Mermaid 图,再用文字解释……」)。这段正文,会被追加进 Claude 的系统提示——从此它每次解释都会先画图。

frontmatter 支持的字段一共四个,逐个说清:

Frontmatter 字段管什么默认值
name样式名(不写就用文件名)从文件名继承
description样式描述,会显示在 /config 选择器里
keep-coding-instructions是否保留 Claude Code 内置的软件工程指令false
force-for-plugin仅 plugin 用:启用插件时自动套用此样式,无需用户手动选false

前两个好懂——name 是名字,description 是给你自己在菜单里认的一句话说明。后两个得单独讲,尤其 keep-coding-instructions,它是整个自定义样式里最容易按错的开关,下一节专门拆。force-for-plugin 只跟做插件相关(第 24 篇讲过 plugin 能打包分发各种扩展点,输出样式也能被插件带着走),日常自己写样式用不到,知道有这么个字段即可。

第三步:切到你的新样式

存好文件、写好内容,回到 /config 的输出样式菜单里,你的新样式就会出现在选项里(旁边显示你写的那句 description),选中它。一样的规矩——/clear 或开新会话后生效

💡 一句话总结:自定义样式就是一个 Markdown 文件——frontmatter 写元数据、正文写要追加进系统提示的指令;存对级别(用户级跨项目、项目级随项目走),文件名即样式名,写完去 /config 里选、/clear 生效。


05 那个最容易按错的开关:keep-coding-instructions

上一节卖了个关子——keep-coding-instructions 这个 frontmatter 字段,值得单开一节,因为按错它,你的自定义样式效果会差十万八千里

先说它管什么。前面讲过,Claude Code 默认那套系统提示里,塞满了「软件工程指令」——怎么限定改动范围、怎么写注释、怎么验证工作。当你写一个自定义样式时,这套内置指令默认会被「踢掉」。官方原话:

自定义输出样式排除了 Claude Code 的内置软件工程说明……除非 keep-coding-instructions 设置为 true

这个字段默认是 false,意思是:默认情况下,你的自定义样式会把内置工程指令整个换掉,只留你写的那段。是保留还是踢掉,全看你的样式是不是还要 Claude 干编程这件事。判断只需问一句:

「我这个样式,Claude 还在写代码吗?」

  • 还在写代码、只是换个说话方式(比如「编程照旧,但每次都先给我画个图」)→ keep-coding-instructions: true,把内置工程指令留着。上一节那个「Diagrams first」例子就是这种——它要的是「解释时先画图」,但 Claude 仍在正常编程,所以官方给它设了 true
  • 根本不写代码了(比如把它当写作助手、数据分析师)→ 省掉这个字段(让它默认 false),把那套工程指令踢干净。它都不编程了,「限定改动范围、写测试」这些指令留着纯属干扰。

官方把这条判断讲得特别清楚,值得原样记住:

当你改变 Claude 的通信方式但仍在编程时(例如总是用图表回答),请保留它们。当 Claude 根本不进行软件工程时(例如写作助手或数据分析师),请省略它们。

我把这两种情况摆一张对照表,按错的后果一目了然:

你的样式想干嘛keep-coding-instructions 该设按错了会怎样
✅ 编程照旧,只改回应方式(先画图、固定格式…)true(保留工程指令)❌ 设成 false:它丢了「限定范围、验证」这些工程纪律,改起代码毛手毛脚
✅ 完全不编程(写作 / 数据分析 / 翻译…)省略(默认 false❌ 设成 true:满脑子还想着拆函数、加测试,跟你要的写作助手不在一个频道

回到第 01 节那个改简历的场景——根子就在这儿。那时候要的是个「写作助手」人格,根本不该带任何编程指令。如果当时就会写自定义样式、并且省掉 keep-coding-instructions(让工程指令默认踢掉),它就不会动不动想「帮你把这段拆成函数」了。这个开关,本质就是在回答「这副新人格,还兼着工程师那份工吗」。

💡 一句话总结:keep-coding-instructions 默认 false,会把内置工程指令整个踢掉;判断只问一句「这样式 Claude 还编程吗」——还编程就设 true 保留、不编程就省掉它踢干净,按反了人格就跑偏


06 它在底层到底怎么作用:拼进系统提示的那一截

前面几节零散提到「追加进系统提示」「会话开始读一次」「踢掉工程指令」——这一节把这套底层机制一次讲透。搞懂它怎么工作,前面那些「为什么」就全通了:为什么改了样式要 /clear、为什么它影响每一个回应、为什么 keep-coding-instructions 能控制工程指令的去留。

官方把工作原理总结成三条,我逐条翻成你能记住的话:

  • 所有输出样式都在系统提示的末尾添加了自己的自定义说明。
  • 所有输出样式都会在对话期间触发提醒,让 Claude 遵守输出样式说明。
  • 自定义输出样式排除了 Claude Code 的内置软件工程说明……除非 keep-coding-instructions 设置为 true

第一条:你的样式指令,是被拼到系统提示「末尾」的。系统提示是 Claude 每次会话开头就装进去的底层指令,你的 output style 内容追加在它最后——所以它对这段会话里的每一个回应都生效,不是只管某一句。

第二条:对话期间它会反复「提醒」Claude 守住样式。聊久了 AI 容易把开头的设定忘到脑后,这条提醒机制就是定期把样式说明拎出来念一遍,保证它不跑偏。

第三条,正好解释了上一节那个开关:自定义样式默认会把内置工程指令从系统提示里「抽掉」,只留你写的那段keep-coding-instructions: true 则是告诉它「别抽,留着」。这就是为什么这个字段能决定「Claude 还编不编程」——它管的是系统提示里那段工程指令的存废

一张图把「系统提示是怎么被组装出来的」画清楚:

Output Styles 三种样式的系统提示组装:内置 / 自定义保留工程指令 / 自定义抽掉工程指令;改样式后必须 /clear 才生效

这张图说的是:会话一开始,Claude Code 按你选的样式组装系统提示——内置样式和 keep=true 的自定义样式都带着工程指令,keep=false 的自定义样式则把工程指令抽掉只留你的说明;拼好之后,这段提示对本次会话每个回应都生效;而你中途改了样式,得 /clear 或开新会话,它才会拿新样式重新组装一遍(第 03 节那个坑的根源就在这)。

最后提一句 token(计费见第 06 篇)。官方说得实在:

令牌使用情况取决于样式。向系统提示添加说明会增加输入令牌,尽管 prompt caching 在会话中的第一个请求之后会降低这个成本。

翻成大白话:样式说明会进系统提示,多少占点输入 token;好在 Claude Code 有 prompt caching(提示缓存,把不变的系统提示缓存起来重复用),会话里第一次请求之后这点成本就降下来了,不用太焦虑。真正会显著拉高消耗的是 Explanatory、Learning 那种「设计上就产出长响应」的样式(多在输出 token 上),这跟你自己写的样式让 Claude 生成多少东西,是同一个道理——说明越长、让它产出越多,token 越多,按需用就好。

💡 一句话总结:output style 的指令被拼到系统提示末尾、对本段会话每个回应都生效,还会被反复提醒守住;自定义样式默认抽掉内置工程指令(keep=true 才留);改样式要 /clear 重新组装;样式占点 token,但有 prompt caching 兜着,长响应样式才是消耗大头。


07 output styles vs CLAUDE.md / Skill / Subagent:到底差在哪

学到这儿,你脑子里大概率冒出一个问题:这玩意儿跟前面学的 CLAUDE.md、Skill、Subagent,听着都能「定制 Claude 行为」,到底有啥不一样? 这一节一次理清。第 30 篇那张「功能怎么选」的大表是从需求出发挑工具,这里我们专盯着「跟 output styles 怎么区分」来看。

先抓住 output styles 最独一份的那个特征——它是直接改系统提示本身,而且对每一个回应都生效。别的功能要么是「在系统提示之后加一段」,要么是「特定时机才加载」,没一个像它这样动到系统提示的根上。官方那张对比表把这层差别讲透了,我搬过来并各补一句大白话:

功能工作原理什么时候用它(而不是 output style)
输出样式直接修改系统提示,每个回应都套用你要的就是「每轮都换个角色 / 语气 / 默认格式」
CLAUDE.md(第 18 篇)在系统提示之后附加一条用户消息你要灌的是项目约定和代码库背景,不是说话方式
--append-system-prompt往系统提示追加内容、不删任何东西你只想给某一次调用临时加点指令
Subagent(第 23 篇)自己独立的系统提示、模型、工具跑子代理你要把某件专注的活交给一个有独立上下文的小助手,它干完只交结论
Skill(第 26 篇)调用时 / 相关时才加载特定指令你有个可复用的工作流,按需调出

这张表里,新手最该分清的是头两行——output style vs CLAUDE.md。它俩最容易混,因为都「能让 Claude 听你的」。但本质天差地别:

CLAUDE.md 装的是「内容 / 背景」,output style 装的是「方式 / 角色」。 一个回答「这个项目是什么、有哪些约定」,一个回答「你该用什么口吻、什么格式来回应」。再说得直白点,回到开篇那个比喻:CLAUDE.md 是递给员工的项目资料,output style 是给他定的岗位说明。你不会把「我们用 pnpm 不用 npm」写进 output style(那是项目约定,进 CLAUDE.md),也不会把「请你每次都先画图再解释」写进 CLAUDE.md 当成铁律(那是回应方式,该做成 output style)。官方在文档里专门留了一句指路,就是怕你放错:

有关你的项目、约定或代码库的说明,请改用 CLAUDE.md。

至于 --append-system-prompt,它跟 output style 像在「都动系统提示」,但一个是一次性、一个是持久的--append-system-prompt 是你启动某一次 claude 时临时往系统提示后面贴一句,用完即走;output style 是存进配置、每次会话都套上的持久人格。落到命令上就是这个区别——临时试某个风格,你会这么开一次会话:

claude --append-system-prompt "这次回答都用中文、尽量简短"

关掉再开就没了。但如果这个风格你天天要,就别每次手打这串参数,把它沉淀成一个 output style,一劳永逸。临时试一句用前者,固定下来用后者,这就是它俩的分工。

还有 Skill(第 26 篇)也值得一句区分:Skill 是「按需才加载的可复用工作流」,平时不占系统提示,你 /<name> 或 Claude 判断相关时才把它调进来;output style 是「一直挂着、每个回应都套的角色设定」。一个是「需要时才翻出来的菜谱」,一个是「贯穿整场的主持台风」——要某个特定任务的流程做 Skill,要全程都变的角色语气做 output style。

还有一种常见的混淆是另一个方向:想让 Claude「回答风格简洁、少废话」,第一反应是写进 CLAUDE.md。结果它时灵时不灵——因为风格这种东西,本就该靠改系统提示(output style)来定,而不是靠 CLAUDE.md 那条「附加在后面的请求」去提醒。这跟第 30 篇里把「三百行接口塞进 CLAUDE.md」是同一类错误:东西没放进对的抽屉。

💡 一句话总结:output styles 独一份在于直接改系统提示、每个回应都套用;跟它最容易混的是 CLAUDE.md——CLAUDE.md 装项目背景(内容)、output style 装角色语气格式(方式);一次性临时加指令用 --append-system-prompt,要独立作用域的助手用 Subagent。


08 动手:写一个「先画图再解释」的自定义样式并跑通

光看不练假把式。这一节带你从零建一个自定义输出样式、切上去、并亲眼验证它真的生效。我们就做官方那个最经典的「先画图再解释」,全程不依赖你任何已有的复杂项目,随便找个目录就能练。

第一步:建样式文件

我们存到用户级目录(~/.claude/output-styles),这样你以后任何项目都能选到它。先建目录、再建文件:

mkdir -p ~/.claude/output-styles

然后用你顺手的编辑器,在 ~/.claude/output-styles/ 下新建一个文件 diagrams-first.md,填入下面的内容(正文我翻成了中文,方便你读;指令用中文 Claude 一样照办):

---
name: Diagrams first
description: 每次解释都先画一张图,再用文字说明
keep-coding-instructions: true
---

解释代码、架构或数据流时,先给一张展示结构的 Mermaid 图,再用文字解释。

## 画图约定

控制流用 `flowchart TD`,请求路径用 `sequenceDiagram`。每张图节点控制在 15 个以内。

注意 keep-coding-instructions: true——因为这个样式下 Claude 还在正常编程,只是多了个「先画图」的习惯,所以要把内置工程指令留着(第 05 节讲的判断)。

第二步:切到这个样式

进 Claude 会话,敲 /config,进输出样式菜单,应该能看到一个 Diagrams first(旁边显示你写的那句 description),选它。

claude

进去后敲 /config,方向键移到「Output Styles」,回车,选 Diagrams first

预期:菜单里能看到 Diagrams first 这一项,且旁边挂着「每次解释都先画图……」那句描述。看到它在列表里 = 文件被正确识别了。如果没看到,八成是文件没存对目录、或 frontmatter 的 --- 写漏了。

第三步:/clear 让它生效

选完别急着提问——/clear(还记得第 03 节那个坑吗?系统提示会话开始才读一次):

/clear

第四步:提个问题,验证它真先画图了

清完台面,问它一个「需要解释」的问题,看它是不是乖乖先上图:

解释一下用户登录的请求是怎么从前端走到数据库的

预期:它的回答会先甩出一个 Mermaid 图(多半是个 sequenceDiagram,画前端 → 后端 → 数据库的请求路径),然后才用文字解释。看到「图在前、文字在后」这个顺序 = 你的自定义样式生效了

作为对比,你可以 /config 切回 Default、再 /clear、问同一个问题——它就只给文字、不画图了。这一前一后的差别,就是 output style 实打实在改「怎么回应」的铁证。

第五步:清理(可选)

不想留这个样式,直接删文件即可:

rm ~/.claude/output-styles/diagrams-first.md

删完它就从 /config 菜单里消失了。(如果当前正用着它,记得先在 /config 里切回 Default。)

跑通这五步,你就把「建文件 → 切样式 → /clear 生效 → 验证 → 清理」这条完整链路亲手走了一遍。以后做任何自定义样式,无非换换正文指令、按需调 keep-coding-instructions,流程都是这一套。

💡 一句话总结:自定义样式实操就五步——.md 文件、/config 选、/clear 生效、提问验证「图在前文字在后」、删文件清理;切回 Default 问同一问题做对比,差别一眼可见。


09 小结

这一篇我们拆透了 output styles——那个改「Claude 怎么回应你」而非「Claude 知道什么」的开关

把核心要点串起来回顾:

你想干的事怎么做关键点
理解 output styles 是什么改系统提示里的角色 / 语气 / 格式改「怎么回应」,不改「知道什么」
用现成的内置样式Default / Proactive / Explanatory / Learning后两档响应更长更费 token,按需切
切换样式/config 菜单选,或编辑 outputStyle 字段老的 /output-style 命令已移除;切完 /clear 才生效
自定义一个样式写一个 Markdown 文件(frontmatter + 正文)文件名即样式名;存用户级跨项目、项目级随项目走
决定保不保留工程指令keep-coding-instructions还编程就 true、不编程就省掉(默认 false
跟 CLAUDE.md 分清看装的是「方式」还是「内容」角色语气格式→output style;项目背景约定→CLAUDE.md

你现在应该能: 一句话说清 output styles 改的是什么、内置四档分别什么时候切、用 /config(而不是已废弃的 /output-style)把样式切对并知道要 /clear 才生效;自己写一个 Markdown 自定义样式、把 keep-coding-instructions 这个开关按对,还能拎清它跟 CLAUDE.md、Skill、Subagent 的边界。说白了,你手里这下多了一把「给 Claude 换人格」的钥匙——同一个模型,能让它从工程师切成导师、切成写作助手,按活儿换装。

回到开篇那句「想定制就往 CLAUDE.md 里堆」——现在你该清楚了:项目背景往 CLAUDE.md 堆没错,但「换个口吻、换个角色、换种格式」这类「怎么回应」的事,归 output styles 管,放对抽屉,两个都好用。


下一篇 33「钩子(Hooks)」——output styles 是「换 Claude 说话的方式」,但它终究还得靠 Claude 自己「愿意照办」。有没有一种机制,能让某件事雷打不动、不靠 Claude 自觉就自动发生?比如「每次它一改完文件,就自动跑一遍格式化」「某条危险命令必须被硬拦」。这就是 Hook 干的活——事件一触发就执行的自动卡点,前面第 30 篇给你留了个引子,下一篇彻底拆开。想想看:哪些事你已经叮嘱过 Claude 好几遍、却还想要个「保证它每次都做」的硬约束?


33 · 钩子(Hooks):在固定时机自动扣扳机

设想这样一个数字:某一周里,让 Claude 改完代码后,手动补敲 prettier --write 的次数,是 23 次

23 次。同一个动作,机械地重复了 23 遍。更离谱的是中间还漏了两回——提交上去之后被 CI 的格式检查打回来,又得重新跑一遍流程。

这时候就该想一个事:这种「每次都得做、做的内容还一模一样」的动作,凭什么要靠人记着、靠 Claude 自觉? 在 CLAUDE.md 里写过「改完文件记得跑 prettier」,可它三次里总有一次想不起来——因为那只是一句请求,不是保证

而配一个钩子(Hook),一行配置,问题就彻底没了:从那以后,Claude 每编辑完一个文件,格式化就自动跑,再没手敲过一次 prettier,也再没被 CI 打回来过。这一篇,就把这个能「自动扣扳机」的东西,从是什么讲到怎么配、怎么调。

看完这一篇,你会拿到:

  • 一句话讲明白 Hook 是什么、它跟「写进 CLAUDE.md 的请求」差在哪个根本点上
  • Claude 干活的生命周期里,到底有哪些「时机」能挂钩子(PreToolUse / PostToolUse / Stop / SessionStart 等)
  • 钩子配在哪个文件、matcher 怎么把它收窄到「只在改文件时触发」
  • 三个能直接抄走的真实例子:改完自动格式化、拦危险命令、干完发通知
  • 钩子和 Claude 之间靠什么对话(stdin 的 JSON、退出码、stdout)——这是看懂一切的钥匙
  • 钩子不触发、报错时,怎么一步步查出来

01 先搞懂:Hook 到底是什么、强在哪个「保证」上

先给结论:Hook 是「某个事件一发生,就自动执行的一段命令或请求」——它不靠 Claude 思考决定要不要做,触发是有保证的。(最常用的是 shell 命令,此外还支持 HTTP 端点、MCP 工具、LLM 提示等形式。)

官方的定义很干脆,先放这儿:

Hooks 是用户定义的 shell 命令,在 Claude Code 生命周期中的特定点执行。它们对 Claude Code 的行为提供确定性控制,确保某些操作始终发生,而不是依赖 LLM 选择运行它们。

注意里头两个词:「确定性控制」「始终发生」。这就是 Hook 的命门所在。

类比:家里贴的自动化规则(「当……就……」)。 你给智能家居设过那种规则吧——「有人开门,亮灯」「我离家,关掉所有插座」。条件一满足,动作必然发生,不需要谁记得。Hook 就是给 Claude Code 装的这种规则——你定义「某个事件发生,跑这段命令」,它自动执行,雷打不动。

这里要把一个最关键的区别钉死:写进 CLAUDE.md 的是「请求」——它大概率照办,但可能漏;配成 Hook 的是「保证」——只要那个事件触发,动作一定执行,跟 Claude 记不记得没半点关系。官方的话:

CLAUDE.md 或 skill 中的「永远不要编辑 .env」之类的说明是请求,而不是保证。阻止编辑的 PreToolUse hook 是强制执行。

这就是开头那 23 次的根源:「改完跑 prettier」写在 CLAUDE.md 里是请求,三次漏一次;配成 Hook 是保证,一次不漏。

几个你大概率会遇到、值得「上保证」的场景,先感受一下:

  • 「每次改完文件,自动格式化 / 跑 lint」——别再手敲,也别指望它自觉
  • rm -rf、删生产库这种命令,给我硬拦死」——要确定地拦住,不能靠提示
  • 「它干完活、或者等我输入时,给我发条桌面通知」——你好切去干别的,不用盯着终端

💡 一句话总结:Hook 是「事件触发的自动动作」,核心价值是把「请求」变成「保证」——CLAUDE.md 拜托它做的事可能漏,Hook 挂上的事件一触发就必然执行。


02 有哪些「时机」能挂钩子:认识生命周期事件

Hook 不是随便什么时候都能挂,它得挂在 Claude 干活流程里特定的「时机」上。这些时机,官方叫事件(event)。想用好 Hook,第一步就是认清「我想让这事在什么时候发生」。

回想第 03 篇讲的「代理循环」——Claude 干活是「想 → 做 → 看」转圈。这些事件,正好散布在这个循环的前前后后。官方把它们按触发频率分成了三档,这个分法特别好记:

  • 每个会话一次SessionStart(会话开始 / 恢复时)、SessionEnd(会话结束时)
  • 每一轮对话一次UserPromptSubmit(你刚提交提示、Claude 还没开始处理时)、Stop(Claude 答完这一轮时)
  • 代理循环里每次工具调用PreToolUse(某个工具就要执行前)、PostToolUse(某个工具成功执行后

光说有点抽象,画张图你一眼就懂这几个最常用的事件卡在哪儿:

Claude Code 钩子 7 个时机:SessionStart → UserPromptSubmit → Pre/Post 工具循环 → Stop → SessionEnd

这张图把「想→做→看」的循环摊开了:一进会话是 SessionStart,你说话是 UserPromptSubmit,然后进入「要不要用工具」的循环——每次动工具,前面有 PreToolUse、后面有 PostToolUse,循环转完这一轮就是 Stop,整个会话收摊是 SessionEnd你想让动作在哪一步发生,就挂对应的那个事件。

这六个是日常用得最多的。其实官方支持的事件有三十来个(比如压缩前后的 PreCompact/PostCompact、文件落盘变动的 FileChanged、配置被改的 ConfigChange、子代理起停的 SubagentStart/SubagentStop 等),但对小白来说,先把下面这四个吃透,能覆盖九成场景

事件什么时候触发最典型的用法
PreToolUse某个工具执行前拦危险命令、保护敏感文件(能阻止操作
PostToolUse某个工具成功执行后改完文件自动格式化 / 跑 lint
StopClaude 答完这一轮提醒「活还没干完,继续」、扫一遍工作区
SessionStart会话开始或恢复时往上下文里注入项目状态(如最近的提交)

记这张表有个窍门:看名字里的 PrePost——Pre 是「之前」,所以只有它能在动作发生前拦住Post 是「之后」,工具都跑完了,它只能「事后补一刀」(格式化、记日志),拦不了。这个差别下一节细说。

💡 一句话总结:Hook 挂在 Claude 生命周期的特定事件上,按频率分三档(每会话 / 每轮 / 每次工具调用);新手先吃透 PreToolUse(前,能拦)、PostToolUse(后,补刀)、Stop(答完)、SessionStart(开场)这四个就够用。


03 钩子配在哪、matcher 怎么把它收窄

知道了能挂哪些事件,来看怎么写。Hook 写在设置文件(settings.json)里——就是第 31 篇专门讲过的那套配置文件。写在哪个文件,决定了它管多大范围

配在哪个文件生效范围能共享给团队吗
~/.claude/settings.json你所有项目否,只在你这台机器
.claude/settings.json(项目根)仅当前项目,可以提交进 git
.claude/settings.local.json(项目根)仅当前项目否,被 gitignore

这跟第 31 篇讲配置时的「项目档案柜 vs 工位抽屉」是同一套逻辑:全队都该有的 Hook(比如「改完一律格式化」)写进项目的 .claude/settings.json 提交进 git;只是你自己想要的(比如发通知到你的桌面)写进 ~/.claude/settings.json

Hook 配置长什么样

先看一个最小的完整例子——「每次用 Edit 或 Write 改完文件,自动跑 prettier 格式化」,就是治好那 23 次毛病的那段。写进项目根目录的 .claude/settings.json

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

别被这层嵌套吓到,它就三层,对着拆一下你立刻懂:

  1. "PostToolUse"——挂哪个事件(这里:工具执行后)。
  2. "matcher": "Edit|Write"——收窄到哪些工具才触发(这里:只在 EditWrite 工具之后,不在 BashRead 之后)。
  3. 里层的 hooks 数组——真正要跑的动作"type": "command" 表示跑一条 shell 命令,"command" 就是那条命令。

这条命令里的 jq 是个解析 JSON 的小工具(Mac 用 brew install jq 装,Ubuntu 用 apt-get install jq)。它的作用下一节讲——简单说就是从 Claude 递来的数据里,把刚改的那个文件路径抠出来,喂给 prettier。

matcher:让钩子「只在该触发的时候触发」

matcher 是 Hook 配置里最该搞懂的一个字段。一句话:没有它,钩子会在那个事件的「每一次」都触发;有了它,你能把范围收窄

类比:保安的进门规定——不是来送货的,不进大门。 没有 matcher 的钩子就像让保安「任何人来都登记」,效率很低;有了 matcher,就变成「只有快递员来才登记,其他人直接放行」。matcher 干的就是这个「划定触发范围」的活。

对工具类事件(PreToolUse/PostToolUse),matcher 匹配的是工具名。它的写法有三种,看这张表:

你写的 matcher含义例子
"Edit|Write"精确匹配这几个工具(| 是「或」)只在 Edit 或 Write 之后触发
"Bash"精确匹配单个工具只在跑 Bash 命令时触发
"" 或省略匹配所有,每次都触发该事件每次发生都跑

注意:matcher 区分大小写,写成 edit 是匹配不上 Edit 工具的——这是新手钩子不触发最常见的原因之一。

还有一点新手容易忽略:有些事件压根不支持 matcher(比如 UserPromptSubmitStop),因为它们没有「工具名」这种东西可筛,总是每次都触发。给这些事件加 matcher,会被静默忽略。

💡 一句话总结:Hook 写进 settings.json(全局放主目录、项目放 .claude/),配置就三层——事件、matcher、动作matcher 负责把钩子收窄到「只在该触发的工具上触发」,且区分大小写


04 钩子和 Claude 怎么对话:stdin、退出码、stdout

这一节是看懂一切的钥匙。前面那条 jq 命令为什么能拿到文件路径?钩子怎么「拦」住一条命令?答案全在这套「对话机制」里。

机制本身极简单,就三条管道:Claude 把事件数据从 stdin 喂给你的脚本 → 脚本干活 → 脚本用「退出码 + stdout」告诉 Claude 接下来怎么办。逐个拆。

输入:Claude 从 stdin 递给你一坨 JSON

事件一触发,Claude Code 会把这个事件的相关数据,作为一段 JSON 从标准输入(stdin)塞给你的命令。比如 Claude 要跑 Bash 命令时,PreToolUse 钩子收到的大概长这样:

{
  "session_id": "abc123",
  "cwd": "/Users/sarah/myproject",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

看到了吧——Claude 要干什么、用哪个工具、参数是什么,全在里头。第 03 节那条 jq -r '.tool_input.file_path',干的就是从这坨 JSON 里把 tool_input.file_path(要改的文件路径)抠出来。jq 就是专门解析 JSON 的工具,-r 是让它输出纯文本(不带引号)。

输出:用「退出码」告诉 Claude 下一步

脚本干完活,靠退出码(exit code)给 Claude 下指令。这是 Hook 最核心的约定,记住三个数就行

退出码含义效果
0没意见,正常走操作继续(PreToolUse不等于批准,照常走权限流程)
2拦住!操作被阻止;你写到 stderr 的内容会作为反馈递给 Claude,让它调整
其他(如 1)出错了,但不拦操作继续,终端显示一条 hook 报错提示

重点是 exit 2——这是钩子「踩刹车」的唯一方式。注意一个反直觉的坑:

对于大多数 hook 事件,仅退出代码 2 阻止操作。Claude Code 将退出代码 1 视为非阻止错误并继续操作,尽管 1 是传统的 Unix 失败代码。如果您的 hook 旨在强制执行策略,请使用 exit 2

翻成人话:想拦操作,必须 exit 2,不是 exit 1。很多人按 Unix 习惯写了 exit 1,结果钩子「报了错但没拦住」,命令照样跑了。这是第一次写拦截钩子时最容易栽的跟头——脚本明明判断出了危险命令、也打印了警告,但因为顺手写成 exit 1,Claude 该跑还是跑了,吓人一身汗。

还有个细节关系到「能不能拦住」:只有 Pre 类事件能真正拦操作PostToolUse 收到 exit 2 也拦不住——因为工具已经跑完了,木已成舟,它只能把 stderr 显示给 Claude 看。这就呼应了上一节那句「Pre 能拦、Post 只能补刀」。

进阶:用 stdout 返回 JSON,做更细的控制

退出码只能「拦 / 不拦」两档。想要更细的控制(比如拦的同时告诉 Claude 具体原因、或往它上下文里注入一段信息),就改成 exit 0 然后往 stdout 打印一段 JSON。

用退出码 2 配 stderr 来「阻止」,或用 JSON 配退出码 0 来做「结构化控制」。两者不要混用:你退出 2 时,Claude Code 会忽略 JSON。

举两个最常见的 JSON 输出:

PreToolUse 想拦截、并说明理由——用 permissionDecision

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "这条命令会动生产库,禁止执行"
  }
}

permissionDecision 有四个值:"deny"(拦掉,把理由发给 Claude)、"ask"(照常弹权限框问你)、"allow"(跳过权限框直接放行)、"defer"(延迟执行,让工具稍后恢复,适合非交互模式下的异步审批场景)。

这里有条安全红线必须讲清楚,呼应第 20、21 篇的权限与安全:钩子返回 "allow",不能绕过你设置里的拒绝规则。官方原话——

返回 "allow" 跳过交互式提示但不覆盖权限规则。如果拒绝规则与工具调用匹配,即使你的 hook 返回 "allow",调用也会被阻止。

也就是说:Hook 只能「收紧」限制,不能「放松」到超过权限规则允许的范围。这是个很重要的安全设计——它保证了恶意钩子没法靠返回 allow 把你的安全护栏拆了。反过来,PreToolUse 钩子的拦截优先级极高:哪怕你开了 --dangerously-skip-permissions(跳过所有权限),一个返回 deny 的钩子照样能拦住。所以拿 Hook 来强制团队红线,是真·拦得死。

SessionStart 想往上下文里塞点信息——直接往 stdout 打印文本就行(这几个事件特殊,stdout 会被当成上下文喂给 Claude)。比如会话一开就把最近 5 条提交告诉它:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "git log --oneline -5"
          }
        ]
      }
    ]
  }
}

💡 一句话总结:钩子和 Claude 靠三条管道对话——stdin 喂 JSON 进来、退出码下指令、stdout 做精细控制;记死「exit 2 才拦得住(不是 1)」「退出码和 JSON 别混用」「Hook 只能收紧、不能放松权限」这三条,就抓住了七成。


05 三个能直接抄走的真实例子

理论够了,上三个常用、你也能直接抄的钩子。每个都标清楚「挂哪个事件、收窄到哪、配在哪个文件」。

例子一:改完文件自动格式化(PostToolUse

就是治好那 23 次毛病的那个,最实用、零风险,强烈建议每个项目都配上。写进项目根的 .claude/settings.json

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

逻辑:Claude 每次用 Edit/Write 改完文件 → 钩子从 stdin 的 JSON 里抠出文件路径 → 丢给 prettier --write 格式化。从此格式永远统一,不用你操心。把 prettier 换成 eslint --fixgofmtblack 都是一个套路。

例子二:拦掉危险命令(PreToolUse + 脚本)

这个用上了「exit 2 拦截」。命令复杂时,把逻辑写进一个单独的脚本比硬塞进 JSON 清爽得多。

第一步,把脚本存到 .claude/hooks/block-dangerous.sh

#!/bin/bash
# block-dangerous.sh:拦截 rm -rf 这类危险命令
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q "rm -rf"; then
  echo "Blocked: 检测到 rm -rf,已拦截" >&2   # 写到 stderr,会反馈给 Claude
  exit 2                                       # exit 2 = 阻止这次工具调用
fi

exit 0   # 其余命令放行,走正常权限流程

第二步,给脚本加可执行权限(Mac/Linux 必须,否则 Claude 跑不了它):

chmod +x .claude/hooks/block-dangerous.sh

第三步,在 .claude/settings.json 里注册它,挂到 Bash 工具的 PreToolUse 上:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

这里那个 $CLAUDE_PROJECT_DIR 是 Claude Code 提供的环境变量,指向项目根目录——用它拼路径,钩子不管在哪个子目录跑都能找到脚本,比写死的相对路径稳。

⚠️ 这一节回到第 21 篇的安全主线:钩子是用你的完整用户权限跑的 shell,能删你能删的任何文件。官方反复强调,加任何钩子前先审一遍它的命令,尤其别从来路不明的地方抄整段脚本就往设置里塞。

例子三:它需要你输入时,发条桌面通知(Notification

Claude 干到一半要你批准、或者答完等你下一句时,你可能早切去刷别的了。挂个通知钩子,它就主动喊你。这个用 Notification 事件(Claude 发通知时触发)。

macOS 写进 ~/.claude/settings.json(这种「叫我」的钩子是你个人偏好,放全局):

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code 在等你\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

平台差异(官方给的原生命令,照抄即可):

平台通知命令(填进 command
macOSosascript -e 'display notification "..." with title "Claude Code"'
Linuxnotify-send 'Claude Code' '...'
Windows用 PowerShell 的 MessageBox(官方文档有完整片段)

macOS 上如果通知没弹出来,多半是 Script Editor 没拿到通知权限——去「系统设置 → 通知」里找到 Script Editor 把开关打开。第一次配很容易死活没声响,折腾半天才发现是这个权限没给。

💡 一句话总结:三个钩子按风险递增——格式化(PostToolUse,零风险,建议人人配)、拦命令(PreToolUse+脚本,记得 chmod +xexit 2)、发通知(Notification,平台命令不同);脚本路径用 $CLAUDE_PROJECT_DIR 拼最稳。


06 动手:5 分钟配一个钩子并亲眼看它触发

光看不练记不住。下面带你配一个最安全、最容易看到效果的钩子——每次 Claude 跑完 Bash 命令,就把这条命令记进一个日志文件。全程不动你任何代码,纯加一段配置,跑完能亲眼验证。

这个练习用到 jq。没装的话:Mac 跑 brew install jq,Ubuntu 跑 sudo apt-get install jq。装不装得上不需要魔法上网。

第一步:找一个练手目录,打开它的项目设置文件

随便找个空目录(别在重要项目里练),在里头建 .claude/settings.json。如果文件已存在且有别的内容,把 hooks 这块作为新键加进去,别整个覆盖。文件内容:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/claude-bash-log.txt"
          }
        ]
      }
    ]
  }
}

这条钩子:每次 Bash 工具跑完(PostToolUse + matcher: "Bash")→ 从 stdin 的 JSON 里抠出命令 → 用 >> 追加写进主目录的 claude-bash-log.txt

第二步:在这个目录里启动 Claude,确认钩子已注册

claude

进去后输入 /hooks

/hooks

预期:弹出一个只读的钩子浏览器,列出所有事件。找到 PostToolUse,旁边应该显示有 1 个钩子。选中它能看到详情:事件、matcher(Bash)、来源文件(Project,即项目的 .claude/settings.json)、还有那条命令。看到它在列表里 = 钩子注册成功了。

/hooks 菜单是只读的——它只能让你查、不能加和改。要改钩子,直接编辑 settings.json,或者直接让 Claude 帮你改。

第三步:让 Claude 跑条 Bash 命令,触发钩子

Esc 回到对话,让它跑个无害的命令:

帮我用 ls 看一下当前目录有哪些文件

它会调用 Bash 工具跑 ls这一跑,PostToolUse 钩子就该触发了。(钩子成功执行时是「静默」的,终端不会特意提示——这是正常的。)

第四步:验证钩子真跑了——查日志文件

新开一个终端,看日志:

cat ~/claude-bash-log.txt

预期:文件里出现了刚才那条 ls 命令(以及这个会话里 Claude 跑过的其他 Bash 命令)。看到命令被记下来了 = 钩子真的在每次 Bash 后自动触发了,你这条「自动化规则」立起来了。

第五步:清理(可选)

练完拆掉很简单——把 .claude/settings.json 里那段 hooks 删掉就行(钩子没有单独的「删除命令」,从配置里移除条目即可)。日志文件 rm ~/claude-bash-log.txt 删掉。

跑通这五步,你就把「写配置 → /hooks 确认注册 → 触发事件 → 验证副作用」这条完整链路亲手走了一遍。以后配任何钩子,本质都是这套流程,无非换个事件、换个 matcher、换条命令。

💡 一句话总结:练手就配「记录 Bash 命令」这种零风险钩子最稳——写进 .claude/settings.json、用 /hooks 看它注册、让 Claude 跑命令触发、查日志文件验证;亲眼看到副作用发生,比记十条定义都顶用。


07 钩子不触发 / 报错了,怎么查

Hook 配好了不灵,是新手最常见的卡点。别瞎猜,按下面这个顺序查,基本都能定位。我把官方的排查清单整理成了「症状 → 怎么查」:

症状最可能的原因 / 怎么查
钩子压根不触发① 跑 /hooks 看它到底注册了没;② matcher 区分大小写,edit 匹配不上 Edit;③ 事件挂错了(想拦操作要用 PreToolUse,不是 PostToolUse
/hooks 里根本没有我配的钩子① JSON 格式错了(JSON 不允许尾逗号、不允许注释);② 文件位置错了(项目钩子在 .claude/settings.json,全局在 ~/.claude/settings.json);③ 改完没生效就重启一次会话
终端报 hook error脚本意外非零退出。手动测一下(见下方命令);报 command not found 多半是脚本路径不对,用绝对路径或 $CLAUDE_PROJECT_DIR;报 jq: command not found 就是没装 jq
脚本没跑起来Mac/Linux 上脚本忘了加可执行权限,补 chmod +x
想拦却没拦住八成是写了 exit 1,改成 exit 2(见第 04 节那个坑)

两个最实用的排查手段单独拎出来:

① 手动喂假数据测脚本。不用真在 Claude 里触发,自己造一段 JSON 管道喂给脚本,看它退出码对不对:

echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | ./block-dangerous.sh
echo $?   # 看退出码:拦截脚本这里应该输出 2

这是调拦截钩子的标准动作——先把脚本单独喂熟了,再挂到 Claude 上,省得在真实会话里反复试。

② 开调试日志看细节。想看「到底哪些钩子匹配了、退出码多少、stdout/stderr 是啥」,用 --debug 启动,日志会写进 ~/.claude/debug/<会话id>.txt

claude --debug

或者会话里直接敲 /debug 也能开。日志里会有类似这样的行,一眼看出钩子有没有跑、跑成啥样:

[DEBUG] Executing hooks for PostToolUse:Bash
[DEBUG] Hook command completed with status 0

还有个一键开关值得知道:想临时关掉所有钩子(比如怀疑某个钩子在捣乱),在设置文件里加一句 "disableAllHooks": true 就行,不用一个个删。

💡 一句话总结:钩子不灵别瞎猜,按「/hooks 看注册 → 查 matcher 大小写和事件选对没 → 手动喂 JSON 测脚本 → --debug 看日志」的顺序查;想拦没拦住先看是不是写了 exit 1


08 小结

这一篇我们把 Hook 从「是什么」一路讲到「怎么配、怎么调」——它是给 Claude Code 装的「自动化规则」:某个事件一触发,就雷打不动替你跑一段动作

把核心串起来回顾:

你想干的事怎么落地关键点
理解 Hook 是什么事件触发的自动 shell 命令把「请求」变成「保证」,不靠 Claude 自觉
选对挂载时机认生命周期事件Pre 能拦、Post 补刀、Stop 答完、SessionStart 开场
写一个钩子配进 settings.json三层:事件 + matcher(区分大小写)+ 动作
让钩子和 Claude 对话stdin / 退出码 / stdoutexit 2 才拦得住,退出码和 JSON 别混用
拦危险操作PreToolUse + 脚本记得 chmod +x,Hook 只能收紧不能放松权限
钩子不灵了按顺序排查/hooks 看注册、手动喂 JSON 测、--debug 看日志

你现在应该能: 说清 Hook 和「写进 CLAUDE.md 的请求」差在哪个根本点(保证 vs 请求);知道 PreToolUse/PostToolUse/Stop/SessionStart 各自挂在 Claude 干活流程的哪一步;照着模板往 settings.json 里写一个带 matcher 的钩子;看懂钩子靠 stdin/退出码/stdout 跟 Claude 对话,并记死「exit 2 才能拦」;钩子不触发时知道从 /hooks--debug 入手查。开头那 23 次手敲 prettier 的苦差,到这儿你已经有能力用一行配置永久解决了。

Hook 是「系统配置与优化」这一组里很硬核的一块——它让你对 Claude 的行为有了确定性的掌控,而不只是「拜托它」。


下一篇 34「CLI 参考手册:命令与全部标志」——这一路你敲了不少 claude 开头的命令、用了 --debug--dangerously-skip-permissions 这些标志,但它们其实只是冰山一角。下一篇把 claude 命令行的全部命令和标志系统梳一遍,当成一本随手能翻的「字典」。想想看:你现在能脱口而出的 claude 标志有几个?翻完那篇,你会发现自己之前至少漏用了一半能省事的开关