Claude Code 接入纯文本大模型:API Error 400 Model only support text input 彻底解决方案
目录
- 问题现象
- [根因分析:为什么 CLAUDE.md 不够](#根因分析:为什么 CLAUDE.md 不够)
- [方案设计:PreToolUse 钩子](#方案设计:PreToolUse 钩子)
- 实现细节
- 工作原理流程
- 测试验证
- 生效条件与重启
- 边界与限制
- 维护与回退
- 文件清单
- 附录
问题现象
Claude Code 通过环境变量接入 GLM 模型(glm-5.2[1m],经 Volcengine Ark API),该模型仅支持文本输入,不具备多模态能力。
当 Claude Code 调用 Read 工具读取图片文件时,出现报错:
Let me visually verify the composite images look correct.
Read layout_7classes.png
API Error: 400 Model only support text input
Request id: 02178...
模型尝试"目视检查"生成的组合图像,调用 Read 读取 .png,harness 将图片字节当作多模态内容打包发送给模型,模型拒绝并返回 400。
根因分析:为什么 CLAUDE.md 不够
此前已在 ~/.claude/CLAUDE.md 中加入软提示:
markdown
## 注意
当前使用的大模型不支持多模态功能,请不要上传图片。
该提示无效,原因在于"软"与"硬"的区别:
| 层级 | 谁来执行 | 是否可靠 |
|---|---|---|
| CLAUDE.md 软提示 | 模型自己读上下文后"自觉"遵守 | ❌ 不可靠 |
| PreToolUse 钩子 | Claude Code 本体在工具执行前本地拦截 | ✅ 确定拦截 |
关键点:
- CLAUDE.md 靠模型自觉,一旦模型决定调用 Read 读图片,提示就被绕过。
- 真正把图片字节当作多模态内容发给模型的是 Claude Code 本体(harness),而非模型。harness 不读 CLAUDE.md,只认工具调用。
- 只要模型调用 Read 读图片,就必然触发 400。必须在**"工具调用真正执行之前"**由本地代码硬性阻断------PreToolUse 钩子的职责。
方案设计:PreToolUse 钩子
选型理由
| 方案 | 可靠性 | 依赖模型自觉 | 选用 |
|---|---|---|---|
| CLAUDE.md 软提示 | ❌ | 是 | 否(仅作备份提醒) |
| PreToolUse 钩子(本地 shell) | ✅ 确定拦截 | 否 | ✅ |
钩子是 Claude Code 本体在每次工具调用前/后本地执行的 shell 命令,与后端模型无关 (无论接的是 GLM 还是 Claude 都会执行)。通过 exit 2 即可阻断工具调用,并将 stderr 作为反馈发回给模型。
完整拦截策略(三道闸)
单纯拦截 Read 工具读图片文件还不够,实际存在三条路径会让图片进入上下文:
| # | 路径 | 触发场景 | 拦截方式 |
|---|---|---|---|
| ① | Read 工具读图片/PDF | 模型调用 Read xxx.png 或 Read xxx.pdf |
PreToolUse Read → 检查扩展名,exit 2 |
| ② | MCP 截图工具 | 模型调用 mcp__chrome-devtools__take_screenshot 等 |
PreToolUse *screenshot* → 无条件 exit 2 |
| ③ | MCP 工具返回图片结果 | 某些 MCP 工具在返回值中内嵌 base64 图片块 | PostToolUse mcp__* → 扫描结果含图片标记则 block |
三道闸缺一不可,构成完整防线:
┌──────────────────────────────────┐
│ 模型决定操作 │
└──────────┬───────────────────────┘
│
┌──────────────┴──────────────┐
│ PreToolUse │
│ (工具执行前拦截) │
│ │
│ ① Read + 图片/pdf → exit 2 │
│ ② *screenshot* → exit 2 │
└──────────────┬──────────────┘
│ 通过
┌──────────────┴──────────────┐
│ 工具实际执行 │
│ (Read / MCP 等) │
└──────────────┬──────────────┘
│ 返回结果
┌──────────────┴──────────────┐
│ PostToolUse │
│ (工具执行后拦截结果) │
│ │
│ ③ mcp__* 含图片块 → block │
└──────────────┬──────────────┘
│ 通过
┌──────────────┴──────────────┐
│ 结果发给模型 ✅ │
└─────────────────────────────┘
实现细节
闸①:拦截 Read 读图片/PDF
文件:~/.claude/hooks/block_images.ps1
powershell
# PreToolUse hook: block Read calls on image files.
# Why: the current model chain is text-only. Reading an image packs its bytes as
# multimodal content into the API request, triggering
# "API Error: 400 Model only support text input".
# Behavior: on image extension, exit 2 to block; stderr is fed back to the model,
# guiding it to fetch info via text instead.
#
# NOTE: Keep this file ASCII-only. Windows PowerShell 5.1 parses BOM-less .ps1 as
# ANSI/GBK, so any non-ASCII byte here would corrupt the script.
# Claude Code passes hook input on stdin as JSON:
# {"tool_name": "Read", "tool_input": {"file_path": "/path/to/file.png"}}
$stdin = [Console]::In.ReadToEnd()
if ([string]::IsNullOrWhiteSpace($stdin)) { exit 0 }
# Parse failure -> pass through, never false-positive block.
try {
$data = $stdin | ConvertFrom-Json
} catch {
exit 0
}
# Only intercept the Read tool.
if ($data.tool_name -ne "Read") { exit 0 }
$filePath = $data.tool_input.file_path
if ([string]::IsNullOrWhiteSpace($filePath)) { exit 0 }
# No extension -> pass through.
$ext = [System.IO.Path]::GetExtension($filePath)
if ([string]::IsNullOrEmpty($ext)) { exit 0 }
$ext = $ext.TrimStart('.').ToLowerInvariant()
$imageExts = @(
'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp',
'tiff', 'tif', 'svg', 'ico', 'heic', 'heif', 'avif'
)
$docExts = @('pdf')
if ($imageExts -contains $ext) {
[Console]::Error.WriteLine(
"Blocked by PreToolUse hook: reading image file (.${ext}) is not allowed " +
"because the active model is text-only. Do not retry reading the image. " +
"Get info via text instead (e.g. use PowerShell System.Drawing to read " +
"image size/pixel stats), or ask the user to inspect the image."
)
exit 2
}
if ($docExts -contains $ext) {
[Console]::Error.WriteLine(
"Blocked by PreToolUse hook: reading PDF file (.${ext}) not allowed " +
"(text-only model). Use pdftotext to extract text:" +
" pdftotext `"${filePath}`" - (stdout)" +
" or pdftotext `"${filePath}`" out.txt`r`nDo not retry Read on the PDF."
)
exit 2
}
exit 0
关键实现点
- 输入来源 :Claude Code 通过 stdin 传入 JSON,形如:
{"tool_name": "Read", "tool_input": {"file_path": "/path/to/file.png"}} - 字段解析 :用
ConvertFrom-Json解析,失败时安全放行(exit 0) - 扩展名提取 :
[System.IO.Path]::GetExtension()取扩展名,.ToLowerInvariant()统一小写 - 图片扩展名 :
png、jpg、jpeg、gif、webp、bmp、tiff、tif、svg、ico、heic、heif、avif - 文档扩展名 :
pdf(PDF 会被 harness 逐页渲染成图片发给模型,同样触发 400) - 阻断机制 :
exit 2让 Claude Code 阻断该次工具调用,stderr 内容作为反馈发回模型 - Windows 关键坑 :PowerShell 5.1 把无 BOM 的 UTF-8 .ps1 当 GBK 解析,中文会乱码弄断字符串。脚本必须保持 ASCII-only(英文),stderr 经 GBK 代码页回传也可能乱码,所以反馈信息也用英文。
闸②:拦截 MCP 截图工具
文件:~/.claude/hooks/block_screenshots.ps1
powershell
# PreToolUse hook: block MCP screenshot tools.
# Why: the active model chain is text-only. MCP screenshot tools (e.g.
# chrome-devtools take_screenshot) return image blocks in their tool RESULT.
# Unlike file Reads, those images are NOT intercepted by the Read hook, and
# they get packed as multimodal content into the API request, triggering
# "API Error: 400 Model only support text input".
# Behavior: always exit 2 (block) with an English hint. The model should fall
# back to text-based inspection (take_snapshot / evaluate_script).
#
# NOTE: Keep this file ASCII-only. Windows PowerShell 5.1 parses BOM-less .ps1
# as ANSI/GBK, so any non-ASCII byte here would corrupt the script.
[Console]::Error.WriteLine(
"Blocked by PreToolUse hook: screenshot tools are disabled because the " +
"active model is text-only (their image results would trigger API 400). " +
"Use text-based inspection instead: chrome-devtools take_snapshot for the " +
"accessibility tree, or evaluate_script to query computed styles / hit tests. " +
"Do not retry the screenshot tool."
)
exit 2
要点
- 这个脚本不需要解析 stdin ,因为它在
settings.json中注册时已经用matcher: "*screenshot*"精确匹配了截图工具 - 只要匹配到工具名包含
screenshot(如mcp__chrome-devtools__take_screenshot),直接exit 2阻断 - 引导模型改用
take_snapshot(无障碍树)或evaluate_script(查询计算样式)等文本方式
闸③:兜底拦截 MCP 返回的图片结果
文件:~/.claude/hooks/block_image_results.ps1
powershell
# PostToolUse hook: strip any MCP tool result that carries image content.
# Why: safety net on top of the PreToolUse blockers (Read images, screenshot
# tools). Any MCP tool that returns an image block in its result would
# otherwise inject multimodal content into the API request and trigger
# "API Error: 400 Model only support text input" on a text-only model.
# Behavior: scan the raw stdin JSON for base64 image markers; if found, output
# JSON with decision=block so the image result is hidden from the model.
#
# NOTE: Keep this file ASCII-only. See block_images.ps1 header.
$stdin = [Console]::In.ReadToEnd()
if ([string]::IsNullOrWhiteSpace($stdin)) { exit 0 }
# Claude Code serializes the hook input as one outer JSON object, so inner
# quotes inside tool_response are escaped as \" in the raw stream. Unescape that
# one level so markers below match both the raw and the decoded form.
# (base64 never contains a double quote, so this replacement is safe.)
$norm = $stdin -replace '\\"', '"'
$markers = @(
'"type":"image"',
'data:image/',
'data:image;',
'"mimeType":"image',
'"mimetype":"image'
)
$hit = $false
foreach ($m in $markers) {
if ($norm.Contains($m)) { $hit = $true; break }
}
if (-not $hit) { exit 0 }
$reason = "Blocked by PostToolUse hook: this tool returned an image block. " +
"The active model is text-only, so the result was hidden to avoid API 400. " +
"Rely on text results only (take_snapshot / evaluate_script)."
$out = @{
hookSpecificOutput = @{
hookEventName = "PostToolUse"
decision = "block"
reason = $reason
}
} | ConvertTo-Json -Compress
[Console]::Out.WriteLine($out)
exit 0
要点
- 这是 PostToolUse 钩子(在工具执行之后 、结果返回模型之前执行)
- 不阻断工具本身,而是把包含图片的结果藏起来 (
decision=block) - 扫描 stdin 中的 base64 图片标记:
"type":"image"、data:image/、"mimeType":"image等 - 关键坑 :Claude Code 会对 JSON 内层引号做
\"转义,需要先-replace '\\"','"'反转义一层,否则标记匹配不到 - 这是安全网(safety net),兜住前两道闸没拦住的漏网之鱼
注册到全局 settings.json
文件路径:~/.claude/settings.json
json
{
"env": { "...": "..." },
"includeCoAuthoredBy": false,
"permissions": { "...": "..." },
"effortLevel": "xhigh",
"theme": "dark",
"autoCompactEnabled": true,
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -NonInteractive -File \"C:\\Users\\<用户名>\\.claude\\hooks\\block_images.ps1\""
}
]
},
{
"matcher": "*screenshot*",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -NonInteractive -File \"C:\\Users\\<用户名>\\.claude\\hooks\\block_screenshots.ps1\""
}
]
}
],
"PostToolUse": [
{
"matcher": "mcp__*",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -NonInteractive -File \"C:\\Users\\<用户名>\\.claude\\hooks\\block_image_results.ps1\""
}
]
}
]
}
}
关键点
matcher精确匹配对应的工具名:"Read":只拦截 Read 工具,不影响 Bash / Edit / Write 等"*screenshot*":通配符匹配所有名称含screenshot的工具"mcp__*":通配符匹配所有 MCP 工具(用于 PostToolUse 兜底)
command用powershell -NoProfile -NonInteractive -File显式调用,不依赖执行策略- 放在全局 settings 而非项目级,跨项目生效
工作原理流程
未拦截时(触发 400)
模型调用 Read("xxx.png")
→ harness 读图片字节
→ 当多模态内容打包发送给 API
→ 纯文本模型拒绝 → 400 Error ❌
三道闸拦截后
模型决定操作
│
┌─────────────┴─────────────┐
│ PreToolUse 检查 │
│ │
│ ① Read + .png → 闸①命中 │
│ exit 2 阻断 │
│ stderr 反馈给模型 │
└─────────────┬─────────────┘
│
┌─────────────┴─────────────┐
│ 模型改用文本方式 │
│ (take_snapshot / │
│ evaluate_script / │
│ Python PIL 读属性) │
└─────────────┬─────────────┘
│
┌─────────────┴─────────────┐
│ PreToolUse 检查 │
│ ② *screenshot* → 闸② │
│ 命中 → exit 2 阻断 │
└─────────────┬─────────────┘
│
┌─────────────┴─────────────┐
│ 工具执行 (如 evaluate) │
└─────────────┬─────────────┘
│ 返回结果
┌─────────────┴─────────────┐
│ PostToolUse 检查 │
│ ③ 扫描含图片标记 → 闸③ │
│ 命中 → decision=block │
└─────────────┬─────────────┘
│
┌─────────────┴─────────────┐
│ ✅ 纯文本结果发给模型 │
│ 不触发 400 │
└───────────────────────────┘
测试验证
闸①:Read 图片/PDF 拦截测试
通过模拟 stdin JSON 直接测试脚本逻辑(4 种场景全通过):
| 测试场景 | 输入 | 预期 | 实际 |
|---|---|---|---|
读 .png |
{"tool_name":"Read","tool_input":{"file_path":"/tmp/test.png"}} |
阻断,exit 2 | ✅ exit=2,输出引导文案 |
读 .py |
{"tool_name":"Read","tool_input":{"file_path":"/home/jie/train.py"}} |
放行,exit 0 | ✅ exit=0,无输出 |
读 .pdf |
{"tool_name":"Read","tool_input":{"file_path":"/tmp/doc.pdf"}} |
阻断,exit 2 | ✅ exit=2,提示用 pdftotext |
| 非 Read 工具 | {"tool_name":"Bash","tool_input":{"command":"ls"}} |
放行,exit 0 | ✅ exit=0 |
Windows 复现命令:
powershell
echo '{"tool_name":"Read","tool_input":{"file_path":"/tmp/test.png"}}' | powershell -NoProfile -NonInteractive -File ~\.claude\hooks\block_images.ps1; echo "exit=$LASTEXITCODE"
闸②:截图工具拦截测试
截图钩子无条件阻断,只需确认注册正确:
powershell
# 直接运行脚本,应 exit 2 并输出提示
powershell -NoProfile -NonInteractive -File ~\.claude\hooks\block_screenshots.ps1; echo "exit=$LASTEXITCODE"
闸③:MCP 图片结果兜底测试
模拟含 base64 图片的 MCP 返回结果:
powershell
# 模拟含图片标记的 MCP 返回值
$json = '{"tool_name":"mcp__chrome-devtools__take_screenshot","tool_input":{},"tool_response":"{\"type\":\"image\",\"data\":\"iVBORw0KGgoAAAANSUhEUg...\"}"}'
echo $json | powershell -NoProfile -NonInteractive -File ~\.claude\hooks\block_image_results.ps1
# 应输出含 decision=block 的 JSON 到 stdout
settings.json 合法性校验
bash
# 确认三个钩子都已注册
jq -e '.hooks.PreToolUse | length' ~/.claude/settings.json
# 应输出 2(两个 PreToolUse 钩子)
jq -e '.hooks.PostToolUse | length' ~/.claude/settings.json
# 应输出 1(一个 PostToolUse 钩子)
生效条件与重启
钩子在会话启动时加载。修改 settings.json 或新增钩子后,当前会话不会立即生效,必须重启 Claude Code。
操作步骤:
- 退出当前 Claude Code 会话
- 重新打开 Claude Code
- 在新会话中验证
边界与限制
三道闸覆盖了绝大部分路径,但仍有残余局限:
| 场景 | 是否拦截 | 说明 |
|---|---|---|
Read 读 .png / .jpg / ... |
✅ 拦截 | 闸① |
Read 读 .pdf |
✅ 拦截 | 闸①(PDF 会被 harness 逐页渲染成图片发给模型) |
Read 读 .ipynb |
✅ 放行(正确) | 以单元格/文本形式读取 |
MCP 截图工具(take_screenshot) |
✅ 拦截 | 闸②无条件阻断 |
| 任意 MCP 返回 base64 图片 | ✅ 拦截 | 闸③扫标记兜底 |
| 用户直接拖拽/粘贴图片到 Prompt | ❌ 不拦截 | 不经过任何工具,图片直接作为多模态内容进入上下文 |
| 模型直接引用 URL 加载图片 | ❌ 不拦截 | 不经过 Read 工具 |
残余局限说明
用户直接拖拽/粘贴图片进对话是唯一无法通过钩子拦截的路径。图片文件被拖拽后直接作为多模态消息内容进入上下文,不经过任何工具调用,因此 PreToolUse 和 PostToolUse 钩子都拦截不到。
如果再遇到 400 错误,大概率是历史对话中还残留了图片块(之前被拖拽进去的),执行 /compact 或 /clear 丢掉历史即可。
PDF 替代方案
纯文本模型读取 PDF 应使用命令行工具提取文本(已在闸①的 PDF 拦截提示中引导模型使用):
bash
pdftotext input.pdf - # 输出到 stdout
pdftotext input.pdf out.txt # 输出到文件后用 Read 读 .txt
维护与回退
新增/移除拦截扩展名
编辑 ~/.claude/hooks/block_images.ps1 中的 $imageExts 或 $docExts 数组:
powershell
$imageExts = @(
'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp',
'tiff', 'tif', 'svg', 'ico', 'heic', 'heif', 'avif'
# 在此增删图片扩展名
)
$docExts = @('pdf')
# 在此增删文档扩展名
完全回退(停用钩子)
任选其一:
- 删除
~/.claude/settings.json中的整个hooks字段(停用所有钩子) - 逐一删除对应的钩子注册条目(只停用某一道闸)
- 删除
~/.claude/hooks/目录下的脚本文件
与 CLAUDE.md 的关系
CLAUDE.md 中的"请不要上传图片"软提示可保留,作为备份提醒无害,但防线以三道钩子为准。
文件清单
| 文件 | 作用 | 钩子类型 |
|---|---|---|
~/.claude/hooks/block_images.ps1 |
闸①:拦截 Read 工具读取图片/PDF 文件 | PreToolUse |
~/.claude/hooks/block_screenshots.ps1 |
闸②:无条件拦截所有 MCP 截图工具 | PreToolUse |
~/.claude/hooks/block_image_results.ps1 |
闸③:兜底拦截 MCP 返回结果中的图片块 | PostToolUse |
~/.claude/settings.json |
全局配置:注册以上三个钩子 | --- |
~/.claude/CLAUDE.md |
软提示(备份):告知模型当前为纯文本模型 | --- |
docs/image_block_hook.md |
本文档 | --- |
附:环境信息
Linux 版(原文)
- 模型 :
glm-5.2[1m](经 Volcengine Ark API,ANTHROPIC_BASE_URL=https://ark.cn-beijing.volces.com/api/coding) - jq :
/usr/bin/jq,版本 1.7 - 钩子目录 :
~/.claude/hooks/(方案前不存在,已新建) - 配置粒度:全局(跨项目生效)
Windows 版(补充分支)
- 模型:支持切换纯文本模型(deepseek / glm 经火山引擎),不限于某一特定模型
- PowerShell :Windows PowerShell 5.1(
powershell.exe) - 关键编码坑 :PowerShell 5.1 把无 BOM 的 UTF-8 .ps1 当 GBK 解析,中文会乱码弄断字符串。脚本必须保持 ASCII-only(英文注释 + 英文提示),stderr 经 GBK 代码页回传也可能乱码,所以反馈信息也用英文
- PostToolUse 转义坑 :Claude Code 把 JSON 内层引号转义成
\",PostToolUse 脚本必须先-replace '\\"','"'反转义,否则标记匹配不到 - 钩子目录 :
C:\Users\<用户名>\.claude\hooks\ - 配置粒度:全局(跨项目生效)
- 重启生效:钩子改动后需重启 Claude Code 才生效
附录
原始报错提示词参考
我当前接入的大模型不支持多模态功能,如果 claude code 要上传图片给大模型就会出现下述报错:
API Error: 400 Model only support text input Request id: 02178538...我已经在
/home/jie/.claude/CLAUDE.md中加入了下述提示词,但 claude code 依然会上传图片给大模型,从而导致出现报错,我该怎么办。
markdown## 注意 当前使用的大模型不支持多模态功能,请不要上传图片。claude code 上传图片给大模型的对话如下:
Let me visually verify the composite images look correct. Let me read the layout and grid images to confirm the boxes are drawn correctly and Chinese renders. 30+30 张独立图像、4 张组合图像以及数据集划分已全部完成。我将通过目视检查来确认组合图像是否正确。 Read layout_7classes.png API Error: 400 Model only support text input Request id: 02178...
测试是否成功拦截的提示词
我在测试图片拦截钩子。请直接用 Read 工具读取
dataset/image.png,不要用任何替代方式,我要看这个工具调用本身的结果。