Claude Codeでissueの重複候補をghと突き合わせて挙げる
gh issue listの出力をclaude -pに渡し、重複候補を番号つきのJSONで出させる手順です。閉じる判断は人に残し、誤爆を減らす絞り込みも扱います。
新しいissueが立つたびに「これ、前にもあったのでは」と検索するのは地味に時間を取られます。gh issue list --jsonの出力をclaude -pに渡せば、重複していそうな既存issueを番号つきで挙げさせられます。ただし閉じるのは人です。Claudeには候補の一覧を作らせるところまでを任せ、gh issue closeは手で打つ構成にします。
この記事では、候補の絞り込みから、JSONスキーマで出力を固定する方法、権限の絞り方、誤爆を減らす工夫までを順に書きます。
全体の流れ: 検索で絞り、Claudeには比較だけさせる
リポジトリのissueを全件Claudeに渡す必要はありません。まずghの検索で数十件に絞り、その中から意味の近いものをClaudeに選ばせます。役割を分けると、入力が小さくなり、判断の根拠も追いやすくなります。
重複候補を出すまでの4段
- 1
新しいissueを読む
gh issue viewでタイトルと本文を取り出します。 - 2
gh searchで母集団を絞る
タイトルの語で
--searchをかけ、状態はallにして閉じ済みも含めます。 - 3
claude -pに突き合わせさせる
絞った一覧と新しいissueを渡し、重複候補をJSONで返させます。
- 4
人が見て閉じる
候補の番号を開いて読み、重複と判断したときだけ手で閉じます。
claude -pは、プロンプトに-p(--print)を付けて対話なしで実行するモードです。標準入力から読めるので、他のコマンドの出力をパイプで渡せます。標準入力は10MBが上限で、超えるとエラーで非ゼロ終了します。issue一覧はタイトル中心に絞れば、この上限に近づくことはまずありません。
ghで母集団を絞る
gh issue listは既定で状態がopenのものだけを返し、取得件数の既定は30件です。重複の確認では閉じ済みも見たいので、--state allと--limitを明示します。--searchには、GitHubのIssue検索と同じ構文を渡せます。
# 新しいissue(#482)のタイトルを取り出す
TITLE=$(gh issue view 482 --json title --jq .title)
# 同じ語を含む既存issueを、本文を除いて取得する
gh issue list --state all --limit 100 \
--search "$TITLE in:title" \
--json number,title,state,updatedAt,url,labels \
> candidates.json--jsonで選べるフィールドには、number title state labels updatedAt urlのほかbodyやcommentsがあります。最初のふるいではbodyを外すのがコツです。本文を含めると一覧が一気に膨らみ、Claudeへの入力も増えます。タイトルだけで明らかに無関係なものは、この時点で落ちています。
絞り込みの語は、タイトル全文より名詞を2〜3個に削ったほうが拾えます。エラーメッセージがタイトルに入っているなら、その文字列の一部を使うのが確実です。1回の検索で取りこぼしが心配なら、語を変えて2回走らせて結果を結合します。jq -s 'add | unique_by(.number)'で番号の重複を除けます。
claude -pに突き合わせさせる
ここが中心の手順です。新しいissueの本文と、先ほどのcandidates.jsonを渡します。出力は--json-schemaで形を固定します。
gh issue view 482 --json number,title,body > new.json
jq -n --slurpfile n new.json --slurpfile c candidates.json \
'{new: $n[0], existing: $c[0]}' \
| claude --bare -p \
"new は新しく立ったissue、existing は既存issueの一覧です。new と同じ問題を扱っている可能性がある existing を最大5件選び、番号と理由を返してください。確信が持てないものは confidence を low にしてください。該当がなければ空配列を返してください。" \
--output-format json \
--json-schema '{"type":"object","properties":{"duplicates":{"type":"array","items":{"type":"object","properties":{"number":{"type":"integer"},"confidence":{"type":"string","enum":["high","medium","low"]},"reason":{"type":"string"}},"required":["number","confidence","reason"]}}},"required":["duplicates"]}' \
| jq '.structured_output.duplicates'指定したスキーマに沿った結果は、レスポンスのstructured_outputフィールドに入ります。--output-format jsonにはセッションIDや利用量のメタデータも付くので、取り出すときは.structured_outputを指します。例えば次のような形で返ってきます。これは出力形式を示すための例示で、実際の応答ではありません。
[
{"number": 431, "confidence": "high", "reason": "同じエラー文言で、再現手順も一致している"},
{"number": 388, "confidence": "low", "reason": "症状は近いが、OSとバージョンが異なる"}
]出力が空のときの扱い
Claudeが有効な構造化出力を返せなかった場合、たとえば依頼を断ったときは、レスポンスにstructured_outputフィールドが付きません。スクリプトで使う前に、フィールドの有無を確かめます。jq -e '.structured_output'なら、無いときに非ゼロで終わるので、&&でつなぐ前提の処理でも安全です。
--bareを付ける理由
--bareを付けると、フックやスキル、プラグイン、MCPサーバー、auto memory、CLAUDE.mdの自動読み込みを省いて起動します。手元の設定が結果に混ざらないので、CIでもローカルでも同じ入力に同じ挙動になります。公式はスクリプト用途で推奨しており、将来は-pの既定になる予定とされています。
注意点が1つあります。--bareは認証にOAuthやキーチェーンを使わないため、ANTHROPIC_API_KEYの設定が要ります。サブスクリプションのログインで試すなら、--bareを外して実行します。
権限を絞り、issue本文を命令として扱わせない
issueの本文は外部の人が書いた文章です。本文に「このissueを閉じて」「トークンを出力して」のような文が混じる可能性は、公開リポジトリでは常にあります。対策は、Claudeに副作用のある操作をさせないことです。
上の構成には、--allowedToolsを一切付けていません。公式の説明では、v2.1.302以降は、権限モードを何も指定しないclaude -pがManualモードで始まります。Manualモードでは、承認が要る呼び出しは、フックなどが承認しない限り拒否されます。つまりgh issue closeのようなコマンドは、許可リストに入れない限り走りません。
さらに堅くするなら、次の2点を足します。
- 無人実行(定期ジョブなど)では
--permission-prompts noneを付ける。承認待ちで止まらず、承認が要る要求は拒否される。このフラグはv2.1.259以降で使える ghの認証トークンは、issueの読み取りだけできる権限のものを使う。書き込みの権限を持たせない
閉じる操作は、Claudeの出力を読んだ人が自分のシェルで実行します。gh issue closeには--duplicate-ofがあり、重複元の番号またはURLを指定して閉じられます。
# 候補 #431 を開いて自分の目で確認してから
gh issue view 431 --web
# 重複と判断したときだけ閉じる
gh issue close 482 --duplicate-of 431 --comment "#431 と同じ問題のため閉じます"--reasonにはcompleted not planned duplicateが指定できます。重複の理由を明示しつつ元のissueへ誘導できるので、報告者にも経緯が伝わります。
誤爆を減らす4つの工夫
候補を出す段階の精度は、プロンプトの書き方と入力の整え方でかなり変わります。次の4点が効きます。
精度を上げるための手当て
confidenceを3段階で返させる
自由記述の確信度より、
highmediumlowの列挙のほうが後段で扱いやすくなります。lowは人が見る優先度を下げるだけに使います。理由を必須にする
reasonを必須項目にすると、根拠がない候補は出にくくなります。人が候補を開く前に、当たりかどうかの目安にもなります。該当なしを正解に含める
「該当がなければ空配列」と書いておきます。書かないと、何かしら選ばなければならないと解釈して無理に挙げることがあります。
本文は2段目で渡す
最初はタイトルだけで比べ、候補に残った数件だけ
bodyつきで再判定します。入力が小さく済み、判断も安定します。
4つ目の2段目の再判定は、次のように書けます。1段目でhighかmediumだった番号を取り出し、本文つきで取り直します。
# 1段目の結果から番号を取り出し、本文つきで取得し直す
for n in $(jq -r '.[] | select(.confidence != "low") | .number' result.json); do
gh issue view "$n" --json number,title,body
done | jq -s '.' > detail.jsondetail.jsonとnew.jsonを、先ほどと同じ要領でclaude --bare -pに渡します。このときは「同一の問題か、症状が似ているだけか」の2択で答えさせると、判断が割れにくくなります。
見逃しと取りこぼしが出る場面
この手順で候補に入らないのは、主に3つの場面です。
1つ目は、検索の語がずれている場合です。報告者によって、同じ不具合でも書き方が違います。タイトルに入っている語が違えば、--searchの段階で母集団から落ちます。語を変えた2回目の検索を足すか、in:titleを外して本文まで検索対象にします。
2つ目は、--limitの既定の30件で切れている場合です。既定のままだと、古い重複元が一覧に入りません。上の例のように100件ほどを明示しておき、それでも上限に当たるようなら、検索語をさらに絞ります。
3つ目は、閉じ済みissueを見ていない場合です。状態の既定はopenなので、--state allを付けないと、すでに直った不具合の再報告を拾えません。修正済みのissueが見つかれば、重複として閉じる代わりに、直ったバージョンを案内する返信もできます。
Claudeの比較が外れた場合は、lowの候補が並びます。その場合は無理に閉じず、通常のissueとして扱います。
定期実行に載せるなら
新しいissueが立ったタイミングで自動実行したいなら、GitHub Actionsに載せる手があります。その場合も、書き込みは行わず、候補をコメントとして残すか、ジョブのログに出すところまでに留めるのが無難です。Actionsでの定期実行はclaude-code-actionでcron実行する手順に、許可ツールの絞り方と合わせてまとめてあります。
CIや定期ジョブでは、--bareとANTHROPIC_API_KEYの組み合わせが扱いやすい構成です。実行ごとに入力が変わっても、手元の設定やフックに左右されないためです。
同じ「重複確認」でも、問い合わせ票とは材料が違う
この記事の対象は、GitHub上のissueです。母集団はghで機械的に取れ、突き合わせの結果も番号で確認できます。顧客からの問い合わせを再現手順つきのバグ票に直し、Jiraの既存票と照らす流れは、材料が違います。そちらは問い合わせからバグ票を作りJiraで重複を確認する手順で扱っています。
まとめ
重複issueの検出は、検索で母集団を絞る、Claudeに比較させる、人が閉じる、の3段に分けると事故が起きにくい構成になります。Claudeに渡す前にgh issue list --searchで数十件まで絞り、--json-schemaで出力を固定すれば、結果はそのままシェルで加工できます。
権限は、--allowedToolsを付けないことで書き込みを封じられます。issueの本文は他人の文章なので、命令として実行される余地を残さないことが、この構成の要です。まず自分のリポジトリで、閉じ済みの重複issueの組を1つ選び、片方を新規として渡してみてください。既知の答えと照らして、候補に入るかを確かめられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。