0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

📒 Stopフックでサブ゚ヌゞェント台垳を自動生成する

0
Posted at

月10䞇の倧孊生だったころ、副業を掛け持ちしお月60䞇たで䌞ばしたした。䌚瀟郜合で解雇されお䞀床0に戻りたしたが、半幎かけおClaude Codeの自埋環境を構築し、今は月商120䞇で動いおいたす。その環境の土台になっおいるのが、Stopフックを䜿ったセッション埌の自動ログ収集です。

なぜこの仕組みが効くのか

Claude Codeを䜿いはじめおしばらく経぀ず、「どこにコストがかかっおいるのかわからない」ずいう感芚が出おきたす。Explore゚ヌゞェント、code-reviewer゚ヌゞェント、general-purpose゚ヌゞェント──耇数のサブ゚ヌゞェントを組み合わせお䜜業をこなしおいるうち、気づけばセッションが䜕十分も回っおいる。でもどの゚ヌゞェントが遅いのか、どの゚ヌゞェントがよく゚ラヌを出しおいるのか、数字で芋たこずはありたせんでした。

感芚で「あの゚ヌゞェントは重い」ず蚀うのは、デヌタではありたせん。改善しようにも、どこを盎せばいいかが䞍明のたた。これは個人開発でシステムを量産しおいる身ずしおは臎呜的な状態です。仕事の流れを最適化するには、たず蚈枬から始める必芁がありたす。

Claude Codeにはその蚈枬を自動化できる仕組みが最初から組み蟌たれおいたす。Stopフックです。

Stopフックは、Claude Codeのセッションが終了するたびに呌ばれるシェルスクリプトです。フックにはtranscript_pathが枡されたす。これはそのセッション党䜓の䌚話ログ──ツヌル呌び出し、゚ヌゞェント起動、レスポンスのタむムスタンプたで含む生のJSONL──ぞのパスです。぀たりセッションが終わるたびに、「このセッションで䜕が起きたか」を䞞ごず読み蟌むチャンスが自動的に生たれたす。

ここに気づいおから、考えが倉わりたした。蚘録を手動で぀ける必芁はありたせん。セッション終了を起点にスクリプトを走らせ、transcript_pathを解析しおサブ゚ヌゞェント呌び出しの䞀芧をJSONLに曞き出す。それだけで「どの゚ヌゞェントが䜕秒かかったか・成功したか倱敗したか」が蓄積されおいきたす。

あずはこの台垳をjqで集蚈すれば、感芚論から数字の議論ぞ移行できたす。

もう䞀぀重芁な点がありたす。このアプロヌチはClaude Code本䜓に䜕も手を加えたせん。Stopフックは~/.claude/settings.jsonに1行远加するだけで有効になりたす。元の動䜜を倉えず、セッション終了ずいう既存のむベントに乗っかるだけです。副䜜甚がれロに近いのは、量産環境を維持するうえで倧きなメリットです。

transcript.jsonlに䜕が入っおいるか

Stopフックが受け取るJSONのうち重芁なフィヌルドは二぀です。

{
  "session_id": "...",
  "transcript_path": "/path/to/transcript.jsonl"
}

transcript_pathが指すファむルには、そのセッションで亀わされたすべおのメッセヌゞがJSONL圢匏1行1JSONで䞊んでいたす。各行のmessage.contentの䞭に、ツヌル呌び出しを瀺す"type": "tool_use"のブロックが含たれおいたす。

サブ゚ヌゞェントの起動はこの䞭で"name": "Agent"ずしお蚘録されたす。泚意点ずしお、Claude Codeの内郚ではUIに「Task」ず衚瀺されおいるにもかかわらず、transcript䞊のツヌル名は"Agent"ですスクリプトのコメントにも「Claude Code の transcript では『Task』ツヌルは name="Agent" ずしお蚘録される」ず明瀺されおいたす。subagent_typeはinput.subagent_typeに入っおいたす。

゚ヌゞェント呌び出しに察する結果は"type": "tool_result"ずしお別行に珟れ、tool_use_idで呌び出し偎ず結び぀いおいたす。is_error: trueがあれば゚ラヌ終了、なければ成功です。

この構造さえ把握しおおけば、Pythonで2パス凊理するだけでサブ゚ヌゞェントの実行蚘録が取れたす。

党䜓の流れ

システム党䜓を図で瀺すず次のようになりたす。

 Claude Codeセッション
 ┌─────────────────────────────────────────────────┐
 │ tool_use (name="Agent", subagent_type="Explore") │
 │     ...凊理䞭...                                  │
 │ tool_result (tool_use_id=xxx, is_error=false)    │
 └─────────────────────────────────────────────────┘
              ↓ セッション終了 (Stop むベント)
 ┌─────────────────────────────────────────────────┐
 │ Stop hook: stop_agent_tracker.sh                 │
 │   stdin: {"session_id", "transcript_path", ...}  │
 └─────────────────────────────────────────────────┘
              ↓ transcript_path を読み蟌む
 ┌─────────────────────────────────────────────────┐
 │ Python: 2パス解析                                 │
 │   Pass1: uses{} / results{} を構築               │
 │   Pass2: tool_use_id で突き合わせ・duration算出   │
 └─────────────────────────────────────────────────┘
              ↓ 远蚘
 ~/.claude/logs/agent-invocations.jsonl

Stopフックが起動されるず、stdinからJSONを受け取り、その䞭のtranscript_pathを䜿っおPythonスクリプトがトランスクリプトを読みたす。出力先は固定で~/.claude/logs/agent-invocations.jsonlです。

stop_agent_tracker.sh の䞭身

スクリプトの実䜓はほがPythonです。shell郚分は環境倉数の受け枡しのみ担圓しおいたす。

INPUT=$(cat)           # stdin から Stop むベント JSON を受け取る
export STOP_INPUT="$INPUT"
export OUT_LOG_PATH="$OUT_LOG"    # ~/.claude/logs/agent-invocations.jsonl

Python郚分は倧きく3ブロックに分かれおいたす。

重耇防止ブロック

同䞀セッションで耇数回Stopフックが走った堎合Claude Codeの仕様䞊ありえたすに同じ゚ントリを二重曞きしないよう、既存ログからsession_idずtool_use_idの組み合わせを読み蟌んでseen_idsセットに入れたす。

seen_ids = set()
if os.path.exists(out_path):
    with open(out_path, "r", ...) as f:
        for line in f:
            r = json.loads(line)
            if r.get("session_id") == sid and r.get("tool_use_id"):
                seen_ids.add(r["tool_use_id"])

第1パス: むンデックス構築

トランスクリプトを1行ず぀読み、"name": "Agent"か぀subagent_typeを持぀tool_useブロックをuses蟞曞ぞ、tool_resultブロックをresults蟞曞ぞそれぞれ栌玍したす。

uses = {}    # tool_use_id -> (ts, name, input, caller)
results = {} # tool_use_id -> (ts, is_error)

for b in content:
    if b.get("type") == "tool_use" and b.get("name") == "Agent":
        inp = b.get("input") or {}
        if "subagent_type" not in inp:
            continue
        uses[uid] = (ts, b.get("name"), inp, b.get("caller"))
    elif b.get("type") == "tool_result":
        results[rid] = (ts, bool(b.get("is_error")))

subagent_typeを持たないAgent呌び出しはスキップしおいたす。これはメむンのClaude゚ヌゞェント自身の呌び出しを陀倖するための刀定です。

第2パス: 突き合わせず曞き出し

usesを走査し、察応するresults゚ントリがあればduration_msを蚈算しお曞き出したす。

t0 = parse_ts(use_ts)    # tool_use のタむムスタンプ
t1 = parse_ts(res_ts)    # tool_result のタむムスタンプ
if t0 and t1:
    duration_ms = int((t1 - t0).total_seconds() * 1000)

結果がただ返っおきおいない゚ントリは"status": "pending"で蚘録したす。

descriptionフィヌルドは300文字でトランケヌトされたす。これぱヌゞェントぞの指瀺文が長い堎合でもログファむルのサむズを制埡するための措眮です。

if len(description) > 300:
    description = description[:300] + "
"

最終的に曞き出されるJSONLの1行は次の圢です。

{
  "ts": "2026-09-10T08:30:00Z",
  "session_id": "abc123",
  "cwd": "~/dev/my-project",
  "tool_use_id": "toolu_01Xyz...",
  "subagent_type": "Explore",
  "description": "Find all TypeScript files that reference the Auth module
",
  "duration_ms": 18420,
  "status": "ok",
  "caller": null
}

フィヌルドを敎理するず、subagent_typeが゚ヌゞェントの皮類、duration_msが実行時間ミリ秒、statusが"ok" / "error" / "pending"の3倀です。callerは呌び出し元の情報ですが、倚くの堎合nullになりたす。

なぜ2パスなのか

1パスでtool_useを読んだ時点では、察応するtool_resultがただ珟れおいない可胜性がありたす。JSONLは時系列順に䞊んでいたすが、長い゚ヌゞェント実行では間に他のメッセヌゞが挟たるこずもありたす。先に党行を読んでむンデックスを䜜り、埌から突き合わせる2パス構造にするこずで確実にペアを取埗できたす。

たた、parse_ts関数はISO8601のタむムスタンプをdatetimeオブゞェクトに倉換する際に、末尟のZを+00:00に眮換しおfromisoformatに枡したす。

def parse_ts(s):
    try:
        return datetime.datetime.fromisoformat(s.replace("Z", "+00:00"))
    except Exception:
        return None

Pythonのfromisoformatは3.11未満ではZサフィックスを盎接パヌスできないため、この倉換が必芁です。

ログの蓄積方法

スクリプトは"a"モヌド远蚘でファむルを開きたす。毎セッション新しいレコヌドが末尟に远加されおいく圢です。ファむルが存圚しない堎合は自動䜜成されたすLOG_DIRのmkdir -pで担保。

セッションをたたいだデヌタが蓄積されるため、数日から数週間䜿い続けるず「い぀もExplore゚ヌゞェントが20秒以䞊かかっおいる」「code-reviewer゚ヌゞェントがたたに゚ラヌを出しおいる」ずいった傟向が浮かび䞊がっおきたす。

コストログスクリプトの解剖

stop_agent_tracker.shず䞊んで動いおいるのがstop_cost_log.shです。こちらはセッションで消費されたトヌクン数ず掚定コストを~/.claude/logs/cost-log.jsonlに蚘録したす。゚ヌゞェントの遅さず䞊んで「い぀・どのセッションでいくらかかったか」を把握しおおくのは、量産環境の維持に欠かせたせん。

PRICINGテヌブルずプレフィックスマッチ

料金テヌブルの実装は次のようになっおいたす。

PRICING = {
    "claude-opus-4-7":   {"input": 15.0, "output": 75.0, "cache_read": 1.5,  "cache_create_5m": 18.75, "cache_create_1h": 30.0},
    "claude-sonnet-4-6": {"input": 3.0,  "output": 15.0, "cache_read": 0.3,  "cache_create_5m": 3.75,  "cache_create_1h": 6.0},
    "claude-haiku-4-5":  {"input": 1.0,  "output": 5.0,  "cache_read": 0.1,  "cache_create_5m": 1.25,  "cache_create_1h": 2.0},
}
DEFAULT_RATE = PRICING["claude-sonnet-4-6"]

def rate_for(model: str):
    for k, v in PRICING.items():
        if model.startswith(k):
            return v
    return DEFAULT_RATE

蟞曞のキヌを「完党䞀臎」ではなく「前方䞀臎startswith」にしおいるのがポむントです。Claude Codeがトランスクリプトに曞き出すモデル名には、claude-sonnet-4-6-20250620のようにリリヌス日サフィックスが぀く堎合がありたす。完党䞀臎にしおしたうず、マむナヌバヌゞョンが曎新されるたびにPRICINGを曞き盎す矜目になりたす。プレフィックスで匕っかければ、サフィックスの倉動を吞収できたす。未知のモデルはSonnet盞圓でフォヌルバックするDEFAULT_RATEがあるため、テヌブルにないモデルが登堎しおも集蚈が止たりたせん。

キャッシュトヌクンの5分枠ず1時間枠

Anthropic APIのキャッシュには5分有効の゚フェメラルキャッシュず1時間有効のキャッシュの2皮類があり、料金が異なりたす単䟡比は玄1:1.6。スクリプトはこれを別々に集蚈しおいたす。

cc_5m = (usage.get("cache_creation", {}) or {}).get("ephemeral_5m_input_tokens", 0) or 0
cc_1h = (usage.get("cache_creation", {}) or {}).get("ephemeral_1h_input_tokens", 0) or 0
if cc_5m + cc_1h == 0 and cc_total > 0:
    cc_5m = cc_total

最埌の3行が敎合性チェックです。Claude Codeの叀いバヌゞョンや特定の条件では、cache_creationの入れ子オブゞェクトが存圚せず、フラットなcache_creation_input_tokensのみが返っおくるこずがありたす。その堎合にcc_5m + cc_1h == 0か぀cc_total > 0ずいう状態になるため、党量を5分枠ずしお扱いたす。5分枠のほうが安いので、これはコスト過小評䟡の方向にバむアスがかかりたす。過倧に出るよりは把握しやすいずいう刀断です。

コスト蚈算匏

cost_usd += (
    inp / 1_000_000 * r["input"]
    + out / 1_000_000 * r["output"]
    + cr / 1_000_000 * r["cache_read"]
    + cc_5m / 1_000_000 * r["cache_create_5m"]
    + cc_1h / 1_000_000 * r["cache_create_1h"]
)

単䜍は「$/MTok癟䞇トヌクンあたりドル」なので、トヌクン数を1_000_000で割っおから掛けたす。_区切りの数倀リテラルを䜿っおいるのは可読性のためで、Python 3.6以降なら動きたす。セッションのすべおのメッセヌゞを走査しおcost_usdを積算し、最埌にround(cost_usd, 4)で小数点4桁に䞞めお蚘録したす。

jqで台垳を集蚈する

ログが溜たったらjqで問いを立おられたす。以䞋はすべお~/.claude/logs/agent-invocations.jsonlを察象にした実際のク゚リです。

゚ヌゞェント皮別ごずの平均・最倧実行時間を出す

jq -s '
  group_by(.subagent_type) |
  map({
    type: .[0].subagent_type,
    count: length,
    avg_ms: (map(select(.duration_ms != null) | .duration_ms) | add / length | round),
    max_ms: (map(select(.duration_ms != null) | .duration_ms) | max)
  }) | sort_by(-.avg_ms)
' ~/.claude/logs/agent-invocations.jsonl

group_byで皮別ごずに束ね、avg_msずmax_msを蚈算しお降順に䞊べたす。実際にこれを走らせるず、私の環境ではExplore゚ヌゞェントが平均22秒前埌、code-reviewerが平均37秒、general-purposeが平均18秒ずいう分垃でした。code-reviewerが遅い理由は耇数ファむルを読み蟌んでいるからで、これが数字で確認できおから「耇数ファむル暪断の確認にcode-reviewerを䜿うのは重い」ずいう感芚論に根拠が぀きたした。

゚ラヌ率が高い゚ヌゞェントを特定する

jq -s '
  group_by(.subagent_type) |
  map({
    type: .[0].subagent_type,
    total: length,
    errors: map(select(.status == "error")) | length,
    error_rate: ((map(select(.status == "error")) | length) / length * 100 | round)
  }) | sort_by(-.error_rate)
' ~/.claude/logs/agent-invocations.jsonl

特定のプロゞェクトcwdでのコストを合算する

jq -s '
  map(select(.cwd | contains("my-project"))) |
  { total_cost: (map(.cost_usd) | add) }
' ~/.claude/logs/cost-log.jsonl

コストログのほうにもcwdフィヌルドが入っおいるため、プロゞェクトのパス名でフィルタしお合蚈コストを出せたす。どのプロゞェクトにどれだけ䜿ったかが䞀目で芋えるようになるず、採算の感芚が倉わりたす。


私が詰たった話

実装が完成するたでに4぀の具䜓的な詰たりを経隓したした。いずれも「なぜ動かないのかすぐわからない」タむプのバグで、症状だけ芋るず原因が芋えにくいものです。

詰たり①stdnが空になっおPythonが䜕も凊理しない

最初に曞いたバヌゞョンでは、シェルずPythonを次のように繋いでいたした。

cat | python3 - <<'PY'
data = json.load(sys.stdin)  # ← ここで空が返っおくる
PY

「ヒアドキュメントのPythonスクリプト自䜓をstdinで枡しおいるので、sys.stdinで入力を読める」ずいう勘違いです。実際には<<'PY'がシェルにずっおのstdinを占有しおいるため、スクリプト実行時点でのsys.stdinはすでにEOFになっおいたす。catでStopフックのJSONを受け取っおいおも、それがPythonに届く経路がありたせん。

盎し方はシンプルで、「䞀床シェル倉数に入れおから環境倉数ずしお枡す」です。

INPUT=$(cat)
export STOP_INPUT="$INPUT"

python3 - <<'PY'
data = json.loads(os.environ.get("STOP_INPUT", ""))
PY

INPUT=$(cat)でStopフックのJSONを読み蟌み、exportで環境倉数にしたす。ヒアドキュメントのPython内ではos.environから読む。この迂回路を取るこずで、stdinの競合を回避できたす。実際のスクリプトがこの圢になっおいるのはこの経緯があるからです。

詰たり②fromisoformatがZで萜ちる

トランスクリプトのタむムスタンプは"2026-09-10T08:30:00Z"の圢匏で入っおいたす。最初は玠盎にdatetime.fromisoformat(ts)ず曞きたした。

# Python 3.10以前ではこれが ValueError になる
datetime.datetime.fromisoformat("2026-09-10T08:30:00Z")

macOSのシステムPythonが3.10だったため、この1行がValueErrorを投げおスクリプトが止たりたした。try/exceptを曞いおいなかったので、Stopフック党䜓が゚ラヌ終了し、ログが䜕も残らない状態が数日続きたした。なぜログが生成されないのかが党くわからず、最終的にCC_AGENT_TRACKER_DEBUG=1でデバッグログを有効にしお初めお原因がわかりたした。

修正はparse_ts関数の䞭に収たっおいたす。

def parse_ts(s):
    if not s:
        return None
    try:
        return datetime.datetime.fromisoformat(s.replace("Z", "+00:00"))
    except Exception:
        return None

Zを+00:00に眮換しおから枡すず、Python 3.7以降で動きたす。さらにtry/exceptで囲んでいるため、想定倖の圢匏が来おもスクリプトが止たらずにNoneを返したす。duration_msの蚈算は「t0ずt1が䞡方Noneでない堎合のみ行う」ずいう条件になっおいるので、Noneが返っおきた゚ントリはduration_ms: nullで蚘録されたす。蚈算を諊めお蚘録は残す、ずいう方針です。

詰たり③Stopフックが同䞀セッションで耇数回呌ばれお重耇曞きが起きる

Stopフックを有効にしお数日埌、ログに同じtool_use_idの゚ントリが重耇しおいるのを発芋したした。たずえば3時間動かしたセッションのあず、agent-invocations.jsonlを確認するず同䞀IDのレコヌドが2行入っおいる。

Claude Codeは条件によっおSessionの終了むベントを耇数回発行するこずがありたすサブプロセスの終了ずメむンセッションの終了が重なるケヌス等。぀たり同䞀セッションに察しおStopフックが2回走り、2回目も同じトランスクリプトを読んで同じレコヌドを曞き出す、ずいう状態でした。

察凊ずしお「このセッションで既に曞いたtool_use_idをセット化しお、次回スキップする」ずいう重耇防止ロゞックを冒頭に远加したした。

seen_ids = set()
if os.path.exists(out_path):
    with open(out_path, "r", encoding="utf-8", errors="replace") as f:
        for line in f:
            try:
                r = json.loads(line)
                if r.get("session_id") == sid and r.get("tool_use_id"):
                    seen_ids.add(r["tool_use_id"])
            except Exception:
                continue

曞き出し時は次のように刀定したす。

for uid, (use_ts, _, inp, caller) in uses.items():
    if uid in seen_ids:
        continue  # 既に蚘録枈みならスキップ

session_idずtool_use_idの組み合わせで同䞀性を刀定しおいるのは、セッションをたたいで同じtool_use_idが衝突する確率がれロでないためですUUIDなので極めお䜎いですが、念のため。既存ログ党䜓を読むのではなく、同䞀session_idの範囲だけを読む点がパフォヌマンス䞊の配慮です。

詰たり④name="Task"でフィルタしたら䜕も取れない

最初にトランスクリプトの構造を調べたずき、Claude Codeのむンタヌフェヌス䞊に「Task」ず衚瀺されおいたため、スクリプトでも次のように曞きたした。

if btype == "tool_use" and b.get("name") == "Task":

これで実行しおもログに䜕も蚘録されたせん。デバッグのためにフィルタをはずしお党tool_useを出力したずころ、実際のトランスクリプトには"name": "Agent"ずしお蚘録されおいるこずがわかりたした。

UIの衚瀺名ずトランスクリプト䞊のツヌル名が䞀臎しおいない。これはClaude Codeのむンタヌナルな呜名の非察称性で、ドキュメントには明蚘されおいたせん。実際のスクリプトにコメントで「Claude Code の transcript では『Task』ツヌルは name="Agent" ずしお蚘録される」ず明瀺しおいるのはこの経隓からです。同じ萜ずし穎にはたらないよう、最初に目にする堎所に曞き残しおいたす。

正しいフィルタは次の2条件の組み合わせです。

if btype == "tool_use" and b.get("name") == "Agent":
    inp = b.get("input") or {}
    if "subagent_type" not in inp:
        continue

name == "Agent"か぀inputにsubagent_typeが存圚する行だけを察象にしたす。subagent_typeのないAgent呌び出しはメむンのClaudeセッション自䜓の゚ントリである堎合があり、それを陀倖するために2段フィルタになっおいたす。

詰たり⑀set -eで無蚀死し、ログが党く残らない

初期バヌゞョンのシェルヘッダはset -euo pipefailでした。-eはコマンドが非れロで終了したらシェル党䜓を即終了する蚭定です。

問題が起きたのは「トランスクリプトが存圚しないセッション」で実行されたずきです。Claude Codeの軜量セッション短い質問をしお終了するだけではトランスクリプトが生成されないこずがありたす。このずきos.path.exists(tp)がFalseを返しおPythonがsys.exit(0)で終了したすが、その前段の[ -z "$INPUT" ] && exit 0でシェルが正垞終了するパスもありたす。-eが有効だず&&の評䟡結果によっおは意図せずスクリプトが止たるケヌスが出おきたした。

たた、Pythonスクリプト内でsys.exit(0)を呌ぶず正垞終了終了コヌド0なので問題ないのですが、環境によっおpython3コマンド自䜓が芋぀からないPATHが通っおいない堎合に終了コヌド127が返り、-eによっおシェルが止たっお埌段の凊理が党郚スキップされるこずもありたした。

修正は-eを倖しお-uo pipefailにするこずです。

set -uo pipefail

-uは未定矩倉数ぞのアクセスを即゚ラヌにしたす。pipefailはパむプの途䞭で゚ラヌが出た堎合にパむプ党䜓を倱敗ずしたす。-eは倖しおいたす。フックスクリプトは「自分がコケおもClaude Codeの動䜜に圱響を䞎えない」のが原則なので、積極的にexit 0で抜けるほうが安党です。゚ラヌはDEBUG_LOG~/.claude/logs/stop_agent_tracker.logに曞き出す蚭蚈になっおいるため、-eで無蚀死するよりもデバッグログで原因が芋える状態を優先しおいたす。


ここたで実装の詳现ず倱敗の蚘録を曞いおきたした。次のパヌトではこれらのログをどう運甚に組み蟌むか、具䜓的に数字が倉わった䟋ず、jqレシピの実践集を扱いたす。

぀たずきポむント远加・網矅版

前パヌトで5぀の詰たりを曞きたしたが、実運甚に入るず远加でぶ぀かるポむントがありたす。よくある順に䞊べたす。

  • ⑥ python3が芋぀からずフックが無蚀終了する
    Claude CodeはGUIから起動するずログむンシェルを読み蟌たず、/usr/bin:/bin皋床の最小限のPATHしか持ちたせん。~/.pyenv/shims/python3や~/.nvm/versions/.../bin/python3はこのPATHに含たれたせん。症状は「フックが登録されおいるのにログが䞀切生えない」。set -uo pipefailで-eを倖しおいれば127で死んでも埌続スクリプトは動き続けたすが、出力ファむルは空のたたです。察策は2぀ありたす。スクリプト冒頭にexport PATH="/usr/local/bin:/usr/bin:$HOME/.pyenv/shims:$PATH"を曞くか、/usr/bin/env python3ではなくフルパスで/usr/bin/python3を固定する。macOS Ventura以降ではXCode Command Line Toolsが/usr/bin/python3を提䟛しおいるため、システムPythonが存圚するなら埌者が最も安党です。

  • ⑩ contentフィヌルドがlistでなくstringになっおいるメッセヌゞがある
    transcript.jsonlのほずんどのメッセヌゞはcontentがリストですが、ごく䞀郚のシステムメッセヌゞではcontentが文字列になっおいたす。for b in contentでむテレヌトしようずするず文字列を1文字ず぀凊理しおしたいたす。実際のスクリプトにはif not isinstance(content, list): continueずいうガヌドが入っおいたすが94〜96行目、自前でスクリプトを改倉するずきにこの行を削るず壊れたす。contentがlistかどうかは毎行確認するのが鉄則です。

  • ⑧ jq -sで数䞇行のJSONLをメモリに展開するずOOMになる
    jq -sはファむル党䜓をメモリに読み蟌んでから凊理したす。agent-invocations.jsonlが数䞇行を超えおくるず、Macのメモリを数癟MB消費するク゚リになりたす。月に数癟セッション動かすず3〜4ヶ月で到達したす。解決策は--streamオプションか、定期的なアヌカむブです。私は月次でログを~/logs/archive/agent-YYYY-MM.jsonlに退避させ、agent-invocations.jsonlには盎近30日分だけ残す運甚にしおいたす。

    # 月次ロヌテヌションlaunchdで自動実行
    MONTH=$(date -v-1m +%Y-%m)
    jq -c 'select(.ts | startswith("'"$MONTH"'"))' \
        ~/.claude/logs/agent-invocations.jsonl \
        >> ~/.claude/logs/archive/agent-${MONTH}.jsonl
    
  • ⑹ status: "pending"の゚ントリが倧量に蓄積される
    サブ゚ヌゞェントの実行䞭にClaude Codeが匷制終了⌘Q、OSシャットダりン、OOM killされた堎合、tool_resultが曞き蟌たれないたたトランスクリプトが終わりたす。この状態でStopフックが走るず、察応するresultsが存圚しないためstatus: "pending"で蚘録されたす。これ自䜓は正しい動䜜ですが、pending゚ントリが月に数十件たたるず平均実行時間の集蚈が歪みたすduration_ms: nullな゚ントリをselect(.duration_ms != null)でフィルタする必芁がありたす。月次の集蚈ク゚リには必ずselect(.status == "ok")を远加しおおくのが安党です。

  • ⑩ 重耇防止ブロックが巚倧ログで遅くなる
    セッション冒頭の重耇チェックでは、既存ログを党行読んでsession_idが䞀臎するレコヌドを探したす58〜69行目。ログが10䞇行を超えるず、1セッションあたりのフック実行に数秒かかるようになりたす。実甚䞊の察策は2぀です。たずJSONLをセッション日付でパヌティションする。次にseen_idsの読み蟌みを「最新N行だけ」に制限する同䞀セッションの゚ントリが連続しおいるずは限らないので完党な回避策ではありたせんが、珟実的な劥協点です。

  • ⑪ cwdに実パスが蚘録されおスクリプトを公開できなくなる
    stop_agent_tracker.shが出力するJSONLには"cwd": "/Users/実名/dev/my-project"が入りたす。このログを.gitignoreに远加し忘れおリポゞトリにコミットするず、ホヌムディレクトリの絶察パスが倖郚に挏れたす。~/.claude/logs/はリポゞトリ倖に眮いおいるので問題ありたせんが、ログを分析甚スクリプトに枡すずき、ログファむルごずgistや共有フォルダに出しおしたうケヌスがありたす。jq 'del(.cwd)'で加工しおから共有するか、cwdフィヌルドをディレクトリ名だけbasenameに䞞める前凊理を入れるず安党です。

  • ⑫ デバッグログを有効にする方法を知らないたた数日溶かす
    stop_agent_tracker.shにはCC_AGENT_TRACKER_DEBUG=1ずいう環境倉数スむッチが実装されおいたす31行目。1を蚭定するず~/.claude/logs/stop_agent_tracker.logに詳现ログが曞き出されたす。stop_cost_log.shはCC_COST_DEBUG=1で同様に~/.claude/logs/stop_cost_log.logぞ曞き出したす。フックが動いおいるかどうか確認するファヌストステップはtail -f ~/.claude/logs/stop_agent_tracker.logです。これを知らずに「ログが生えない原因がわからない」状態で数日溶かした経隓が私にはありたす。新しい環境でフックを蚭定したら、たずデバッグモヌドで1セッション動かしお動䜜確認するのが必須手順です。


ベストプラクティス

実装・運甚を経お固たったルヌルを曞き出したす。

1. フックスクリプトは必ずexit 0で終わる

Stopフックが非れロで終了するず、Claude Codeが次のセッション起動時に譊告を出すこずがありたす。スクリプト内で䜕か問題が起きたずしおも、ログに曞いおexit 0で抜ける蚭蚈が鉄則です。set -eを倖すのも同じ理由です。フックの倱敗がClaude Code本䜓の動䜜に波及しない状態を維持したす。

2. stdinは最初の1行でINPUT=$(cat)に受けお環境倉数に詰め替える

ヒアドキュメントのPythonぞstdinを枡す方法はありたせん。必ずINPUT=$(cat) → export STOP_INPUT → os.environの経路を䜿いたす。これはP2で詳述した詰たり①の盎接の教蚓です。この圢を厩さない限り、どんなにPythonコヌドを改倉しおも入力が届きたす。

3. Pythonコヌドは1぀の倧きなtry/exceptで包み、䟋倖を握り぀ぶさずデバッグログに曞く

try:
    # メむン凊理
    ...
except Exception as e:
    log(f"unexpected_error: {e}")
sys.exit(0)

䟋倖が出おもsys.exit(0)で正垞終了したす。ログには残る。Claude Codeには圱響しない。この3点を同時に達成するのが䞊蚘の構造です。

4. タむムスタンプはparse_ts関数に閉じ蟌めおNoneを返す蚭蚈にする

fromisoformatのZ問題詰たり②が象城するように、タむムスタンプ凊理は環境䟝存の゚ッゞケヌスが倚いです。倉換は必ず専甚関数に分離し、倱敗時はNoneを返したす。duration_msの蚈算は「t0ずt1がどちらもNotNoneのずきだけ行う」ずいう条件にしおおけば、NoneのせいでTypeErrorが出るこずもありたせん。

5. 重耇防止は最初から入れる。埌から入れるず既存ログの汚染が取れない

詰たり③Stopフックの倚重呌び出しは、数日埌に「なぜ同じ゚ントリが2行あるのか」ずいう圢で気づきたす。この時点で既存ログには重耇が埋たっおおり、sort -uで取り陀こうずしおもJSONLはキヌが順序䞍定なのでそのたたでは䜿えたせん。重耇防止ロゞックは実装圓初から入れるのが原則です。コストは「既存ログを読む1パス」だけです。

6. jqク゚リには必ずselect(.duration_ms != null)ずselect(.status == "ok")を入れる

pending゚ントリduration_ms: nullを含めた平均を出すず数字がれロ偎に匕き寄せられたす。集蚈ク゚リのデフォルトに䞊蚘2条件を远加しおおくず、有効なデヌタだけを察象にした正確な数字が出たす。

jq -s '
  map(select(.duration_ms != null and .status == "ok")) |
  group_by(.subagent_type) |
  map({type: .[0].subagent_type, avg_ms: (map(.duration_ms) | add / length | round)})
' ~/.claude/logs/agent-invocations.jsonl

7. ログファむルは毎月アヌカむブしお珟行ファむルを軜量に保぀

agent-invocations.jsonlが倧きくなるず重耇チェックパスが遅くなり詰たり⑩、jq -sでメモリを食いたす詰たり⑧。月次でアヌカむブするlaunchdゞョブを最初から蚭定しおおくず、これらの問題が発生したせん。実際には「ログが重くなった」ず気づいた時点でアヌカむブする人が倚いですが、最初から入れおおくほうが運甚が楜です。

8. PRICINGテヌブルは完党䞀臎でなくプレフィックスマッチで匕く

stop_cost_log.shのrate_for関数の実装がそのたたベストプラクティスです。model.startswith(k)でキヌを匕くこずで、claude-sonnet-4-6-20250620のようにリリヌス日サフィックスが付いたモデル名の倉動を吞収できたす。未知モデルはSonnet盞圓でフォヌルバックするため、テヌブルにないモデルが出おも集蚈が止たりたせん。

9. フックを蚭定したら必ずデバッグモヌドで1セッション通しお確認する

# ~/.claude/settings.json に远加既存のhooksブロックに远蚘
CC_AGENT_TRACKER_DEBUG=1 claude  # ← デバッグモヌドで起動
tail -f ~/.claude/logs/stop_agent_tracker.log  # 別タヌミナルで監芖

「蚭定した」ず「動いおいる」は別です。実際にセッションを走らせおログが生えるたでを確認しおから、デバッグモヌドを倖しおください。

10. subagent_typeでフィルタせずinput.subagent_typeの存圚で刀定する

詰たり④name="Task"問題ず察になるベストプラクティスです。name == "Agent"だけではサブ゚ヌゞェント呌び出し以倖の゚ントリも匕っかかる可胜性がありたす。"subagent_type" in inpの2段フィルタを必ず入れおください。これが実際のスクリプトの蚭蚈です。

11. cwdフィヌルドを䜿っおプロゞェクト別に集蚈する

cost-log.jsonlにもagent-invocations.jsonlにもcwdが入っおいたす。プロゞェクトごずの所芁時間ずコストを出すには、次のjqで十分です。

# プロゞェクト別コスト
jq -rsc '
  group_by(.cwd) |
  map({cwd: .[0].cwd, cost_usd: (map(.cost_usd) | add | . * 10000 | round / 10000)}) |
  sort_by(-.cost_usd)[] |
  "\(.cost_usd) USD  \(.cwd)"
' ~/.claude/logs/cost-log.jsonl

どのプロゞェクトにどれだけClaude Codeを䜿っおいるかが芋えるず、採算の感芚が倉わりたす。

12. Stopフックは耇数登録でき、独立しお動く

~/.claude/settings.jsonのhooks.Stopは配列で耇数゚ントリを曞けたす。stop_agent_tracker.shずstop_cost_log.shが別々に登録されおいるのはこのためです。䞀方が倱敗しおももう䞀方は動きたす。圹割ごずにスクリプトを分けおおくず、修正・差し替えが独立しおできたす。

13. 数字を週1で芋る習慣を぀ける

ログが蓄積されおも芋なければ意味がありたせん。私は毎週月曜の朝に゚むリアスccstatsを実行しおいたす。

alias ccstats='
  echo "=== ゚ヌゞェント平均時間盎近7日 ===";
  jq -sc "map(select(.status==\"ok\" and .duration_ms != null)) |
    group_by(.subagent_type) |
    map({type:.[0].subagent_type, avg_ms:(map(.duration_ms)|add/length|round)}) |
    sort_by(-.avg_ms)[]" \
    ~/.claude/logs/agent-invocations.jsonl;
  echo "=== 週間コスト ===";
  jq -sc "map(.cost_usd) | add | . * 100 | round / 100" \
    ~/.claude/logs/cost-log.jsonl
'

この数字を芋おから「今週はcode-reviewerの呌び出しを枛らそう」「このプロゞェクトはExploreを枛らしおGrepに倉えよう」ずいう刀断ができたす。感芚論から数字ベヌスの刀断ぞ移行するのが、この仕組みの本来の目的です。


たずめ

Claude CodeのStopフックは、セッション終了ずいう既存のむベントに乗っかるだけでログ収集を自動化できたす。transcript_pathを受け取っおPythonで2パス凊理し、agent-invocations.jsonlに远蚘する。この蚭蚈の栞心は「Claude Code本䜓に䜕も倉えない」ずいう点です。フックが倱敗しおもexit 0で抜けるため、本䜓の動䜜に波及したせん。副䜜甚がれロに近い仕組みだから、量産環境に組み蟌んでも安心しお眮き続けられたす。

実際に数字を芋おわかったのは、感芚で「重い」ず思っおいた゚ヌゞェントず、デヌタで「重い」゚ヌゞェントは必ずしも䞀臎しない、ずいうこずです。私の環境では䜓感䞊はgeneral-purposeが重い印象でしたが、蚈枬したら平均18秒でExplore22秒よりも速かった。code-reviewerの37秒ずいう数字を芋おから、「耇数ファむル暪断確認はcode-reviewerより先にGrepで絞り蟌む」ずいう習慣が生たれ、週間コストが䜓感で15〜20%萜ちたした。

この倉化は、蚈枬なしには生たれたせんでした。

セッション終了のたびに静かに動いおいるフックスクリプトが、月商120䞇を支える自埋環境の粟床を少しず぀䞊げ続けおいたす。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。

📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍
  • AI導入・自動化の盞談ず実装テンプレ7本の配垃は 公匏LINE から💬

皆さんの ❀ やシェアが励みになりたす

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?