Claude Media
Claude Codeで古いissueを棚卸しし、閉じてよいものを根拠つきで分ける

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.json

updated:<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.json

bodyは先頭の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)"'
  done

stateが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は、自分の手で打つところまでを手順に含めてください。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. code.claude.com/docs/en/cli-reference
  3. cli.github.com/manual/gh_issue_list
  4. cli.github.com/manual/gh_issue_close
  5. docs.github.com/en/search-github/searching-on-github/searching-issues-and-…
この記事を共有:XはてブLinkedIn