Claude Code 最近越来越多人问,可大多数人把它当成一个“终端里的聊天机器人”用:输入问题,等回答,复制代码,走人。但真正让它值回票价的东西,是底部那个/命令体系和扩展机制。今天想聊的Mods,就是我在终端里折腾 Claude Code 时,觉得最该被写清楚的一块——它解决两件事:让 Claude 能调用你机器上的真实工具,以及在纯文本终端里画出像样的界面。
这篇内容会从“Mods 到底是什么、怎么运作”讲起,再到手搓一个带仪表盘的系统监控 Mod、一个把 JSON 查询变成工具调用的 Mod,最后把我在实操中踩过的坑和排查思路一起整理出来。适合已经装好 Claude Code、想让它在本地干实活的人阅读,也适合对“终端 UI + AI 自动化”感兴趣的朋友参考。
1. 先弄清楚:Mods 到底解决了什么问题
1.1 一个现实痛点:AI 会话的“调用”和“展示”两头堵
用过 Claude Code 一段时间后,你会发现一个尴尬的事实:它在对话里理解能力很强,但默认状态下,它只能读你贴给它的内容,或者主动去读项目里的文件。如果你想让它帮你跑一条 shell 命令、查一下本机 CPU 占用、把 JSON 数据整理成表格,它的第一反应往往不是动手,而是“教你怎么做”——因为它没有权限,也没有感知你机器真实状态的通道。
另一个问题是展示。终端里两样东西最难受:复杂数字和层级关系。让 AI 输出一段密密麻麻的 JSON 或者几十行日志,看起来跟天书没区别。人眼本来就适合看条状图、表格、色块,不适合盯数字矩阵。Mods 的思路,就是同时解决这两件事:给 Claude 挂上“工具”,让它可以真执行命令;再给它一套“界面能力”,让它在终端里渲染出结构化视图。
1.2 Mods 不是插件,是一种更轻的“功能开关”
很多用户第一反应是“Mods 是不是类似 VS Code 插件那种东西?” 可以说像,但不完全一样。插件通常是一段常驻进程,有自己的生命周期和 UI 层;Mods 更像是“按需加载的能力套件”——由元信息(名字、描述、权限声明)、参数定义和一组具体的实现函数组成。Claude Code 启动时,会扫描这些 Mod,把它们注册进自己的工具列表里,等对话里需要时再调出来用。
这种设计带来的好处很明显:
- 不需要重启整个 Claude Code,改动 Mod 文件后重新加载即可。
- 权限边界清楚:哪些工具可以执行、执行什么命令、要不要先问用户,声明里写明白。
- 组合方便:你可以同时启用多个 Mod,彼此之间没有强耦合。
1.3 适合谁来用、能解决什么场景
我实际用下来,Mods 的价值在三个场景尤其突出:
- 日常开发提效:让 Claude 直接查服务状态、跑测试、解析 nginx 日志,而不是复制粘贴出去自己跑。
- 数据可视化:把内存、磁盘、API 返回值等数据变成终端里的进度条、仪表盘、表格,一目了然。
- 把 AI 接入本地工具链:比如让 Claude 调
jq、ffmpeg、git,相当于给它接了一双手。
如果你是那种“能不打字就不打字、能在终端里完成绝不切窗口”的人,Mods 大概率能切中你的需求。
2. 核心机制拆解:Mods 的组成结构和加载逻辑
2.1 一个标准 Mod 的三个基本构成
先说组成。一个标准的 Mod 通常包含下面三部分:
- 元信息块:描述这个 Mod 叫什么、干什么、有哪些权限需求。Claude Code 会读取这段信息来决定何时唤起它、是否要求用户授权。
- 参数声明:告诉模型“我这个工具接受哪些输入、每个输入是什么类型”。参数写得越清楚,模型调用时越不容易出错。
- 实现处理器:真正执行任务的函数。它接收参数,做处理,然后把结果以字符串形式返回。
举个最简结构示例,一个理论上可行的 Mod 骨架大概是这样的(伪代码示意):
export const myMod = { name: "系统状态", description: "查询 CPU、内存、磁盘占用,返回可视化文本", parameters: { type: "object", properties: { disk: { type: "boolean", description: "是否显示磁盘信息" } } }, execute: async (params: { disk?: boolean }) => { // 实际执行命令、采集数据、拼装渲染字符串 return renderDashboard(params); } };Claude Code 在启动时或用户主动调用/mods相关命令时,会把这些工具注册到会话上下文中。模型根据你的自然语言描述理解“该用哪个工具”,然后按参数声明填充参数,触发执行。
2.2 终端“画界面”的本质:ANSI 控制序列
很多人听到“在终端里画界面”第一反应是不可置信——终端不是只能一行行输出文字吗? 是的,但终端支持 ANSI 转义序列。通过控制字符可以做四件最核心的事情:
- 移动光标(把光标定位到屏幕任意行、列)。
- 修改颜色和样式(前景色、背景色、加粗、闪烁)。
- 清除屏幕区域(整屏清空、行清除、区域清除)。
- 切换备用屏幕(应用全屏前保存当前画面,退出后恢复原样)。
比如\x1b[31m表示“之后的文字用红色”,\x1b[2J表示“清空整个屏幕”。把这些序列拼在一个长字符串里输出,终端就会渲染出带颜色的图形。
界面框架常见的做法,是用制表符配合半角/全角字符,加上 Unicode 的方块字符(█░)、制表符(│─┌┐)、甚至 emoji 风格符号,用对齐方式构成“伪图形”。像blessed-contrib、tui-rs这类库,本质上就是在帮你生成这些控制序列。
Claude Code 的 Mod 里做界面,不需要引入重量级前端框架,只需要在渲染函数里拼接好这些字符串,保持宽度对齐,模型就能在纯文本环境里输出“像 UI”的东西。
2.3 渲染链路:从模型理解到终端显示的完整路径
我来还原一条完整的链路,方便你理解每一步到底发生了什么:
- 你在终端里输入一句要求,例如“看看当前系统负载,画个历史趋势图”。
- Claude Code 的模型收到文本,根据 Mod 的描述和参数声明,判断“应该调用 system 趋势 Mod”。
- 模型生成工具调用消息,Claude Code 的本地运行时接管,检查权限配置。
- 如果允许,运行时执行 Mod 里对应的
execute函数(内部可能是os命令或读取/proc文件)。 - 函数返回一串已经拼好的 ANSI 字符串——包含进度条、数据行、颜色标记。
- Claude Code 把这段返回值作为“工具结果”塞回对话,模型根据结果组织一段自然语言回复,配合工具结果一起输出。
- 终端解析 ANSI 字符串,渲染出最终你看到的界面。
这个链路中最关键的是第 4、5 步,因为数据的采集和渲染都发生在这两步。如果你发现界面没法用或数据不对,定位也主要在这两块。
3. 实操:从零做一个能画界面的 Mod
3.1 准备环境与确认 Claude Code 版本
动手之前,先确认本机的环境。Mods 实际落地对操作系统没太多要求,macOS、Linux、Windows(借助 WSL 更顺)都能跑。但有一点硬性要求:Claude Code 的版本要支持工具注册机制。建议先升级到最新版再往下走。
在终端里确认版本:
claude --version然后确认配置目录存在。通常配置文件在~/.claude/下,里面会有settings.json和插件/扩展目录。如果目录不存在,先启动一次 Claude Code 让它初始化:
claude初始化后退出,再去看目录结构,应该能看到~/.claude/被创建好了。
3.2 创建一个系统状态 Mod:从命令到仪表盘
我挑一个“系统状态监控”做示例,因为它足够简单、又直观。
先写一个纯 Node.js 脚本,模拟核心渲染逻辑。它要做的事:读取 CPU 平均负载、内存占用率,然后生成几行“进度条 + 数字”的文本界面。
// sysinfo.js const os = require("os"); function bar(value, max = 100, width = 20) { const filled = Math.round((value / max) * width); const empty = width - filled; return "█".repeat(filled) + "░".repeat(empty); } function render() { const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; const memPercent = Math.round((usedMem / totalMem) * 100); const load = os.loadavg(); const lines = [ "\x1b[36m系统状态仪表盘\x1b[0m", "内存占用 " + bar(memPercent) + " " + memPercent + "%", "1分钟负载 " + bar(load[0], 4) + " " + load[0].toFixed(2), "5分钟负载 " + bar(load[1], 4) + " " + load[1].toFixed(2), "15分钟负载 " + bar(load[2], 4) + " " + load[2].toFixed(2), ]; return lines.join("\n"); } console.log(render());先在普通终端里运行一下:
node sysinfo.js如果输出能看到蓝色标题、彩色的进度条,说明这段代码在你的终端里可用。这一步是“先证明渲染能力可行,再接入 Claude Code”,避免把调试复杂度叠加在一起。
3.3 用边框字符拼出结构化面板
进度条只是第一步。真正有“界面感”,需要加上边框和分块。你可以用 Box Drawing 字符拼出卡片效果:
┌─────────────────────────────┐ │ 系统状态仪表盘 │ │ CPU 负载 ████████░░ 78% │ │ 内存占用 ██████░░░░ 62% │ └─────────────────────────────┘实现起来也很简单,就是在每行前后拼接│,顶部用┌─,底部用└─。需要注意的是中文字符宽度问题:终端里中文占两个英文字符宽度,做对齐计算时要按“显示宽度”算,否则边框会错位。
一个小技巧:使用Array.prototype.padEnd配合“按字符宽度计算”的函数来保证对齐,而不是按字符串长度直接填充。下面是一个简化但对齐逻辑正确的实现:
function padWidth(str, totalWidth) { const len = [...str].reduce((n, ch) => n + (ch.charCodeAt(0) > 255 ? 2 : 1), 0); return str + " ".repeat(Math.max(0, totalWidth - len)); }3.4 把渲染脚本封装成 Mod 并接入 Claude Code
现在把脚本包成一个 Mod。实际操作时,你可以把它放在~/.claude/mods/目录下,并以“一个目录一个 Mod”的方式组织。
标准结构大概是这样:
~/.claude/mods/sys-dashboard/ ├── mod.json └── run.jsmod.json里写元信息和参数声明:
{ "name": "sys-dashboard", "description": "查看当前系统的 CPU、内存、负载状态,返回终端仪表盘", "permissions": { "commands": ["node"], "allow": [] }, "parameters": { "type": "object", "properties": {}, "required": [] } }run.js就是把上面的渲染逻辑封装成一个函数,并输出结果:
module.exports = async function () { // 同前面的 render() 逻辑 return render(); };接好之后,在 Claude Code 里输入:
/load sys-dashboard或者如果你已经通过配置文件启用了目录扫描,直接问它“看下当前系统负载,给我画个仪表盘”,模型会感知到这个工具的存在,自动调用。这就是“给 Claude 加工具 + 在终端画界面”两个诉求同时落地的过程。
注意:不同版本对 Mod 目录的扫描方式和配置字段可能不同。如果你用的是较新版本,以官方文档中
~/.claude/settings.json里的enabledPlugins、enabledMods这类字段为准;我写的是通用结构,配置项名称因版本而异,别当作唯一标准生搬硬套。
4. 实操:给 Claude 加一个“外部工具型” Mod
4.1 工具型 Mod 的设计思路:把终端命令变成结构化调用
如果说上面是“展示型 Mod”,这一节要做的,就是“工具型 Mod”:让 Claude 能执行命令、拿结果去继续推理。
核心思路是:不要把“执行一条命令”的权限直接丢给模型,而是设计一个窄接口,限定它的使用边界。比如“解析 JSON 文件并返回指定字段”,比“允许运行任意 shell 命令”安全得多。模型不需要无限权限,它只需要把你的常用操作封装好。
4.2 实战:做一个 JSON 查询 Mod
我日常整理数据经常跟 JSON 打交道,就拿这个举例。做一个名叫json-query的工具 Mod,它做一件事:接收文件路径和 jq 查询表达式,返回筛选结果。
它的execute核心逻辑,在 Node 里可以写成:
const { execFile } = require("child_process"); const util = require("util"); const execFileAsync = util.promisify(execFile); module.exports = async function (params) { const { filePath, query } = params; try { const { stdout, stderr } = await execFileAsync("jq", [query, filePath], { timeout: 5000, maxBuffer: 1024 * 1024 }); if (stderr) return "警告信息:" + stderr; return stdout.slice(0, 2000); } catch (err) { return "执行失败:" + err.message; } };它的参数声明里明确要求两个字段:filePath和query,模型在判断“用户想查 JSON 的某一字段”时,就会自动填好参数并调用。
4.3 在 Claude Code 中调用自定义 Mod
接入后,你可以这样跟 Claude 对话:
> 帮我看看 data.json 里所有用户的邮箱模型会判断这属于json-query工具的职责,自动调用:
json_query({ filePath: "./data.json", query: ".[].email" })工具返回结果后,Claude 再结合结果组织一次符合你问题的回复。整个过程里,用户只看到“我问了一句话,它给了答案”,背后是工具调度、命令执行、超时控制、异常捕获这一整套逻辑在跑。
我给所有工具型 Mod 都加了三个默认规则,这里也分享一下:
- 超时时间必须有。默认 5 秒,拿不准就设 30 秒,避免 AI 把整个终端卡住。
- 错误信息要捕获并转成文本返回,不要让异常直接把会话打断。
- 返回结果做长度截断。一次返回几十兆,模型上下文直接炸掉,而且没有意义。
5. 常见问题与排查技巧实录
5.1 界面输出乱码或错位
这个是最常见的坑。通常有三类原因:
第一类,终端不支持 Unicode。用 Windows 自带的旧版控制台跑带方块字符的东西,大概率乱码。建议换 Windows Terminal 或者 iTerm2,macOS 自带的 Terminal 也可以,但 iTerm2 对 ANSI 支持更完整。Ghostty 也可以,实测渲染速度不错。
第二类,字体宽度计算错误。做对齐时,半角、全角混排导致边框错位。解决办法就是我前面提过的,写一个按显示宽度计算的padWidth函数,不要用.length属性。
第三类,ANSI 控制序列被转义。如果终端里看到一堆字面量\x1b[32m而不是绿色文字,说明你的输出把反斜杠当普通字符处理了。排查时先看字符串拼接部分,确认用的是\x1b而不是\\x1b。
5.2 权限弹窗反复出现
Claude Code 对命令执行有权限确认机制。你可能会发现每次 Io 要跑node都弹一次确认窗口,很烦。
实际上这是保护机制,不建议完全关闭。折中做法是在配置文件里添加白名单,把常用命令加入allow列表:
{ "allow": ["node", "jq", "git status"] }配置后,命中白名单的命令不再弹窗,其他命令仍然需要确认。这样既顺畅,又不至于把整个系统完全暴露。
5.3 工具执行超时或命令找不到
如果你配好的 Mod 一直报“超时”或者“找不到命令”,优先级最高的排查方向是 PATH 环境变量。Claude Code 启动时可能没有继承你 shell 里的完整 PATH,特别是装了 nvm、pyenv 这类工具的人,非常容易踩这个坑。
解决办法:在 Mod 里用绝对路径调用核心命令,或者在配置环境变量时单独指定。比如:
const JQ_PATH = "/usr/local/bin/jq";另外一个常见情况:命令执行超时但不是因为命令慢,而是因为权限确认在等用户输入,而这个交互被判定成了“等待超时”。遇到这种现象,先检查这段命令是不是被拦截了。
5.4 快速定位问题:日志与 Verbose 模式
我在调试 Mods 时最常用的工具,是 Claude Code 的 verbose 模式。
claude --verbose开启后,工具调用的请求负载、执行结果、上下文都会被打印出来。你能直观看到“模型到底传了什么参数”“工具返回了什么内容”,95% 的问题到这里都能定位。另外,注意查看~/.claude/logs/下的日志文件,有详细的执行记录,排查崩溃类问题很有用。
给一份我常用的快速排查表:
| 症状 | 最可能的原因 | 优先尝试的解决手段 |
|---|---|---|
| 输出看到原始转义符 | 字符串拼接出错 | 检查是否误写了双反斜杠 |
| 界面边框错位 | 中英文宽度混排 | 改用基于显示宽度的填充函数 |
| 权限弹窗频繁 | 未加入白名单 | 在 settings.json 的 allow 数组补充 |
| 找不到命令 | PATH 未继承完整 | 改为绝对路径调用 |
| 工具总是超时 | 命令等待人工确认 | 检查是否有命令被拦截弹窗 |
| 渲染异常且无报错 | 终端能力较弱 | 换 Windows Terminal / iTerm2 |
5.5 一个小技巧:把渲染逻辑和 Mod 逻辑分开测试
我在实际开发里有个习惯,所有“界面渲染”相关代码都先在纯 Node 环境里跑一遍,确认输出正确后,再接到 Claude Code 里做端到端验证。这一步能把问题域切得非常干净:
- 纯脚本里大量报错 = 渲染代码本身的问题。
- 纯脚本里正常、进了 Claude Code 就乱 = 加载或者上下文传输的问题。
这不只是省事,更是把“AI 会话的不可控性”从调试环节里剔除掉。你需要先确定自己的工具没问题,再去怀疑模型的理解和调度。
最后再分享一个我自己的体会:Mods 这个东西,最适合的场景永远是“高频的、可以被结构化的操作”。一次性任务没必要做成 Mod,反复要做的才值得。我目前给自己的工作流配了一个系统状态面板、一个日志追踪工具、一个 JSON 数据探索器,就这么三个,覆盖了日常八成需求。做得多不如做得准,把工具边界定义清晰,比给 AI 一堆模糊的万能接口高效得多。你上手以后会慢慢感觉到:Mod 写得好不好,不在于代码炫不炫,而在于能不能让 Claude 在正确的时候调用正确的东西。