Claude CodeのSessionStart hookは1万字まで:日本語と絵文字の数え方、超えたら届くのは先頭2,000字だけ

当ページのリンクには広告が含まれています。

Claude CodeのSessionStart hookで作業メモやルールを渡しているなら、1つのhookの出力は1万字までにしてください。超えた分は後半が読まれないのではなく、出力全体がファイルへ移され、会話には先頭2,000字とファイルの場所だけが入ります。日本語は1字を1、絵文字の多くは1字を2と数えます。Claude Code 2.1.322で文字の種類と長さを変えて試したので、その結果から、自分のhookが超えていないかの確かめ方と、超えていたときの分け方を説明します。

目次

結論:数え方とhookの分け方で、届く量が変わる

  • 上限は1つのhookの出力ごとに1万字:6,000字のhookを2つ置くと、合計1万2,000字でも両方が全文届きました。
  • 日本語はバイトではなく字数で数える:日本語9,990字(UTF-8では約30KB)は全文届き、10,010字で置き換えられました。
  • 絵文字は1字を2と数える:絵文字5,500字は、数え方の上では1万986になり、置き換えられました。JavaScriptの文字列長と同じUTF-16の単位です。
  • 超えると先頭2,000字だけ:置き換え後もプレビューはモデルに届きましたが、末尾に置いた目印は見えませんでした。後半に大事な指示を置いていると、その指示は守られません。

公式の仕様:1万字を超えた出力はファイルに移される

Claude Codeのhooksリファレンス(2026年11月13日確認)には、hookが返すadditionalContext・systemMessage・initialUserMessageと、JSONでない普通の標準出力は、それぞれ10,000字が上限と書かれています。超えると出力はセッションのフォルダへ保存され、会話にはファイルのパスと先頭最大2,000字のプレビューが入ります。Claude Codeはそのファイルを読むようClaudeに促さず、上限を上げる設定や環境変数もありません。

SessionStartは、起動(startup)・再開(resume)・/clear・圧縮(compact)・fork のときに動き、普通の標準出力もそのまま会話に入る数少ないイベントです。前回の作業メモやgitの状態を渡す用途で広く使われていますが、11月12日には31万字を渡していたのに会話にほぼ入っていなかったという報告がQiitaに出ました。GitHubでも、hookの出力を1万字に収める修正が複数のリポジトリで続いています。

ただ、日本語や絵文字をどう数えるか、hookを複数置いたら合計で数えるのかは、リファレンスに書かれていません。Qiitaの報告も数え方はUTF-16だと仮定して進めており、複数のhookは扱っていません。そこで手元で確かめました。

手元で確かめた条件と結果

macOS上のClaude Code 2.1.322で、実験用フォルダの.claude/settings.jsonにSessionStart hookを置き、claude -pで新しい会話を始めました(モデルはHaiku)。hookの出力は、先頭にBEGIN-1-、末尾に-END-1という目印を置き、間を同じ文字で埋めた文字列です。会話記録(~/.claude/projects/配下のJSONL)のうちモデルへ送られる部分に末尾の目印が残ったかを調べ、モデル自身にも見えた目印を答えさせました。両者の結果は全ケースで一致しています。

  • 英字9,990字:全文届いた
  • 英字10,010字:置き換えられた(会話に入ったのは2,349字。見出しとパス、先頭2,000字)
  • 日本語9,990字:全文届いた
  • 日本語10,010字:置き換えられた
  • 絵文字5,500字(UTF-16で1万986):置き換えられた(プレビューは絵文字約1,000字=2,000単位)
  • JSONでない標準出力10,010字:置き換えられた(additionalContextと同じ扱い)
  • 6,000字のhookを2つ(合計1万2,000字):両方とも全文届いた

置き換えられたとき、モデルに届く文面は次の形でした。

<persisted-output>
Output too large (9.8KB). Full output saved to: ~/.claude/projects/<フォルダ名>/<セッションID>/tool-results/hook-…-SessionStart-…-additionalContext.txt

Preview (first 2KB):
BEGIN-1-あああ…
...
</persisted-output>

「9.8KB」はファイルの大きさではなく、字数を1,024で割った値です。日本語10,010字はUTF-8で約30KBありますが、表示は英字と同じ9.8KBでした。絵文字5,500字は10.7KBと表示されており、ここでもUTF-16の単位で数えていることが分かります。

Qiitaの報告では、置き換えられた会話にプレビューも見当たらなかったとあります。今回の5つの置き換えケースでは、どれもプレビューがモデルに届き、先頭の目印を答えました。報告はWindowsのGit Bashでの結果なので、環境による差かもしれませんが、原因は確かめていません。どちらにしても、先頭2,000字より後ろは届かないと考えて設計するのが安全です。

自分のhookが上限を超えていないか確かめる

確かめ方は2つあります。1つ目は、hookを手で動かして出力を数える方法です。次のスクリプトは、JSONならadditionalContextなどの文字列を、JSONでなければ全文を、上限と同じUTF-16の単位で数えます。標準ライブラリだけで動きます。

"""hookの出力が、Claude Codeの上限(10,000字)の何字にあたるかを数える(標準ライブラリのみ)。

使い方: hookのコマンド | python3 hook_size.py
JSONならadditionalContext・systemMessage・initialUserMessageを、JSONでなければ全文を数える。
数え方はUTF-16の単位(JavaScriptの文字列長)。日本語は1字1単位、絵文字の多くは1字2単位。
"""
import json
import sys

LIMIT = 10_000
FIELDS = ("additionalContext", "systemMessage", "initialUserMessage")


def strings_of(raw):
    try:
        data = json.loads(raw)
    except ValueError:
        return {"stdout": raw}
    if not isinstance(data, dict):
        return {"stdout": raw}
    found = {key: data[key] for key in FIELDS if isinstance(data.get(key), str)}
    specific = data.get("hookSpecificOutput") or {}
    found.update({key: specific[key] for key in FIELDS if isinstance(specific.get(key), str)})
    return found


def utf16_length(text):
    return len(text.encode("utf-16-le")) // 2


def main():
    for name, text in strings_of(sys.stdin.read()).items():
        size = utf16_length(text)
        verdict = "超過:パスと先頭2,000字だけが届く" if size > LIMIT else f"収まる(残り{LIMIT - size}字)"
        print(f"{name}: {size}字(UTF-16)/ {len(text)}字(見た目の文字数)→ {verdict}")


if __name__ == "__main__":
    main()

hookのコマンドは、設定ファイル(~/.claude/settings.jsonやプロジェクトの.claude/settings.json)のhooks.SessionStartに書かれています。hookは標準入力でイベントの情報を受け取るので、手で動かすときは最低限のJSONを渡します(hookのパスは例です)。

echo '{"hook_event_name":"SessionStart","source":"startup"}' | python3 ~/.claude/hooks/session_start.py | python3 hook_size.py

手元で試すと、日本語9,990字は「収まる(残り10字)」、絵文字5,500字は「10986字(UTF-16)/5500字(見た目の文字数)→ 超過」、英字10,010字の標準出力は「超過」と判定され、Claude Code上の結果と一致しました。出力の長さが日によって変わるhook(作業メモやgitの差分を足すもの)は、メモがいちばん長い日を想定して数えてください。

2つ目は、過去に置き換えが起きた跡を探す方法です。置き換えられた出力は、セッションのフォルダのtool-resultsにhook-で始まる名前で保存されます(標準出力ならhook-…-stdout.txt)。

find ~/.claude/projects -path '*tool-results/hook-*'

何も出なければ、残っている会話記録の範囲では一度も超えていません。1件でもあれば、そのファイルが置き換えられた出力の全文です。この端末で実験用フォルダを除いて探すと0件で、普段使っているhookは上限内でした。会話記録は古いものから消えるため、何も出ないことは「これまで一度も超えていない」証明にはなりません。

超えていたときの直し方:固定のルールと毎回変わる情報を分ける

hookの出力を削る前に、中身を2種類に分けると整理しやすくなります。

  • 毎回同じルールや手順:hookではなくCLAUDE.mdに置きます。メモリのドキュメントによると、起動時に読み込まれるCLAUDE.mdは、4MiBを超えない限り全文がClaudeに渡ります。長い資料は@path/to/fileの形で取り込めます(最大4段)。ただし公式は1ファイル200行未満を勧めており、長いほど指示が守られにくくなるとしています。
  • 起動のたびに変わる情報(前回のメモ、gitの状態、未完了のタスク):hookに残し、9,500字程度を上限に要点だけを出します。全文はファイルに書き出し、hookの出力には「詳しくは〇〇を読む」とパスを書いておけば、必要なときにClaudeが読みに行けます。
  • どうしても量が多い場合:hookを用途ごとに分ければ、それぞれ1万字まで届きました。ただし確かめたのは2つまでで、3つ以上や合計の上限は試していません。分けても会話に入る量は減らないため、毎回の入力トークンは増えます。費用の考え方はClaude Codeの料金をログで計算した記事にまとめています。

どの方法でも、守ってほしい指示は先頭に置いてください。上限を少し超えただけでも、モデルに届くのは先頭2,000字だけで、後半に書いた指示は届きません。見出しの装飾に絵文字を多く使っている場合は、それだけで数え方の上では倍になるため、記号に置き換えると余裕が生まれます。

確かめていないこと

  • WindowsとLinux、再開(resume)や圧縮(compact)のときの挙動は試していません。Qiitaの報告とプレビューの有無が違う理由も未確認です。
  • hookは2つまでしか試しておらず、3つ以上や、全hookの合計に別の上限があるかは分かりません。
  • 上限がいつから1万字なのかは、公式の更新履歴で確認できませんでした。今後の版で変わる可能性があります。
  • モデルにはファイルを読まないよう指示して答えさせました。指示しない場合に、Claudeが自分から保存ファイルを読みに行くかは測っていません。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

IT業界で10年、ソフトウェア開発をしてきたエンジニアです。今もChatGPT、Codex、Claude Codeを中心に、開発の効率化や生産性向上にAIをフル活用しています。応用情報技術者。登壇や執筆もしています。このサイトでは、仕事でAIを使う立場から、料金や利用条件、使い方を調べてまとめています。

目次