Claude Code 处理 GBK 编码源文件的实战经验:用 hooks 把乱码挡在读取之前
1. 为什么 Claude Code 一读 GBK 源码中文注释就全乱了如果你维护的是 Keil、IAR 或者早期 Windows 下的 C/C 工程大概率会遇到这种情况源码文件本身是 GBK 编码里面躺着几百行中文注释是团队好几年攒下来的知识资产。你兴冲冲把 Claude Code 接进来让它帮忙改个驱动逻辑结果它读出来的注释是ĬϽ这种鬼东西改完写回去原来的中文注释直接永久丢失。这个问题的本质是 Claude Code 的 Read/Edit/Write 工具链默认按 UTF-8 处理文件。磁盘上的 GBK 字节被当成 UTF-8 解码自然出乱码AI 基于乱码理解后输出 UTF-8 内容再写回文件文件就变成了混合编码。整个过程发生了两次编码错误一次在读取阶段一次在写入阶段。我试过最直接的思路让 AI 每次读文件前先手动转码。但问题是 Claude Code 的读取动作是自动触发的你没法在对话里插一句“等一下先转码”。所以真正靠谱的做法是在文件读写的边界上做拦截——也就是 hooks 机制。这篇就围绕 Claude Code、GBK、编码、源文件、hooks 这几个关键词把可复制的配置和探测脚本完整给出来目标只有一个让 GBK 项目在 Claude Code 里稳定可读中文注释不再丢。适合谁看手上维护着 GBK 编码 C/C 工程的嵌入式开发者或者任何被 AI 助手编码问题坑过的同学。下面从环境准备开始一步步来。2. 前置准备TaoToken 接入与 Claude Code 环境确认在配 hooks 之前得先保证 Claude Code 本身能正常跑起来。Claude Code 需要一个可用的模型接入点这里我用 TaoToken 来做接入它的 API 地址是https://taotoken.net/api官网在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。你需要在控制台生成一个 API Key后面配置里会用到。先确认你的 Claude Code 版本。打开终端执行claude --version我这边实测是 2026.6 版本hooks 机制在这个版本上是稳定的。如果你的版本比较老建议先升级因为早期版本的 hook 触发时机和 matcher 语法有差异。接下来确认 Node.js 环境因为 hook 脚本要用 Node 跑还要装iconv-lite做编码转换node -v npm -vNode 建议 18 以上。然后确认你的项目根目录下有没有.claude文件夹没有就手动建一个mkdir -p .claude这里有个关键点Claude Code 的配置文件分几个层级。.claude/settings.local.json是当前项目、个人使用通常会被 gitignore.claude/settings.json是当前项目、团队共享~/.claude/settings.json是全局所有项目。对于 GBK 项目我建议把 hooks 配置放在.claude/settings.local.json因为编码转换脚本的路径往往跟个人电脑相关不适合直接提交给团队。如果你还没配好 Claude Code 的模型接入可以在 TaoToken 控制台拿到 Key 后参考接入文档把 Base URL 和 Key 填进去。接入文档地址是https://taotoken.net/doc里面有不同客户端的配置示例。配好之后先用一个简单对话验证模型能正常响应再往下做编码拦截不然出了问题不好定位是接入问题还是 hook 问题。另外提醒一句hooks 脚本会读写你的源文件动手前务必先提交一次 git或者把整个工程复制一份出来做实验。编码转换这种事一旦脚本逻辑写错可能把文件写坏有版本控制兜底才安心。3. 可复制配置settings.local.json 与编码探测脚本这一节是核心直接给可复制的配置片段。先说思路我们要在 Read 之前把 GBK 文件临时转成 UTF-8让 Claude Code 读到正确的中文在 Write/Edit 之后再把 UTF-8 内容转回 GBK 写回磁盘。这样对 AI 完全透明它始终以为自己在处理 UTF-8 文件。先看.claude/settings.local.json的完整配置。注意路径要换成你自己的实际路径{ hooks: { PreToolUse: [ { matcher: Read, hooks: [ { type: command, command: node C:/Users/admin/.claude/plugins/gbk-tools/gbk_hook.js pre, timeout: 10 } ] } ], PostToolUse: [ { matcher: Write|Edit, hooks: [ { type: command, command: node C:/Users/admin/.claude/plugins/gbk-tools/gbk_hook.js post, timeout: 10 } ] } ] } }这里matcher用的是工具名Read对应读取Write|Edit对应写入和编辑。timeout给 10 秒编码转换本身很快这个值足够。然后是 hook 脚本gbk_hook.js。它需要接收一个参数区分 pre 和 post 阶段并且从环境变量里拿到当前操作的文件路径。Claude Code 会把工具调用的上下文通过环境变量传进来脚本里读取process.env.CLAUDE_FILE_PATH或者从 stdin 解析 JSON。不同版本传参方式略有差异下面这版兼容性比较好const fs require(fs); const path require(path); const iconv require(iconv-lite); const direction process.argv[2]; // pre 或 post // 从 stdin 读取 Claude Code 传入的上下文 let input ; process.stdin.on(data, (chunk) { input chunk; }); process.stdin.on(end, () { let ctx {}; try { ctx JSON.parse(input); } catch (e) { process.exit(0); } const filePath ctx.tool_input ctx.tool_input.file_path; if (!filePath || !fs.existsSync(filePath)) process.exit(0); // 只处理源码类文件 const TARGET_EXTS new Set([.c, .h, .cpp, .hpp, .txt, .ini, .cfg]); if (!TARGET_EXTS.has(path.extname(filePath).toLowerCase())) process.exit(0); const buf fs.readFileSync(filePath); const enc detectEncoding(buf); if (direction pre enc gbk) { const content iconv.decode(buf, gbk); fs.writeFileSync(filePath, content, utf-8); } if (direction post (enc utf-8 || enc utf-8-bom)) { const content buf.toString(utf-8).replace(/^\uFEFF/, ); fs.writeFileSync(filePath, iconv.encode(content, gbk)); } process.exit(0); }); function detectEncoding(buf) { if (buf[0] 0xEF buf[1] 0xBB buf[2] 0xBF) return utf-8-bom; let isValidUtf8 true; let i 0; while (i buf.length) { if (buf[i] 0x80) { i; continue; } if (buf[i] 0xC0) { isValidUtf8 false; break; } const n buf[i] 0xE0 ? 2 : buf[i] 0xF0 ? 3 : 4; if (i n buf.length) { isValidUtf8 false; break; } for (let j 1; j n; j) { if (buf[i j] 0x80 || buf[i j] 0xC0) { isValidUtf8 false; break; } } if (!isValidUtf8) break; i n; } if (isValidUtf8) return utf-8; for (let k 0; k buf.length; k) { if (buf[k] 0x81 buf[k] 0xFE) { if (k 1 buf.length buf[k 1] 0x40 buf[k 1] 0xFE) return gbk; } } return unknown; }探测逻辑分三步先看有没有 UTF-8 BOM再验证是不是合法 UTF-8 序列最后检查 GBK 特征字节首字节 0x81-0xFE次字节 0x40-0xFE。这个顺序很重要因为纯 ASCII 文件既是合法 UTF-8 也符合 GBK 特征先判 UTF-8 能避免误转。装依赖cd C:/Users/admin/.claude/plugins/gbk-tools npm init -y npm install iconv-lite如果你用的是 Cline MCP 或者 Codex配置思路类似但要注意三件套必须写全Base URL、Key、Model ID。Base URL 填https://taotoken.net/apiKey 用控制台生成的Model ID 按你选的模型填。Cline 的 MCP 配置里hooks 的挂载点跟 Claude Code 不同需要单独在 MCP server 配置里加拦截逻辑。4. 验证请求确认读取前已完成转码配置写完必须验证 hook 真的触发了而不是默默失败。验证分两步先单独测脚本再在 Claude Code 里跑真实读取。第一步手动造一个 GBK 文件。用 Python 生成一个带中文注释的 C 文件content // 这是驱动初始化函数 // 作者王工 void drv_init(void) { // 配置寄存器 } with open(test_gbk.c, wb) as f: f.write(content.encode(gbk))然后手动跑 pre 阶段的脚本模拟 Claude Code 的输入echo {tool_input:{file_path:test_gbk.c}} | node gbk_hook.js pre跑完之后用file命令或者十六进制查看确认文件已经变成 UTF-8file test_gbk.c如果输出里带UTF-8说明 pre 转换成功。再用编辑器打开中文注释应该正常显示。第二步在 Claude Code 里做真实读取。启动 Claude Code让它读这个文件请读取 test_gbk.c 并告诉我里面的中文注释内容如果配置生效Claude Code 读到的应该是正确的中文而不是乱码。这一步很关键因为脚本单独能跑不代表 hook 被正确挂载。如果 Claude Code 读出来还是乱码说明 hook 没触发回到第 5 节排查。第三步验证写回。让 Claude Code 修改注释把 test_gbk.c 里的作者改成李工然后保存改完后用十六进制工具确认文件编码又变回了 GBKxxd test_gbk.c | head -5如果中文部分显示的是 GBK 双字节比如0xD7 0xF7这种说明 post 阶段转换成功。整个链路跑通后你的 GBK 项目在 Claude Code 里就能稳定读写了。这里补充一个验证模型接入是否正常的小技巧如果读取时 Claude Code 报模型无响应先确认 TaoToken 的 Key 和 Base URL 没问题可以在模型对话页面https://taotoken.net/chat里发一条测试消息确认账号和模型都正常再回来查 hook。5. 常见报错排查401、local proxy failed 与乱码残留配 hooks 的过程里报错基本集中在几类。下面按真实遇到的错误对照排查。401 Unauthorized这个通常跟 hook 无关是模型接入的 Key 问题。检查.claude/settings.local.json或者环境变量里的 API Key 是否正确Base URL 是不是https://taotoken.net/api。如果 Key 过期或者额度用完也会报 401。去控制台https://taotoken.net/api-keys重新生成一个换上。local proxy failed这个报错说明 Claude Code 在尝试连接本地代理时失败。先确认你没有配多余的代理环境变量比如HTTP_PROXY、HTTPS_PROXY。如果有清掉再试。另外确认 hook 脚本的路径是对的路径里如果有空格或者反斜杠转义问题Node 会找不到脚本表现也可能是连接类错误。reading choices 相关报错这类错误一般是模型返回格式解析失败常见于接入点返回了非预期结构。确认你用的 Model ID 跟接入点支持的模型一致。如果是在 Cline 里检查 MCP 配置的 Model ID 有没有写错。乱码残留如果读取时部分中文正常、部分乱码说明编码探测误判了。可能是文件里混了 UTF-8 和 GBK 两种编码或者探测逻辑把某些字节序列判错了。这时候可以临时把探测逻辑改成强制按 GBK 处理先保证能读再慢慢清理混合编码的文件。hook 完全不触发最常见的原因是 matcher 写错或者配置文件放错了位置。确认.claude/settings.local.json在项目根目录JSON 格式合法可以用node -e require(./.claude/settings.local.json)验证。另外确认 Claude Code 版本支持你写的 hook 语法。OAuth 相关报错如果你用的是需要 OAuth 的接入方式token 过期会报这个。重新走一遍授权流程或者换成 API Key 方式接入。排查时有个通用方法在 hook 脚本开头加一行日志把收到的上下文写到临时文件里确认脚本到底有没有被调用、收到的 file_path 对不对。这比猜要快得多。6. 长期使用建议与接入入口跑通之后有几个实践建议。第一把 hook 脚本放在项目内而不是个人目录路径用相对路径或者环境变量这样团队其他人拉下来也能用。第二.claude/settings.local.json里的个人路径记得 gitignore团队共享的配置放.claude/settings.json。第三如果项目里 GBK 文件不多其实可以考虑一次性全量转 UTF-8一劳永逸但改动大需要团队和工具链配合存量大的项目还是 hooks 拦截更稳妥。性能上每次 Read/Write/Edit 都会触发 hook大概 50 到 100 毫秒开销日常开发感觉不到但批量操作几百个文件时会有累积延迟心里有数就行。如果你还没接入 Claude Code可以从这几个入口进模型对话在https://taotoken.net/chatAPI Key 管理在https://taotoken.net/api-keys接入文档在https://taotoken.net/doc。长期做编码和 Agent 任务的可以看看 Coding Plan地址是https://taotoken.net/coding-plan。Claude Code 相关的配置参考在https://taotoken.net/claude-code-anthropic。最后说个我踩过的坑一开始我把 hook 脚本里的文件扩展名白名单漏了.h结果头文件里的中文注释还是乱码排查了半天才发现是白名单问题。所以配置完一定要拿.c和.h都测一遍别只测源文件。