恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Claude Code Mods 实战:用 JS/TS 和 hook 打造终端增强工具
首页
资讯中心
/
Claude Code Mods 实战:用 JS/TS 和 hook 打造终端增强工具
Claude Code Mods 实战:用 JS/TS 和 hook 打造终端增强工具
发布时间:2026/10/9 12:43:46
1. Claude Code Mods 到底在折腾什么第一次听到 “Claude Code Mods” 这个词很多人会以为是给 Claude 装插件、换皮肤或者像浏览器扩展那样点一下就能用。实际接触下来你会发现它更像是一套围绕 Claude Code 这个终端工具做“外挂式增强”的思路一边给 Claude 加工具让它能调用外部脚本、读写文件、跑命令另一边在终端里画界面把原本纯文本的交互变成有状态、有布局、有反馈的 TUI。核心关键词绕不开Claude Code、JS、TS、hook因为绝大多数 mods 都是用 JavaScript 或 TypeScript 写的而 hook 机制是它们能挂进 Claude Code 生命周期的关键。我最初是在一个终端里用 Claude Code 写脚本写着写着发现它只能“对话”不能主动帮我跑测试、不能把结果渲染成表格、更不能在终端里弹出一个可交互的面板。后来看到有人用 JS 写 hook在 Claude Code 执行工具前后插入自己的逻辑甚至用 TS 写了一个终端 UI 组件库把输出变成带边框、带高亮、带滚动区域的面板。那一刻我才意识到Claude Code Mods 不是官方插件市场而是一群人在现有能力边界上“手动扩权”的实践集合。这篇文章适合谁看如果你已经在用 Claude Code但觉得它只能聊天、不能深度参与你的工作流那这篇就是写给你的。如果你还没装 Claude Code也没关系我会把安装、配置、hook 挂载、JS/TS 模块编写、终端界面绘制这些环节拆开讲尽量让不同基础的人都能跟着复现。需要提前说明的是下面涉及的具体参数和目录结构一部分来自我自己的实测一部分是基于常见 Node.js 工具链实践的合理补全你实际跑的时候以自己环境为准。2. 整体设计思路为什么是 hook JS/TS 终端 UI2.1 为什么选 hook 作为切入点Claude Code 本身是一个终端里的 AI 编程助手它执行任务时会经历若干阶段接收用户输入、决定调用哪个工具、执行工具、拿到结果、再决定下一步。hook 的价值就在于它允许你在这些阶段之间插入自己的代码。你可以把它理解成一条流水线上的“质检工位”原料进来时你可以改半成品出来时你可以看成品打包前你还能再加工。为什么不用别的方式比如直接改 Claude Code 的源码或者写一个 wrapper 脚本包住它。改源码的问题很明显升级一次就冲突一次维护成本极高。Wrapper 脚本倒是解耦但它只能控制输入输出没法感知 Claude Code 内部“决定调用哪个工具”这种中间状态。hook 刚好卡在中间它不侵入核心又能拿到足够多的上下文。用 JS/TS 写 hook 还有一个好处Node.js 生态里有大量现成的库解析 JSON、操作文件、发 HTTP 请求都是一行代码的事写起来比 shell 脚本舒服得多。2.2 为什么用 JS/TS 而不是 Python 或 Go这个问题我被问过很多次。Python 当然也能写 hookGo 编译出来更快但 JS/TS 在这个场景里有几个很实际的优势。第一Claude Code 本身就跑在 Node.js 运行时上你不需要额外装解释器环境是现成的。第二TS 的类型系统在写 hook 时特别有用因为 hook 的输入输出往往是结构化的 JSON类型定义能帮你提前发现字段拼写错误。第三终端 UI 这块JS 有 blessed、ink 这些成熟的 TUI 库用 TS 写组件化的终端界面体验接近写 React。我自己的选择是逻辑简单的 hook 用 JS 写追求快速迭代需要长期维护、多人协作的 mods 用 TS 写类型即文档。下面这张表是我对不同语言写 hook 的对比你可以根据自己的情况选。维度JSTSPythonGo环境依赖Node 自带需编译步骤需 Python 环境需编译类型安全弱强中强终端 UI 生态丰富丰富一般一般上手速度快中快慢适合场景一次性脚本长期维护 mods数据处理高性能工具2.3 终端画界面的核心诉求很多人觉得终端就是黑底白字画界面是多此一举。但当你用 Claude Code 处理复杂任务时纯文本滚屏会带来两个问题一是信息密度低一个表格要滚好几屏二是状态不直观你不知道当前是在“思考”还是在“执行”。终端 UI 要解决的就是这两点用布局把相关信息聚在一起用颜色和边框区分状态。我试过几种方案。最简单的就是用 ANSI 转义码手动控制光标和颜色优点是零依赖缺点是写复杂布局会疯掉。进阶一点用 blessed它提供了类似 GUI 的组件模型盒子、列表、进度条都有现成的。再往上就是 ink用 React 的写法写终端界面组件复用和状态管理都很舒服但引入的依赖也最多。我的建议是如果只是给输出加个边框和高亮ANSI 就够了如果要做一个带滚动、带输入框的交互面板直接上 ink别在 blessed 上浪费时间。3. 核心细节解析hook 机制与 JS/TS 模块编写3.1 hook 的触发时机与数据流Claude Code 的 hook 不是只有一个而是按生命周期分了好几种。常见的有用户提交输入前触发的、工具调用前触发的、工具调用后触发的、以及会话结束时触发的。每种 hook 拿到的数据不一样能做的事情也不一样。比如“工具调用前”的 hook你能拿到即将执行的工具名和参数可以在这里做校验、做替换、甚至直接拦截“工具调用后”的 hook你能拿到执行结果可以在这里做格式化、做日志、做二次加工。数据流大致是这样的Claude Code 在某个阶段准备好一份 JSON 上下文通过标准输入传给 hook 脚本hook 脚本处理完后把结果通过标准输出返回。如果 hook 返回了非零退出码Claude Code 会认为这个 hook 失败了可能会中断当前流程。这个设计很关键它意味着你的 hook 既能“观察”也能“干预”。我一开始只把 hook 当日志用后来发现可以在工具调用前动态改参数比如把测试命令里的路径替换成当前工作目录省了很多手动调整。注意hook 脚本的标准输出必须是合法的 JSON或者至少是 Claude Code 能解析的格式。如果你在 hook 里 console.log 了一堆调试信息很可能会污染输出导致解析失败。调试信息请写到标准错误或者写到临时文件里。3.2 用 JS 写一个最小可用的 hook先看一个最简单的例子。假设我想在每次工具调用后把工具名和执行耗时追加到一个日志文件里。用 JS 写大概是这样#!/usr/bin/env node const fs require(fs); const path require(path); let input ; process.stdin.on(data, chunk { input chunk; }); process.stdin.on(end, () { try { const ctx JSON.parse(input); const logLine ${new Date().toISOString()} tool${ctx.toolName} duration${ctx.duration}ms\n; fs.appendFileSync(path.join(process.cwd(), claude-hook.log), logLine); process.stdout.write(JSON.stringify({ ok: true })); } catch (err) { process.stderr.write(hook error: ${err.message}\n); process.stdout.write(JSON.stringify({ ok: false })); } });这段代码做了三件事读标准输入、解析 JSON、写日志并返回结果。看起来简单但有几个坑我踩过。第一标准输入是流式的不能假设一次 data 事件就能拿到完整数据必须等 end 事件。第二JSON.parse 一定要包在 try-catch 里因为上游传过来的数据可能不完整或者格式不对。第三返回的 JSON 里最好带一个 ok 字段方便 Claude Code 判断 hook 是否成功。3.3 用 TS 写 hook 的类型定义技巧JS 写小脚本没问题但一旦 hook 逻辑变复杂类型就很重要了。我一般会先定义一个 hook 上下文的接口interface HookContext { sessionId: string; toolName?: string; toolArgs?: Recordstring, unknown; toolResult?: unknown; duration?: number; cwd: string; } interface HookResponse { ok: boolean; message?: string; modifiedArgs?: Recordstring, unknown; }有了这个定义写处理逻辑时编辑器会提示你有哪些字段可用避免拼错。TS 编译成 JS 后hook 脚本本身还是 Node 能跑的。我的做法是在项目里建一个hooks/目录每个 hook 一个 TS 文件用 tsc 或者 esbuild 编译到dist/hooks/然后在 Claude Code 配置里指向编译后的 JS 文件。这样开发时享受类型检查运行时又是纯 JS没有额外依赖。3.4 终端 UI 的绘制原理与选型终端 UI 的本质是通过 ANSI 转义序列控制光标位置、颜色、清屏等。你看到的“边框”其实是用┌─┐│└┘这些字符拼出来的“高亮”是设置了前景色和背景色“滚动区域”是通过限制光标移动范围实现的。理解这一点很重要因为它意味着终端 UI 没有真正的“像素”一切都是在字符网格上做文章。选型上我前面提过 ANSI、blessed、ink 三条路。这里补充一个实际对比如果你要画一个固定布局的面板ANSI 手写大概 50 行blessed 大概 20 行但要多装一个依赖ink 大概 15 行但要多装 React 和 ink 两个依赖。如果你的 mods 只是给自己用依赖多点无所谓如果要分发给别人依赖越少越好。我现在的习惯是一次性工具用 ANSI长期用的交互面板用 ink中间态用 blessed。4. 实操过程从安装到跑通第一个 mod4.1 安装 Claude Code 与确认 Node 环境安装 Claude Code 的前提是你机器上有 Node.js建议版本不低于 18。你可以用node -v确认。安装命令通常是全局安装npm install -g anthropic-ai/claude-code装完后用claude --version确认。如果提示找不到命令大概率是 npm 全局 bin 目录不在 PATH 里。这时候你可以用npm config get prefix看看全局目录在哪然后把它加到 PATH。我在 Ubuntu 上遇到过这个问题加完 PATH 后重新开一个终端就好了。提示如果你在安装时遇到权限报错不要直接用 sudo 装全局包那样后续升级容易出权限问题。更稳妥的做法是配置一个用户级的 npm prefix或者用 nvm 管理 Node 版本。4.2 配置 hook 的挂载点Claude Code 的 hook 配置一般放在项目根目录或者用户主目录下的配置文件中。具体字段名可能随版本变化但思路是一样的你要告诉 Claude Code在哪个阶段、执行哪个脚本。一个典型的配置片段长这样{ hooks: { afterToolUse: [ { command: node, args: [./dist/hooks/log-tool.js] } ], beforeToolUse: [ { command: node, args: [./dist/hooks/validate-args.js] } ] } }这里的关键是command和args要能拼出一个可执行的命令。我建议用绝对路径或者相对于项目根目录的路径避免因为工作目录变化导致找不到脚本。配置改完后重启 Claude Code 会话让它生效。4.3 写一个“工具调用前校验”的 hook光写日志 hook 没什么意思我们做一个能实际干预流程的。假设我不希望 Claude Code 执行任何包含rm -rf的命令可以在 beforeToolUse 的 hook 里做拦截#!/usr/bin/env node let input ; process.stdin.on(data, c input c); process.stdin.on(end, () { const ctx JSON.parse(input); const argsStr JSON.stringify(ctx.toolArgs || {}); if (argsStr.includes(rm -rf)) { process.stdout.write(JSON.stringify({ ok: false, message: 检测到危险命令已拦截 })); process.exit(1); } process.stdout.write(JSON.stringify({ ok: true })); });这个 hook 返回 ok:false 并退出码 1Claude Code 就会知道这次工具调用被拒绝了。实测下来拦截是生效的Claude Code 会收到拒绝信息并尝试换一种方式。这个模式可以扩展成很多规则禁止访问某个目录、禁止发送网络请求、强制给命令加超时参数等等。4.4 用 ink 画一个终端状态面板接下来做终端 UI。先装依赖npm install ink react然后写一个简单的面板组件const React require(react); const { render, Box, Text } require(ink); const Panel ({ toolName, status }) ( React.createElement(Box, { borderStyle: round, borderColor: status running ? yellow : green, paddingX: 1 }, React.createElement(Text, null, 工具: ${toolName}), React.createElement(Text, null, 状态: ${status}) ) ); render(React.createElement(Panel, { toolName: bash, status: running }));这段代码会在终端里画一个圆角边框的盒子里面显示工具名和状态。你可以把这个渲染逻辑放到 hook 里根据工具执行前后的状态切换颜色。实际用起来视觉反馈比纯文本强很多尤其是长时间运行的任务你能一眼看出当前在干什么。4.5 把 hook 和 UI 串起来单独跑 hook 和单独跑 UI 都不难难的是串起来。我的做法是在 afterToolUse 的 hook 里先解析上下文然后调用 UI 渲染函数把结果画到终端上。注意UI 渲染会占用标准输出所以你不能同时用标准输出返回 JSON 给 Claude Code。解决办法是把 UI 画到标准错误或者用一个单独的 TTY 设备。我试过画到标准错误Claude Code 不会解析标准错误所以不会冲突终端里也能正常显示。注意如果你的 hook 既要返回 JSON 又要画 UI一定要把 UI 输出和 JSON 输出分开。标准输出留给 JSON标准错误留给 UI这是最省心的做法。5. 常见问题与排查技巧实录5.1 hook 不生效的排查顺序hook 配了但没反应是最常见的问题。我一般按这个顺序查第一确认配置文件路径对不对Claude Code 可能读的是用户级配置而不是项目级配置第二确认脚本路径能不能被解析手动在终端里跑一遍node ./dist/hooks/xxx.js看报不报错第三确认脚本有没有可执行权限虽然用 node 调用不需要执行权限但路径拼错一样会失败第四看 Claude Code 的日志通常会有 hook 执行失败的提示。这四步走完九成问题都能定位。5.2 JSON 解析失败的典型原因hook 报 JSON 解析错误通常是因为上游传过来的数据不是你以为的格式。可能的原因有Claude Code 版本升级后字段名变了、你的 hook 被调用的时机不对、标准输入里混入了其他内容。我的应对方法是先在 hook 里把原始输入写到文件里看一眼真实数据长什么样再写解析逻辑。不要凭想象写字段名一定要看实际数据。5.3 终端 UI 乱码或错位终端 UI 画出来乱码一般是字符编码或者终端不支持某些转义序列。先确认终端是 UTF-8 编码然后确认你用的边框字符在当前字体下能正常显示。错位问题通常是光标控制没配对比如你移动了光标但没有移回来。用 ink 或 blessed 这类库能避免大部分错位因为它们帮你管理了光标状态。如果手写 ANSI记得每次绘制前先清屏或者保存/恢复光标位置。5.4 性能问题的取舍hook 是在关键路径上执行的如果 hook 本身很慢会拖慢整个 Claude Code 的响应。我见过有人在 hook 里做网络请求结果每次工具调用都要等好几秒。我的建议是hook 里只做轻量级操作重活放到后台任务里。如果确实需要网络请求加超时和缓存。终端 UI 的渲染频率也要控制不要每来一个字符就重绘一次那样 CPU 会飙高。问题现象可能原因排查方法解决方向hook 完全不执行配置路径错误检查配置文件位置改用绝对路径JSON 解析失败字段名不匹配打印原始输入按实际数据改解析UI 乱码编码或字体问题换终端测试用 UTF-8 和常见字符响应变慢hook 逻辑太重计时各阶段耗时异步化或加缓存拦截不生效退出码不对确认 exit code返回非零并输出原因5.5 版本升级后的兼容处理Claude Code 更新比较频繁hook 的上下文结构可能会变。我的经验是不要把字段名硬编码在多个地方抽一个适配层出来所有 hook 都通过适配层拿数据。这样升级后只需要改适配层。另外升级前先备份配置文件升级后跑一遍所有 hook 的冒烟测试。我一般会写一个test-hooks.sh把每个 hook 用样例数据跑一遍确认输出正常。6. 我踩过的坑和几条实用建议第一个坑是标准输出的污染。我一开始在 hook 里用 console.log 打调试信息结果 Claude Code 解析 JSON 失败整个流程卡住。后来改成 console.error问题就没了。这个教训是在 hook 里标准输出是“接口”标准错误是“日志”两者不能混。第二个坑是路径问题。我用相对路径配置 hook在项目根目录跑没问题换到子目录跑就找不到脚本了。后来全部改成基于__dirname或者环境变量拼绝对路径再也没出过问题。第三个坑是 UI 和 JSON 抢标准输出。前面提过解决办法是 UI 走标准错误。但标准错误在某些终端里会被重定向所以更稳妥的做法是判断当前是不是 TTY是 TTY 才画 UI不是就只输出 JSON。几条建议hook 脚本尽量保持无状态需要状态就写文件或者用环境变量TS 项目一定要配好编译输出目录别把源码和编译产物混在一起终端 UI 的配色要考虑深色和浅色终端别只用一种颜色最后mods 这种东西先跑通最小闭环再逐步加功能一上来就搞大而全很容易烂尾。这个方向后续还能怎么扩展我目前在做的是把 hook 和外部工具链打通比如在工具调用后自动触发代码格式化、自动跑单元测试、把结果汇总成一个终端面板。再远一点可以做一个 mods 的加载器扫描目录自动注册 hook省去手动改配置。这些等我跑稳了再单独写一篇。