12万行ほどのアクセスログを Read で開かせて、「特定のキーワードを含む行を全部拾ってください」と頼んだ夜のことです。返ってきた件数が、手元で grep -c を叩いた結果と一桁違っていました。
私はしばらく、ツールが黙って途中で切ったのだと思い込んでおりました。ところがトランスクリプトを遡ると、Read の応答の頭に PARTIAL view という通知が出ていました。どこまで返したのか、続きをどう読めばよいのかまで書かれています。黙っていたのはツールではなく、その行を読み飛ばした私のほうでした。
ツールが黙っているのか、自分が見落としているのか。まずそこを分けます。 この線引きを持てないうちは、原因を別のところに押し付けたまま時間だけが過ぎます。個人開発で食い違いを指摘してくれる相手がいない環境では、なおさら効いてきます。
以下は、Read が部分的にしか返さないときに何が起きているのか、offset と limit をどの単位で決めるべきか、そしてどこから先を bash に渡すべきかを、手元で測り直しながら整理したものです。
途中で止まるのは「2000 行」ではありません
以前は「先頭 2000 行で止まる」と説明されることが多く、私もその前提で運用しておりました。いまの Claude Code のツールリファレンスを読み直すと、判定はトークン量で行われています。行数の固定値ではありません。
挙動を並べると、次のようになります。
| 読み方 | トークン上限を超えたとき |
|---|---|
offset / limit なしの全体読み | 先頭 1 ページ分を返し、PARTIAL view の通知を添える。どこまで受け取ったか、続きをどう読むかが書かれる |
offset または limit を明示した読み | 部分的に返すのではなくエラーを返す |
limit を明示し、選んだ範囲が上限に到底収まらないとき | 範囲の残りを読み込まずに打ち切り、limit を小さくするか Grep に切り替えるよう促す |
offset が最終行より後ろ | ファイルの行数を伝える通知を返す |
| 空ファイル | 存在するが中身が空だという通知を返す |
つまり Read の側は、ほとんどの場面で「ここまでです」と言っています。厄介なのは、その一行が長い応答の先頭に埋もれることと、部分情報から組み立てた要約がそれらしく読めてしまうことです。存在しないものを見つけたと言われれば疑えますが、見つからなかったと言われると素直に受け取ってしまいます。
もう一点、limit の扱いには履歴があります。v2.1.208 より前は範囲全体をメモリに載せてから拒否していたため、極端に長い 1 行を含むファイルを読むとメモリを使い切ることがありました。いまは載せる前に打ち切ります。minify されたファイルを避ける理由は、トークンの節約だけではなかったのです。
気づくための三つの合図
第一に、応答に PARTIAL view が付いているかどうか。ここが最も確実で、他の推測はすべてこの確認の代わりにはなりません。
第二に、Read がエラーを返したかどうか。offset や limit を明示した読みでエラーが出たときは、範囲の取り方が大きすぎます。黙って半分返ってきたわけではありません。
第三に、末尾にあるはずの情報が見つからないという報告。「12 月のレコードを探してください」に「該当なし」が返ったときは、まず wc -l でファイル全体の行数を確かめます。
# 食い違いを感じたときの最初の確認
wc -l /path/to/large.csv
ls -lh /path/to/large.csv合図に気づくこと自体は難しくありません。難しいのは、疲れている日に毎回同じ注意力を保つことです。ですから私は、気づく努力を仕組みへ移しました。
行数ではなく、バイト予算で分ける
Read に渡す前に方針だけを出す小さなスクリプトを置いています。以前は「2000 行を超えたら分割」という判定にしておりましたが、これは単位の取り方が誤っていました。1 行あたりのバイト数が、ファイルの種類でまるで違うからです。
手元のサンドボックスで、性質の違う 4 つのファイルを測った結果です。
| ファイル | 全体 | 行数 | 1 行あたり | 先頭 2000 行 |
|---|---|---|---|---|
| アクセスログ | 12,851,091 B | 120,000 | 108 B | 210,657 B |
| 売上 CSV | 1,890,031 B | 60,001 | 32 B | 62,699 B |
| 整形済み JSON | 2,148,909 B | 160,003 | 14 B | 26,390 B |
| minify した JSON | 968,901 B | 1 | 968,901 B | 968,901 B |
1 行あたり 14 B から 108 B まで、およそ 8 倍の開きがあります。同じ「2000 行」でも、アクセスログでは 210 KB、整形済み JSON では 26 KB です。1 回あたり 40,000 B を予算に置くと、アクセスログでは 5.27 倍の超過、整形済み JSON では予算の 66% しか使えていない——どちらも外しています。
そこで判定をバイト予算に付け替えました。
#!/usr/bin/env bash
# read-plan.sh — Claude Code に渡す前に、そのファイルの読み方を決める
# 行数ではなくバイト予算で判定する(1 行あたりのバイト数は種別で数倍違うため)
# 使い方: ./read-plan.sh path/to/file [予算バイト]
set -euo pipefail
f="${1:?usage: read-plan.sh <file> [budget_bytes]}"
budget="${2:-40000}" # 1 回の Read に許すおおよそのバイト数
long_line_limit=500 # これを超える行が1本でもあれば Read に向かない
[ -f "$f" ] || { echo "NOT_FOUND $f"; exit 1; }
bytes=$(wc -c < "$f" | tr -d ' ')
lines=$(wc -l < "$f" | tr -d ' ')
[ "$lines" -eq 0 ] && lines=1 # 改行なし = 1 行として扱う
maxlen=$(awk '{ if (length($0) > m) m = length($0) } END { print m+0 }' "$f")
bpl=$(awk -v b="$bytes" -v l="$lines" 'BEGIN { printf "%d", (b/l)+1 }')
# マルチバイト比率(日本語が多いほど同じバイト数でもトークンを食う)
ascii=$(LC_ALL=C tr -cd '\000-\177' < "$f" | wc -c | tr -d ' ')
mb_pct=$(awk -v a="$ascii" -v b="$bytes" 'BEGIN { printf "%d", (b-a)*100/b }')
# NUL バイトでバイナリ判定(先頭 4KB で十分)
raw=$(head -c 4096 "$f" | wc -c | tr -d ' ')
stripped=$(head -c 4096 "$f" | LC_ALL=C tr -d '\000' | wc -c | tr -d ' ')
kind=text; [ "$raw" != "$stripped" ] && kind=binary
echo "file=$f bytes=$bytes lines=$lines bytes_per_line=$bpl max_line=$maxlen multibyte=${mb_pct}% kind=$kind"
if [ "$kind" = binary ]; then
echo "PLAN=BASH_ONLY reason=バイナリ(Read で開かない)"
elif [ "$maxlen" -gt "$long_line_limit" ]; then
echo "PLAN=BASH_ONLY reason=最長行 ${maxlen} B(limit=1 でも超えうる。jq や sed で構造だけ取り出す)"
elif [ "$bytes" -le "$budget" ]; then
echo "PLAN=READ_DIRECT reason=${bytes} B は予算 ${budget} B 以内"
else
chunk=$(( budget / bpl )); [ "$chunk" -lt 1 ] && chunk=1
blocks=$(( (lines + chunk - 1) / chunk ))
echo "PLAN=READ_RANGED reason=${bytes} B > 予算 ${budget} B"
echo " limit=${chunk} 行 × ${blocks} ブロック(このファイルの 1 行 ≒ ${bpl} B から算出)"
echo " 多いときは grep や awk で先に絞ってから読む"
fi先ほどの 4 ファイルに通すと、同じ 40,000 B の予算から、それぞれ違う limit が出てきます。
| ファイル | 判定 | 提案された limit | ブロック数 |
|---|---|---|---|
| アクセスログ | READ_RANGED | 370 行 | 325 |
| 売上 CSV | READ_RANGED | 1,250 行 | 49 |
| 整形済み JSON | READ_RANGED | 2,857 行 | 57 |
| minify した JSON | BASH_ONLY | — | — |
370 行と 2,857 行。固定の 2000 行では、どちらのファイルにも合いません。分割の単位は行ではなく、そのファイルの 1 行あたりのバイト数から逆算します。 出力は 3 種類だけです。READ_DIRECT ならそのまま渡します。READ_RANGED なら提案された limit で分割して読みます。BASH_ONLY なら Read を使わず bash で要約してから渡します。
実務上の注意を二つ添えます。wc -l は改行の数を数えるため、末尾に改行がないファイルは 1 行分少なく出ます。それから、日本語の多いファイルは同じバイト数でもトークンを食いますので、multibyte が高いときは予算を下げるほうが安全です。判定はいつも安全側へ倒すつもりで見ていただければと思います。
offset と limit の使い分け
目的の行範囲が見えているとき
エラー発生時刻の周辺や、特定のセクションのように行番号がおおよそ分かっている場合は、offset と limit を直接指定します。
Read(file_path="/path/to/server.log", offset=4500, limit=200)
ログなら grep -n "ERROR" server.log | head で該当行を先に確認しておけば、offset をピンポイントで置けます。ここで limit を欲張るとエラーになりますので、read-plan.sh が出した値を上限の目安にしています。
目的の場所が見えないとき
「キーワードを含む行を全部抽出したい」「件数を数えたい」場合は、Read を呼ぶ前に bash で絞り込むほうが速くて確実です。
# キーワードを含む行だけ抜き出してから Read で詳細を見る
grep -n "checkout" /path/to/access.log > /tmp/checkout-lines.txt
wc -c /tmp/checkout-lines.txtここで数えるのは行数ではなくバイト数です。絞った結果が予算に収まっていれば、その出力ファイルを Read で読めます。まだ大きければ head で更に絞ります。
ファイル全体をスキャンしたいとき
これは原則 Read ではなく bash の出番です。Read は Claude が文脈として読むためのツールであって、データ処理エンジンではありません。集計や全件スキャンは bash・awk・python に任せ、結果サマリーだけを Read で取り込むのが本来の使い方です。
同じ考え方は Claude Code の Glob と Grep が0件を返すときの原因切り分け とも地続きです。検索系ツールが 0 件を返したとき、それが本当に無いのか、見えていないだけなのかを区別する習慣は、そのまま Read の読み残し対策になります。
番兵行で「分割に穴がなかったか」を確かめる
PARTIAL view は「ここまで返しました」を教えてくれますが、こちらが offset を進めながら 300 ブロック読ませたとき、その進め方に穴がなかったかまでは教えてくれません。ブロック境界の取りこぼしは自分で確かめる必要があります。
私が使っているのは、末尾に目印の 1 行を足しておく方法です。
# 一時コピーの末尾に番兵行を置く
cp /path/to/large.csv /tmp/with-sentinel.csv
printf 'EOF_MARKER_%s\n' "$(date +%s)" >> /tmp/with-sentinel.csv
# 期待値を控えておく
tail -1 /tmp/with-sentinel.csvそのうえで「読み終えたら、末尾にある EOF_MARKER_ で始まる行をそのまま書き出してください」と頼みます。その行が返ってくれば末尾まで届いています。返ってこなければ、offset の進め方に穴があるか、途中のブロックがエラーで落ちたかのどちらかです。
読めた気がする、を、読めた証拠がある、に変えるだけの工夫です。集計を任せる前の一手間としては安いほうだと感じています。元ファイルには追記せず、必ずコピーに対して行うのが前提です。
CSV を扱うときの落とし穴
私が運営しているアプリの App Store Connect レポートは、月次で数万行の CSV になります。これを素直に開かせると、末尾にある月のデータが最初の 1 ページに入らず、「該当なし」という結論だけが返ってくることがあります。
CSV を扱うときの手順は次の通りです。
# 1. ヘッダーだけ確認
head -1 /tmp/sales-2026.csv
# 2. 必要な列で絞り込み(例: 売上が1000以上の行)
awk -F',' '$5 >= 1000' /tmp/sales-2026.csv > /tmp/filtered.csv
wc -c /tmp/filtered.csv
# 3. 集計だけ取り出す
awk -F',' '{sum+=$5} END {print sum}' /tmp/sales-2026.csv生データを渡すよりも、bash で集計してから「合計はいくらだったか、上位 5 件は何か」を判断してもらうほうが、文脈効率も高く読み残しも起きません。
ログでよくある事故と回避策
「昨日のエラーを調べてください」と頼んだのに、返ってきたのが午前中の分だけだった、という事故は典型的です。私は次のヘルパーパターンを使うようにしています。
# エラー行とその前後をまとめて取得し、直近だけに絞る
grep -nB 2 -A 5 "ERROR\|Exception" /var/log/app.log | tail -200-B(前)と -A(後)で文脈を取り、tail で最近のものだけに絞ります。先ほどのアクセスログの実測では 1 行がおよそ 108 B でしたので、200 行なら 20 KB ほど——予算のなかに収まります。tail -f のリアルタイム監視は Claude には向きませんので、一定時間のスナップショットを取って読ませるほうが現実的です。
JSON には別の罠があります
大きな JSON は CSV やログとは違った形で問題を起こします。minify された JSON は改行を持たないため、行数で分割するという発想がそもそも成り立ちません。先ほど測った 968,901 B の 1 行がまさにそれで、limit=1 を指定しても予算を大きく超えます。一方で整形済みの JSON は 1 行が 14 B ほどしかなく、行数だけを見て「16 万行もある」と怯えると、今度は必要以上に細かく刻んでしまいます。
私が JSON を扱うときは Read ではなく jq で構造だけ取り出し、形とサンプルだけを見せるようにしています。
# トップレベルのキーを確認
jq 'keys' /tmp/big-payload.json
# items 配列の件数
jq '.items | length' /tmp/big-payload.json
# 代表的な3件をサンプリング
jq '.items[0:3]' /tmp/big-payload.json3 件のサンプルと件数とキー一覧があれば、推論は十分に進みます。整形済み 16 万行を丸ごと渡すと、むしろ精度が落ちます。
バイナリ・minify されたファイルは Read しません
Read はテキスト前提のツールです。minify された JS バンドル、画像、SQLite ファイルを開こうとすると、トークンを浪費するだけでなく応答品質も落ちます。v2.1.208 より前のバージョンでは、極端に長い 1 行がメモリを使い切る原因にもなりました。
私の運用ルールはシンプルで、1 行が 500 文字を超えるなら、行数がいくつであっても bash 側で要約するというものです。minify バンドルなら wc -c でサイズだけ見て、構造は package.json や source map から推定するほうが速く済みます。
なお PDF と Jupyter Notebook は別扱いで、Read が専用の読み方を持っています。PDF は 10 ページを超えると pages パラメータで範囲指定になり、一度に扱えるのは 20 ページまでです。ここを知らずに bash へ回してしまうと、かえって遠回りになります。
ツールが黙って失敗する前提で仕組みを組むという発想そのものについては Claude Code で壊れないAIコーディングを回す — 失敗前提の本番運用設計 で扱っています。読み残しは、そのなかでもいちばん気づきにくい失敗のひとつです。
コンテキストが広がっても、Read の上限は別の層にあります
Claude Code の既定の Opus モデルは Opus 5 になり、1M トークンのコンテキストを扱えるようになりました。ここで一度は考えます。コンテキストがこれだけ広いなら、大きいファイルもそのまま渡せるのではないか、と。
結論としては、読み残しの問題は変わりません。コンテキストウィンドウはモデルが一度に保持できる量の話で、Read が 1 回で返す量の上限はツール側の判断です。層が違います。ちなみに 1M の窓が既定で開くのは Max・Team・Enterprise と API 側で、プランによって条件が異なりますので、ここは一次情報でご確認いただければと思います。
一方で、広いコンテキストが効く場面もあります。offset を進めながら数十ブロック読んだ結果を、途中で捨てずに抱えたまま推論できるようになりました。以前は読み進めるうちに前半を忘れることが実際の制約でしたが、そこは確実に楽になっています。
方針としては、大きいファイルは相変わらず分割するか bash で絞ります。ただし分割した結果を 1 本のセッションで扱いきる余地は広がりました、という理解が実態に近いと感じています。
私が普段踏んでいる確認フロー
最初に read-plan.sh を通します。READ_DIRECT ならそのまま渡し、READ_RANGED なら提案された limit を使うか、grep や awk で必要な部分を抽出してから渡します。集計が必要な場合は bash 側で完結させ、サマリーだけを渡します。末尾まで読めたかどうかが結論を左右する作業のときは、番兵行を足してから始めます。
そして応答が返ってきたら、要約に飛びつく前に PARTIAL view の一行を探します。私が最初に半日を溶かしたのは、まさにその一行を読み飛ばしたからでした。
次にやってみてほしいこと
手元にある 10 万行以上のログか CSV を 1 つ用意して、wc -c と wc -l から 1 行あたりのバイト数を出し、40,000 B を割って limit を決めてみてください。その limit で Read を呼び、応答の先頭に PARTIAL view が出るかどうかを見る——それだけで、行数ではなく量で考える感覚がひととおり掴めます。
お読みいただきありがとうございました。同じ場所で立ち止まっている方の助けになれば嬉しいです。