Claude Codeが画像を開くと止まる。テキスト専用LLMを守るフックを作った

この記事の概要
前回の記事では、Claude Codeが画像を開くと会話が止まる問題を、実際に使っている設定欄を画像対応モデルへ変えることで解決しました。
では、普段使う文章専用モデルを変えずに、画像が必要な作業を続けるには?今回は、AIが画像を開く直前に止め、画像対応モデルへ解析を任せる方法を作りました。
原因と対策を先に書くと、こうです。
- 使っていたAIモデルは、文章は読めても画像は読めない
- Claude Codeが画像を渡すとエラーになり、その会話では作業を続けられなくなる
- 画像を開く直前に止め、画像が読める別のモデルに解析を任せるようにした
この記事では、この仕組みを作った過程と、レビューで見つけた落とし穴を紹介します。
何が起きたのか
私の環境では、Claude CodeをOllama経由で動かしています。裏側で使う3つのモデルは、どれも文章専用です。Claude Codeの画面は同じでも、接続先のモデルが画像を読めるとは限りません。
作業中、AIがフォルダ内のPNG画像をReadという機能で開きました。すると接続先から、次のエラーが返りました。
this model does not support image input
「このモデルは画像入力に対応していない」という意味です。厄介なのは、一度画像が会話に入ると、私の環境では次の指示も同じエラーになること。その会話をそのまま続けられず、/clearで会話を消してやり直しました。
原因は、接続先の3モデルがすべて画像を読めないモデルだったことです。Ollamaとの接続部分は画像を受け取れますが、最後に処理するモデルが拒否します。スクリーンショットを会話に直接貼っても同じです。
画像非対応モデルの見分け方は以前の記事で触れました。今回は、そのモデルを使いながらエージェントの画像読み込みを安全に扱う方法がテーマです。
モデルを選ぶとき、私はコードを書く能力ばかり見ていました。しかしAIは作業の途中で画像も開きます。「文章の性能が高い」だけでは、いつもの作業を安全に続けられるとは限らない。そこが盲点でした。
画像を開く前に止め、別のAIに渡す
必要なのは、画像を読めないモデルに画像を渡さないことです。
Claude CodeにはPreToolUseフックという、AIがファイルを開く直前に動く仕組みがあります。これを門番にして、Readの対象が.pngや.jpgなどの画像拡張子なら開くのを拒否します。画像がモデルに届かなければ、今回のエラーは起きません。
ただし、守れるのはAIがReadで画像拡張子のファイルを開く場合だけです。画像の中身まで判定しているわけではないので、拡張子のない画像は通ります。人が会話欄に画像を直接貼る操作も、この門番を通りません。
止めるだけではAIの作業も止まります。そこで、拒否する理由に次の手を書きました。
このモデルは画像を読めません。代わりに
ollama-visionで画像を解析してください。
ollama-visionは、画像を読める別のモデルに1枚だけ渡し、結果を文章で返す自作コマンドです。元のClaude Codeには画像そのものではなく、解析結果の文章だけが戻ります。
実際のテストでも、Claude Codeは画像を開くのを拒否されたあと、自分でollama-visionを呼び、解析結果を報告しました。会話は止まりませんでした。拒否の理由に代わりの方法を書くと、AIを安全な経路へ案内できる。これが一番の学びです。
2026年9月14日にも、64×64pxの赤いテスト画像で再試験しました。ガードを外すと画像のReadで400エラーになり、同じ会話を再開して画像に関係ない指示を出しても再び400でした。ガードを有効にするとReadが拒否され、代わりのモデルを使って「真っ赤な単色の画像」と答えられました。
ガードなし/ありの再試験を比較。実行ログの要点を画面風に整えた画像で、端末をそのまま撮ったスクリーンショットではありません。長い引数と個人パスは省略しています。画像をタップすると拡大できます。
画像拡張子のファイルをReadする経路を止め、別の画像対応モデルが返した文章だけを使います。図をタップすると拡大できます。
作ってから気づいた5つの落とし穴
ここからは実装寄りの話です。最初に作ったものをCodexに4回レビューしてもらい、安全装置ならではの落とし穴を直しました。
1. 門番が壊れたら、通さず止める
フックのプログラムがexit 1などで終わると、Claude Codeはエラーを知らせつつもファイルを開く処理を続けます。これでは画像がすり抜けます。
そこで、門番に異常があればRead全体を止めるようにしました。具体的には、Claude Codeがブロックとして扱う終了コード2を返します。安全側に倒す、いわゆる「fail-closed」です。
2. 入力の欠落・型違いを見逃さない
フックに渡される情報が空だったり、ファイル名の項目が欠けていたり、文字列ではなかったりした場合は止めます。そこで「画像ではない」と決めつけて通すと、Claude Code側の仕様が変わったときに、守れなくなったことに気づけないからです。
ただし、ファイル名が空文字の場合は現在の実装では通ります。入力検証が万全というわけではありません。
3. 設定が上書きされないようにする
私の起動環境では、--settingsで設定を複数渡すと最後の指定が優先されました。後から別の設定を渡すと、画像を止めるフックが消えてしまいます。
そこで、ガードが有効な間は、別の--settingsが指定されたら起動を止めて理由を表示します。
4. 大きな画像をコマンド引数に入れない
画像をテキストに変換して、コマンドの引数として渡す作りにしたら、867KBのPNGで失敗しました。引数にはサイズの上限があったためです。
引数ではなく標準入力(コマンドに流し込むデータ)で渡す形に変えたところ、4.2MBの画像でも動きました。実装ではjq -Rsとcurl --data-binary @-を使っています。
5. 「もう安全そう」でガードを外さない
最初は、画像対応モデルを設定したらガードを自動で外す作りでした。でも、設定したモデルが使われ続けるとは限りません。
たとえば、非対話のprintモードで予備モデルに切り替わったり、会話の途中でモデルを変えたり、過去の会話を再開したりできます。最初の設定だけでは、今のモデルが画像を読めると保証できません。
そこで自動解除をやめ、使う人が明示的に指定したときだけガードを外すようにしました。
まとめ
- 文章専用のモデルに画像を渡すと、作業が止まることがある
- 画像を開く前に止めれば、今回のエラーは避けられる
- 止める理由に代わりの方法を書くと、AIは自分で作業を続けられた
- 安全装置は、壊れたときや設定が変わったときにも効くようにしておく
今回いちばん面白かったのは、フックが単に「禁止する仕組み」ではなかったことです。次に何をすればいいかまで伝えると、AIを安全な作業へ案内できます。同じような事故を踏んだ人の参考になれば。
なお、この記事で使った実測データ(モデル別の画像拒否応答、vision対応モデルの一覧、thinking無効化による高速化など)は、いずれ別の記事にまとめる予定です。

