Typora加github加PicGo搭建图床使用教程

Claude Code 接入纯文本大模型:API Error 400 Model only support text input 彻底解决方案


目录

  1. 问题现象
  2. [根因分析:为什么 CLAUDE.md 不够](#根因分析:为什么 CLAUDE.md 不够)
  3. [方案设计:PreToolUse 钩子](#方案设计:PreToolUse 钩子)
  4. 实现细节
  5. 工作原理流程
  6. 测试验证
  7. 生效条件与重启
  8. 边界与限制
  9. 维护与回退
  10. 文件清单
  11. 附录

问题现象

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 本体在工具执行前本地拦截 ✅ 确定拦截

关键点

  1. CLAUDE.md 靠模型自觉,一旦模型决定调用 Read 读图片,提示就被绕过。
  2. 真正把图片字节当作多模态内容发给模型的是 Claude Code 本体(harness),而非模型。harness 不读 CLAUDE.md,只认工具调用。
  3. 只要模型调用 Read 读图片,就必然触发 400。必须在**"工具调用真正执行之前"**由本地代码硬性阻断------PreToolUse 钩子的职责。

方案设计:PreToolUse 钩子

选型理由

方案 可靠性 依赖模型自觉 选用
CLAUDE.md 软提示 否(仅作备份提醒)
PreToolUse 钩子(本地 shell) ✅ 确定拦截

钩子是 Claude Code 本体在每次工具调用前/后本地执行的 shell 命令,与后端模型无关 (无论接的是 GLM 还是 Claude 都会执行)。通过 exit 2 即可阻断工具调用,并将 stderr 作为反馈发回给模型。

完整拦截策略(三道闸)

单纯拦截 Read 工具读图片文件还不够,实际存在三条路径会让图片进入上下文:

# 路径 触发场景 拦截方式
Read 工具读图片/PDF 模型调用 Read xxx.pngRead 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() 统一小写
  • 图片扩展名pngjpgjpeggifwebpbmptifftifsvgicoheicheifavif
  • 文档扩展名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 兜底)
  • commandpowershell -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。

操作步骤:

  1. 退出当前 Claude Code 会话
  2. 重新打开 Claude Code
  3. 在新会话中验证

边界与限制

三道闸覆盖了绝大部分路径,但仍有残余局限:

场景 是否拦截 说明
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,不要用任何替代方式,我要看这个工具调用本身的结果。

相关推荐
Liekkas Kono2 小时前
Github Star History 趋势图工具推荐
github·swhl
逛逛GitHub3 小时前
盘点 2 个看起来贼炫酷酷酷的 GitHub 项目,炒的还挺火的。
github
卡卡罗特AI3 小时前
GitHub怎么用?零基础,保姆级使用教程-上!建议收藏
前端·后端·github
YuePeng6 小时前
不写一行接口,让 DBeaver 直连你的指标层——背后只用了一个端口
后端·架构·github
别动我齐刘海6 小时前
Day6 unitree_G1人形机器人GMR—— MotionInput
c语言·c++·人工智能·学习·机器学习·机器人·github
leoZ2316 小时前
实战复盘:用 Claude Code 从零搭一个 GitHub PR 统计工具
java·人工智能·python·深度学习·自然语言处理·github·llama
阿里嘎多学长9 小时前
2026-08-04 GitHub 热点项目精选
开发语言·程序员·github·代码托管
MicrosoftReactor10 小时前
技术速递|GitHub Copilot App 入门指南:快速上手
ai·github·copilot·copilot app
大眼、不聚光12 小时前
9.linux系统管理-ssh免密配置
linux·ssh·github