Claude Codeで古いissueを棚卸しし、閉じてよいものを根拠つきで分ける
gh issue listで最終更新が古いissueをJSONで取り出し、claude -pに修正済み・再現しない・情報不足へ振り分けさせます。閉じるのは人が行う手順です。
放置されたissueが数百件たまると、新しい報告が埋もれます。かといって「半年更新がないから」という理由だけで閉じると、まだ困っている人の報告まで消してしまいます。閉じてよいのは、修正済みと分かるもの、再現しないと確かめられたもの、報告者に聞いても返事がないものに限られます。この仕分けは、issue本文とコードを読み比べる作業です。そこでClaude Codeに仕分けの下書きを作らせ、閉じる操作は人が実行する流れを、コマンドつきで示します。
取得はgh、分類はclaude -p、実行は人
役割を3つに分けます。
- 取得:
gh issue listが候補をJSONで出す。ここでは書き込みをしない - 分類:
claude -pがJSONを読み、リポジトリのコードを調べて判定と根拠を返す - 実行: 人が結果を見て、
gh issue closeを自分の手で打つ
claude -pは非対話で動き、標準入力のパイプを読めます。--output-format jsonと--json-schemaを組み合わせると、structured_outputフィールドにスキーマどおりの結果が入ります。結果を後続のスクリプトに渡せるので、分類の出力を「閉じるコマンドの下書き」に変換する段が作れます。
同じ「取得は機械、判定は人の目で確認」という分担は、アラートを仕分けるDependabotアラートの記事やCodeQL誤検知の記事でも使っています。そちらの対象は脆弱性アラートと静的解析の指摘です。issueは入力が自由文なので、根拠の取り方と分類の基準が変わります。
gh issue listで古い候補だけ取り出す
gh issue listは既定で、直近の30件の未クローズissueだけを返します。棚卸しでは--limitで件数を増やし、--searchで「最終更新が古い順」に絞ります。
gh issue list -R OWNER/REPO --state open --limit 200 \
--search "is:issue is:open updated:<2026-04-28 sort:updated-asc" \
--json number,title,url,body,labels,updatedAt,comments,closedByPullRequestsReferences \
> issues-raw.jsonupdated:<2026-04-28は、最終更新が6か月以上前のissueを意味します。日付は自分のチームの基準に置き換えてください。sort:updated-ascは古い順の並べ替えです。
検索の修飾子は、GitHubの検索構文の表にあるものを使います。棚卸しで効くのは次の3つです。
| 修飾子 | 絞り込み | 使いどころ |
|---|---|---|
-linked:pr | 絞り込みPRが紐づいていないissue | 使いどころ修正のあてがないものだけを見たいとき |
linked:pr | 絞り込みPRが紐づくissue | 使いどころ「修正済み」の根拠候補を先に拾いたいとき |
no:label | 絞り込みラベルがないissue | 使いどころ誰も一次仕分けをしていない山を見つけるとき |
--jsonに渡せるフィールド名はgh issue listのマニュアルに一覧があります。上のコマンドで使ったclosedByPullRequestsReferences、comments、updatedAt、labelsはいずれも載っています。
jqで渡す項目を絞る
生のJSONには、コメント本文や担当者の情報がすべて入ります。そのまま渡すとトークンを食うので、判定に使う項目だけに削ります。
jq '[.[] | {
number, title, url, updatedAt,
labels: [.labels[].name],
commentCount: (.comments | length),
linkedPrs: [.closedByPullRequestsReferences[].number],
body: ((.body // "")[:1500])
}]' issues-raw.json > issues.jsonbodyは先頭の1,500字で切っています。再現手順は冒頭に書かれることが多いためですが、長いログを貼ったissueでは手順が後ろに回ることもあります。切った分は判定の弱さにつながるので、needs_infoが増えたら字数を見直す箇所です。commentsの中身はここでは使わず、件数だけ渡します。コメントの最後に「直りました」と書かれたissueは、本文ではなくコメントに根拠があります。そうしたissueは、後述の見直しの節で人が拾います。
三分類の基準を先に決める
Claudeに「閉じてよいか」を丸ごと聞くと、曖昧な理由で閉じる提案が混ざります。判定の選択肢と、各選択肢に必要な根拠を先に固定します。
| verdict | 意味 | 必要な根拠 |
|---|---|---|
fixed | 意味すでに直っている | 必要な根拠修正PRの番号、またはコード上で該当箇所が変わっていること |
cannot_reproduce | 意味今の版では起きないと思われる | 必要な根拠再現手順の有無と、コード側で条件が変わっていること |
needs_info | 意味報告者に確認が要る | 必要な根拠欠けている情報(版、手順、ログ)の名指し |
keep_open | 意味閉じない | 必要な根拠判断できない理由 |
選択肢にはkeep_openを必ず入れます。入れないと、迷ったissueが無理にどれかへ押し込まれます。
cannot_reproduceは、人が実際に動かして確かめるまで「再現しない」の確定になりません。Claudeにはコードを読ませるだけで、コマンドを実行させないからです。出力名をcannot_reproduceにしているのは、「再現確認の候補」として扱うための割り切りです。
スキーマは次のとおりです。根拠の文章と、根拠にしたPR番号を必須にしています。
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"number": {"type": "integer"},
"verdict": {"type": "string",
"enum": ["fixed", "cannot_reproduce", "needs_info", "keep_open"]},
"evidence": {"type": "string"},
"evidence_prs": {"type": "array", "items": {"type": "integer"}}
},
"required": ["number", "verdict", "evidence", "evidence_prs"]
}
}
},
"required": ["items"]
}これをschema.jsonとして保存します。--json-schemaはclaude -p専用のフラグで、スキーマが不正だとError: --json-schema is not a valid JSON Schemaで止まります。
読み取りだけの権限で実行する
分類の段階で必要なのは、コードを読む権限だけです。issueを閉じる権限も、ファイルを書く権限も渡しません。
cat issues.json | claude -p \
"各issueについて、このリポジトリのコードと履歴を調べて分類してください。
修正済みと判断するときは、根拠のPR番号をevidence_prsに入れてください。
根拠を示せないissueはkeep_openにしてください。" \
--permission-mode dontAsk \
--allowedTools "Read,Bash(git log *)" \
--max-budget-usd 3.00 \
--output-format json \
--json-schema "$(cat schema.json)" \
> result.jsonポイントは3つあります。
--permission-mode dontAskは、承認が要る呼び出しを全部拒否するモードです。許可ルールに書いたものと、承認が要らない読み取りだけが動きます--allowedToolsにReadとBash(git log *)だけを入れ、git logで履歴を引けるようにします。末尾の*の前の空白は、git log-xxxのような別コマンドを巻き込まないためのものです--max-budget-usdは、見積もり上の費用がその額に達したら止めるための上限です。数百件を一度に渡すと費用が読みにくいので、先に上限を置きます。上限は目安で、超えることがあります
CIやスクリプトから呼ぶときは、--bareを付けると手元の設定やフックを読まずに起動します。ただし--bareは購読のログインを使わず、ANTHROPIC_API_KEYが必要です。権限をさらに細かく切る方法はallowedTools/disallowedToolsの記事に、-pの使い方全般はヘッドレスモードの記事にあります。
結果をそのまま信じない
返ってきたresult.jsonは、閉じる前に機械で1回、人の目で1回確かめます。
機械で確かめるのは、fixedの根拠に挙げたPRが実在してマージ済みかどうかです。
jq -r '.structured_output.items[]
| select(.verdict=="fixed") | .evidence_prs[]' result.json \
| sort -u | while read n; do
gh pr view "$n" -R OWNER/REPO --json number,state,mergedAt \
--jq '"\(.number)\t\(.state)\t\(.mergedAt)"'
donestateがMERGEDでないもの、mergedAtが空のものは、fixedからkeep_openに戻します。fixedなのにevidence_prsが空のものも同じ扱いです。コードの変化だけを根拠にしたfixedは、人が差分を見るまで確定しません。
人の目で見るのは、次のissueです。
- ラベルに
bugやregressionがあるもの。直ったように見えて別経路で残っていることがあります - コメントが多いもの。後から同じ症状の報告者が増えているかもしれません
needs_infoの欠落情報が実際には本文にあるもの。1,500字で切った影響が疑われます
件数の内訳は、jqのグループ化で一覧にできます。
jq -r '.structured_output.items | group_by(.verdict)
| map("\(.[0].verdict)\t\(length)") | .[]' result.json結果の形の例
スキーマに沿った出力は、次のような形になります。数値や文面は説明用に作った例で、実際の出力ではありません。
{
"items": [
{"number": 1203, "verdict": "fixed",
"evidence": "起動時クラッシュの原因だった null 参照が PR 1342 で修正済み",
"evidence_prs": [1342]},
{"number": 1187, "verdict": "needs_info",
"evidence": "再現手順とライブラリの版が本文にない",
"evidence_prs": []},
{"number": 1150, "verdict": "keep_open",
"evidence": "該当モジュールが複数回書き換わっており、同じ症状かを判断できない",
"evidence_prs": []}
]
}keep_openの理由が書かれていれば、次の棚卸しで条件を変えるときの手がかりになります。理由が空のkeep_openは、プロンプトの指示が弱いサインです。
fixedのevidenceは閉じるときの--commentにそのまま入ります。報告者が読んで経緯を追える文面になるよう、プロンプトで「PR番号と修正内容を1文で」と指定しておくと、後から読み返す手間が減ります。
件数が多いときは分けて渡す
標準入力でパイプできるデータは10MBまでで、超えるとclaude -pがエラーで終了します。数百件のissueを1回に渡す必要はありません。jqで50件ずつに分けると、1回あたりの費用も読みやすくなります。
jq -c '. as $a | [range(0; length; 50)] | map($a[.:.+50]) | .[]' \
issues.json > batches.jsonl
i=0
while read -r batch; do
i=$((i+1))
echo "$batch" | claude -p "(上と同じプロンプト)" \
--permission-mode dontAsk --allowedTools "Read,Bash(git log *)" \
--output-format json --json-schema "$(cat schema.json)" \
> "result-$i.json"
done < batches.jsonl結果はjq -sで1つに束ねられます。途中で失敗しても、できあがったresult-N.jsonは使えます。
出力にstructured_outputフィールドがない場合は、Claudeが依頼を断った、またはスキーマに合う出力が得られなかったときです。後続のjqを実行する前に、このフィールドの有無を確かめる行を足しておくと、空の入力で閉じるコマンドを作ってしまう事故を防げます。
jq -e '.structured_output.items' result.json > /dev/null \
|| echo "structured_output がありません。閉じる作業を止めます"閉じる操作は人が打つ
ここまでが下書きで、閉じる操作はまだ何もしていません。gh issue closeは--reasonにcompleted、not planned、duplicateの3つを取り、--commentで閉じるときのコメントを残せます。重複として閉じるなら--duplicate-ofで元のissueを指します。
確認を終えたfixedだけを対象に、コマンドを画面に出して目で追います。
jq -r '.structured_output.items[]
| select(.verdict=="fixed")
| "gh issue close \(.number) -R OWNER/REPO --reason completed --comment \(.evidence | @sh)"' \
result.json > close-fixed.sh
cat close-fixed.sh@shは、根拠の文章に含まれる引用符をシェル向けに安全にするjqの機能です。close-fixed.shの中身を読み、問題がなければbash close-fixed.shで実行します。書き込み権限のあるトークンは、このターミナルにだけあれば足ります。分類を走らせた環境には置きません。
needs_infoは閉じません。報告者に聞くコメントを付けて、返事を待ちます。
jq -r '.structured_output.items[]
| select(.verdict=="needs_info")
| "gh issue comment \(.number) -R OWNER/REPO --body \((.evidence + " 上記の情報を教えてください。") | @sh)"' \
result.json返事がなければ、次の棚卸しでupdated:の条件に再び入ります。期限を決めて閉じる運用にするかどうかは、チームの方針で決めることです。
cannot_reproduceは、人が最新版で実際に手順を試してからnot plannedとして閉じるか、completedとして閉じるかを選びます。どちらの理由が自然かは、リポジトリの慣習によります。
古さだけで閉じない
この手順では、最終更新の古さを「候補を絞る条件」として使い、閉じる根拠にはしていません。古いissueが全部不要なわけではありません。
- 古くて静かなissueは、直っていないだけで報告者が諦めた場合があります
- ラベルがないissueは、誰も仕分けていないだけで、重要度が低いとは限りません
- 一度に取れるのは
--limitまでです。--limit 200でも、それ以上古いissueは次回に回ります
閉じる件数を成果にしないことも大事です。指標にするなら、閉じたissueのうち、あとで再オープンされた割合を見ます。再オープンが続くようなら、fixedの根拠を厳しくする合図です。
まとめ
古いissueの棚卸しは、取得をghに、分類をclaude -pに、実行を人に分けると安全に回せます。分類の出力は、修正済みなら根拠のPR番号を必須にし、根拠が示せなければ閉じない設計にします。gh issue listの--searchで候補を古い順に絞り、閉じる前にPRのマージ状態を機械で確かめる段を挟めば、読み違いによる誤クローズを減らせます。最後のgh issue closeは、自分の手で打つところまでを手順に含めてください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。