Claude Code ActionでAgent ModeとTag Modeの挙動差異を調査した記録
こんにちは、『家族アルバム みてね』(以下、みてね)でSREを担当している @kohbis です。
みてねでは、anthropics/claude-code-actionを活用したCIワークフローを運用しています。
そのワークフローが、PR上では安定して動くのに、mainブランチでのworkflow_dispatch実行だけターン上限またはタイムアウトに達して失敗する、という現象に遭遇しました。
原因は最終的にclaude-code-actionの2つの実行モード(Tag ModeとAgent Mode)でエージェントに公開されるツールが違う、という点に落ち着きました。
ただ、そこにたどり着くまでにいくつか見当違いの仮説を立てて回り道をしたので、この記事では結論だけでなく調査の道のりも含めて残しておきます。
実測で確認できたことと、あくまで推測にとどまる部分は分けて書きます。
最初にぶつかった現象
(実測) 以下はCIの実行結果から取得した数値です。
同じワークフロー定義、同じプロンプト、同じモデル(Sonnet)なのに、実行結果に極端な差が出ていました。
| 実行条件 | ターン数 | 所要時間 | 結果 |
|---|---|---|---|
| pull_request | 10〜20 | 約5分 | 成功 |
| workflow_dispatch (main) | 60+ | 20分以上(timeout) | 失敗(max-turns到達) |
PR上では毎回安定して収束するのに、mainブランチでは調査から先に進まずターン上限に達するまでループしてしまう、というのが最初にぶつかった現象です。
見当違いだった仮説
(推測) ここで挙げるのは、当時のログを見る前に立てた仮説で、いずれも外れています。
正直なところ、自分は最初モード差だとはまったく思っておらず、もっと単純な原因を疑っていました。
順番に潰していったので、そのまま書いておきます。
(仮説1)max-turnsが足りないだけ
60ターンで打ち切られているのだから、単に上限を上げれば終わるのでは、と最初は考えました。
ただ、PR上では10〜20ターンで終わっている処理です。
同じタスクで3倍以上のターンを使ってまだ終わらないのは、上限の問題ではなく「収束していない」と考えるほうが自然でした。
ここでmax-turnsを上げるのは、症状を悪化させるだけの対症療法だと判断して見送りました。
(仮説2)モデルやプロンプトが違う
次に疑ったのは、実行条件そのものの差です。
しかし、ワークフロー定義・プロンプト・モデルはどれも共通でした。
入力が同じなのに出力の挙動だけ変わるということは、変数はワークフローの外側、つまりactionの実行モードにあるはず、とここでようやく当たりをつけました。
実行モードの違い
claude-code-action v1には、2つの実行モードがあります。CLAUDE.md にも「Mode is auto-detected」と記載されているとおり、モードは自動検出されます。
-
promptが指定されていればAgent Mode -
@claudeメンションを含むコメント、またはtrack_progress指定時にTag Mode
track_progress を有効にするとTag Modeが強制されます。
判定ロジックの実体は src/modes/detector.ts です。
| Tag Mode | Agent Mode | |
|---|---|---|
track_progress |
true(強制) |
false(または未指定) |
| 主な対応イベント |
pull_request, issue_comment
|
workflow_dispatch, schedule など |
| 動作 | action側が環境を構築しPR/Issueに進捗コメント | プロンプトのみでヘッドレス実行 |
みてねのワークフローでは、以下のようにイベントタイプで切り替えていました。
track_progress: ${{ github.event_name == 'pull_request' }}
(実測) これはつまり、「PRはTag Mode、workflow_dispatch (main) はAgent Mode」で動いていた、ということです。
成功する側と失敗する側で、実行モードそのものが違っていた、というのはログとaction定義から確認できました。
ここまでで「モードが違う」ところまでは分かりましたが、まだ「なぜモードが違うと発散するのか」は分かりません。
中身を見る必要がありました。
会話ログによる診断
推測を重ねても仕方ないので、自分でAgent Modeの全会話ログを取ることにしました。
show_full_output を"true"にすると、エージェントの全入出力がCIログに出ます。
- uses: anthropics/claude-code-action@v1
with:
show_full_output: "true" # エージェントのすべての会話をCIログに出力
# ...
(推測) ログをざっと眺めた段階では、「編集で試行錯誤して、PR作成のあたりで右往左往しているのだろう」と考えていました。
ターン数が多いのだから、たくさん書いては直しているのだろう、という思い込みです。
ところが後述のとおり、この推測も実測であっさり否定されます(Edit / Writeは0回でした)。
実測データ
(実測) ここからは、CIログとactionのソースコードから実際に確認できた内容です。
Tag Modeのツール制御
Tag Modeでは、action側が src/modes/tag/index.ts でホワイトリスト方式のツール一覧を組み立て、SDKオプションとして渡していました。
SDK options: {
"allowedTools": [
"Glob", "Grep", "LS", "Read",
"mcp__github_comment__update_claude_comment",
"mcp__github_ci__get_ci_status",
"mcp__github_ci__get_workflow_run_details",
"mcp__github_ci__download_job_log",
"Bash(git add:*)", "Bash(git commit:*)",
"Bash(.../git-push.sh:*)", "Bash(git rm:*)",
"Edit", "Write", "Bash" // ← ユーザーのclaude_argsから連結
]
}
action側が構築するのはGlob〜Bash(git rm:*)までで、Edit/Write/Bashはユーザーのclaude_argsで指定した--allowedToolsが後ろに連結された結果です。
なお、action側はEdit/Writeを意図的にホワイトリストから除外しており、--permission-mode acceptEditsによって自動許可される設計になっています。
いずれにせよ、この一覧にはAgentやサブエージェント起動系のツールが一切含まれていません。
Tag Modeでは、エージェントがサブエージェントを呼び出す手段がありませんでした。
Agent Modeのツール制御
ワークフローでは--allowedTools Read,Glob,Grep,Edit,Write,Bashの6個を指定していました。
ところが、initメッセージに記録された実際のツール一覧は「26個」でした。
"tools": [
"Task", "Bash", "CronCreate", "CronDelete", "CronList",
"Edit", "EnterWorktree", "ExitWorktree", "Glob", "Grep",
"NotebookEdit", "Read", "ReportFindings", "ScheduleWakeup",
"SendMessage", "Skill", "TaskCreate", "TaskGet", "TaskList",
"TaskOutput", "TaskStop", "TaskUpdate", "ToolSearch",
"WebFetch", "Workflow", "Write"
],
"mcp_servers": [
{ "name": "...", "status": "pending" }
],
"agents": [
"claude", "claude-code-guide", "Explore",
"general-purpose", "Plan", "statusline-setup"
]
6個に絞ったはずが、26個のツール + MCPサーバー + 6種類のサブエージェントが使える状態になっていました。
--allowedToolsがフィルタとして機能していなかった、というのがここで初めて分かった事実です。
モード間の差異まとめ
(実測) 以上をソースコードとログから確認できた範囲で表にすると、2つのモードの差異が一目で分かります。
| 項目 | Tag Mode(PR) | Agent Mode(mainブランチ / schedule) |
|---|---|---|
allowedToolsの扱い |
action側がホワイトリストを構築しSDKに渡す | GitHub MCP設定を付与後、claude_argsをCLI引数として連結するのみ |
| init時の利用可能ツール数 | ホワイトリストに限定 | 26個(指定した6個に絞れていない) |
| サブエージェント | 利用不可 | 6種類が利用可能 |
| MCPサーバー | action側が追加(github_comment / github_ci) |
プロジェクトの.mcp.jsonから自動で有効化 |
--permission-mode |
acceptEditsを指定 |
指定なし |
Agent Modeの実際の挙動
(実測) ツール呼び出しの内訳(回数はCIログを集計したもの)を見ると、何が起きていたかがはっきりしました。
| ツール | 呼び出し回数 |
|---|---|
| Bash(grep/find/catなど) | 70+ |
| Read | 40+ |
| Grep | 20+ |
| Glob | 10- |
| Agent(Exploreサブエージェント) | 5- |
| Edit / Write | 0 |
これらの数字は実測値です。
Exploreサブエージェントが数回起動され、Edit / Writeが0回です。
読み取り系だけでターンを使い切り、本来やるべきファイル編集に一度も到達していませんでした。
(推測) 数字の解釈としては、サブエージェントに調査を投げ続けた結果、読み取りフェーズから抜け出せなかった、と見ています。
先ほどの「編集で試行錯誤しているのだろう」という推測は、Edit / Writeが0回という実測で完全に外れていたわけです。
max-turnsが足りなかったのではなく、「そもそも編集フェーズに入れていなかった」わけです。
最初の仮説1が的外れだったことが、ここで実測として裏付けられました。
対処
知っての通り--allowedToolsは自動承認リストであってツールの絞り込みではないので、ツールをコンテキストから除去する--disallowedToolsを併用しました。
claude_args: >-
--model ${{ vars.CLAUDE_MODEL }}
--max-turns 60
--allowedTools Read,Glob,Grep,Edit,Write,Bash
--disallowedTools Agent,Task,mcp__*
この1行を足しただけで、workflow_dispatch (main) の挙動がPR上と同等に戻りました。
(今回のCIでは、いずれのmcpツールも不要だったため mcp__* でまとめて除外しています)
(実測) 修正前後の数字はいずれもCIの実行結果です。
| 実行条件 | ターン数 | 所要時間 | 結果 | |
|---|---|---|---|---|
| 修正前 | workflow_dispatch (main) | 60+ | 20分以上(timeout) | 失敗 |
| 修正後 | workflow_dispatch (main) | 10〜20 | 約7分 | 成功 |
まとめ
今回の調査を振り返ると、遠回りの原因は「実行条件が同じなら挙動も同じはず」という思い込みでした。
実際にはactionの実行モードという外側の変数が効いていて、そこはログを取るまで見えませんでした。
- max-turnsやモデルを疑う前に、まず「実行モードが同じか」を確認する
- ツールを制限するなら
--allowedToolsではなく--disallowedToolsか--toolsを使う - プロジェクトに
.mcp.jsonがある場合、Agent Modeでも自動でMCPサーバーが有効になる - 挙動が発散したら、まず
show_full_output: "true"で実際に公開されているツールを見る
「PRでは動くのにworkflow_dispatchだと発散する」で同じように悩んでいる方がいたら、まずshow_full_outputでツールの公開状況を確認してみてください。
最後までお読みいただきありがとうございました。
Discussion