メインコンテンツへスキップ
kt-tech.blog

【実装】エージェントに GitHub のスクショを貼らせる — 回り道と、9/1 に来た gh --attach

実装17分で読めます

この記事でわかること

  • gh で画像が貼れなかった理由(user-attachments を発行する公開 API が無かったこと)と、cli/cli で6年続いた経緯
  • gh に機能が無いことを根拠に「できません」と2回言われて、ブラウザ経由で17分で解決した実話
  • 公式対応前にブラウザ自動操作で貼っていた実装(隠れた file input・アップロード待ち・React 管理下の textarea の消し方)
  • 9月1日に来た --attach の仕様と、Changelog に載っていない上限(1コマンド50ファイル・動画に alt は付かない)
  • 実際に叩いて確かめた挙動と、拡張子しか見ていないので中身がテキストの .png がそのまま公開される罠

gh 2.99.0 以上。GitHub の issue や PR を普段 CLI から触っていること。GitHub Enterprise Server は --attach の対象外です。

エージェントに Pull Request を書かせていると、必ず同じところで止まるんですよね。文章は書ける、差分の説明も書ける、なのに Before / After のスクリーンショットだけが貼れない。本文には <!-- ここにスクショを貼る --> というプレースホルダが残って、最後は自分がブラウザを開いてドラッグ&ドロップする。

自分はこれを、2回にわたって Claude Code に「gh CLI では画像をアップロードできないので、ここは手で貼ってください」と言われています。6日空けて、同じ壁で、同じ台詞を。

2026年9月1日、GitHub CLI 2.99.0 に --attach が入って、この回り道が丸ごと不要になりました。この記事は、なぜ長いあいだ貼れなかったのかと、貼れなかった頃にどう回避していたのかと、実際に新しい --attach を叩いて確かめた挙動の記録です。

メモ

この記事でやること

gh で画像が貼れなかった理由(公式 API が無かった)を確認して、ブラウザ自動操作で回避していた実装を残しつつ、9月1日に来た --attach を実際に叩いて挙動を確かめます。

貼れなかったのは、エージェントのせいではなかった

まず事実確認から。GitHub の Web UI で画像をドラッグ&ドロップすると、https://github.com/user-attachments/assets/ に続けて UUID が付いた URL が本文に挿入されます。この URL を発行する公開 API が、長いあいだ存在しませんでした

cli/cli のリクエストは 2020 年の #1895 から始まっていて、#3348、#4228、#4465 と続き、どれも「プラットフォーム側の API 制限で blocked」として閉じられています。CLI の実装が足りないのではなく、叩ける口が無かったわけです。

流れが変わったきっかけが、2026年3月18日に立った cli/cli#12960 だと思っていて。タイトルがそのまま時代を映していて、こうです。

support image/file attachments in gh issue create and gh issue comment
(critical for agentic workflows)

本文では、Claude Code・Codex・Gemini CLI といったエージェントが issue をプログラムから作るようになった以上、スクリーンショットやログを添付できないことが「gh のエージェント用途における最大の穴」だと主張しています。提案されていた回避策は4つで、当時これしか無かったという意味でもあります。

回避策 実際どうか
リリースアセットに上げて URL を埋める 動くが、隠しリリースという置き場所がゴミになる
リポジトリに直接コミットして raw URL で参照 スクショが git の履歴に永久に残る
内部 API の /upload/policies/assets を公開してほしい 願望であって、当時は叩けない
当面1、長期的に3 現実解

この issue も #1895 の duplicate として閉じられています。それが半年後に --attach として実装された、という順番でした。

「gh では無理です」と2回言われた話

ここからは自分の手元の記録です。2026年8月15日、PR にスクショを貼る段になって、Claude Code が選択肢を2つ出してきました。

  1. 画像をリポジトリにコミットして ?raw=true で参照する(履歴に残る旨の注記つき)
  2. 画像は渡すので、GitHub 側で自分でドラッグ&ドロップしてほしい

このとき提示された選択肢に、ブラウザで貼るという案は入っていませんでした。自分の返事はこうです。

playwrifhtMCPでgithub開いて貼るんだよ、

typo はそのままにしておきます。要するに、ブラウザを自動操作できる道具をすでに持っているのだから、それで人間がやっている操作をやればいい、という指摘でした。

面白い(というか、笑ってしまった)のはここからで、6日後の8月21日、まったく同じ壁で同じことを言われています。

残っている作業が1つあります。gh CLI では画像をアップロードできないため、
PR本文の「Before / After」節はプレースホルダのままです。
お渡しした比較画像3枚をそこにドラッグ&ドロップしてください。

自分の再指摘が2発:

スクショとかがかないのかね、before,afterの、playwrightMCPならできるはずでは。
githubをMCPでひらけば晴れるし、もしくは chrome のセッションタブでいけると思うよ

「晴れる」は「貼れる」の変換ミスです。そしてこの指摘から17分後に「両方できました。ご指摘のとおり、ブラウザ経由で貼れました」と返ってきている。つまり17分でできることを、2回にわたって「できません」と言われていたわけですね。

ここは道具の問題ではなくて、エージェントが「自分が使える道具の組み合わせ」を検討しないまま不可能だと結論する、という性質の問題だと思っていて。gh にその機能が無いのは事実なので、嘘は言っていない。ただ、目的は「PR にスクショを貼ること」であって「gh で貼ること」ではない。手元にはブラウザを操作できる MCP があって、GitHub にはログイン済みのセッションがある。その組み合わせに手が伸びなかった。

このあと同じことを3度言わずに済むよう、手順をメモリに残しました。今回 --attach が来たことで、そのメモリごと不要になったのが少し愉快です。

余談: 貼る前に、そもそも読めなかった

実は「貼る」で詰まる2週間前に、「読む」でも同じ壁にぶつかっていました。2026年8月1日、レビューでもらったスクショの中身を確認しようとして、こう叩いています。

Bash
curl -sL -o shot.png "https://github.com/user-attachments/assets/660d27e4-..."
file shot.png
# → shot.png: ASCII text, with no line terminators
head -c 300 shot.png
# → Not Found

-L でリダイレクトを追っているのに、拡張子 png のファイルの中身が9バイトの Not Found でした。

理由は、この URL が画像の実体ではなくリダイレクタだからです。Cookie のセッションを見て、S3 の presigned URL(github-production-user-asset-*.s3.amazonaws.com/...?X-Amz-Expires=300)に飛ばす。プライベートリポジトリの添付なので、未認証の curl には 404 相当が返る。しかもgh auth token の OAuth トークンでも通りません。Web セッションの Cookie が要るので、gh api 経由でも取れない。

結局このときは、ログイン済みの Chrome で URL を開いて、表示された画像をスクリーンショットで読みました。ファイルとして落とすのを諦めて、ブラウザに表示させて読む。

…お気づきかもしれませんが、これが2週間後の「貼る側」の答えそのものなんですよね。ログイン済みのブラウザセッションを使えば GitHub の添付は扱えるという事実を、8月1日の時点で一度手に入れていた。それでも8月15日に「できません」と言っていたわけです。

回り道: ブラウザに貼らせて、URL だけ抜く

参考までに、公式対応が来る前に実際に動いていた手順を残しておきます。GitHub Enterprise Server はまだ --attach に対応していないので、そちらではまだ現役の手です。

考え方はシンプルで、画像のアップロードだけブラウザにやらせて、本文の編集は gh でやるという分担です。

役割 使うもの
画像を GitHub に上げて URL を得る ブラウザ自動操作(Playwright MCP / Claude in Chrome)
得た URL を本文に埋め込む gh pr edit --body-file

流れはこうなります。

1. gh pr create --body-file body.md   # 画像位置は <!-- SCREENSHOTS --> のまま
2. ブラウザで PR ページを開く
3. コメント欄の隠れた file input をクリックしてファイルチューザを出す
4. ローカルの png を渡す
5. textarea をポーリングして user-attachments の URL が入るのを待つ
6. プレースホルダを URL で置換して gh pr edit
7. コメント欄の下書きを消す(投稿はしない)

肝は3と7です。まず、GitHub のコメント欄の input[type=file] は画面上に見えていないので、クリックを送ってファイルチューザを開かせます。

JavaScript
() => {
  const input = document.querySelector('input[type=file]');
  if (!input) return { ok: false };
  input.click();
  return { ok: true };
}

アップロードが終わると、textarea の中身が GitHub 自身の手で img タグに書き換わります。上げた直後は ![Uploading shot.png…]() というプレースホルダが入っているだけなので、これを待つ必要があります。枚数を数えながらポーリングします。

JavaScript
() => new Promise(resolve => {
  const check = (tries) => {
    const ta = document.getElementById('new_comment_field');
    const value = ta ? ta.value : '';
    const done = value.includes('user-attachments')
              && (value.match(/user-attachments/g) || []).length >= 4;
    if (done || tries <= 0) { resolve({ done, value }); return; }
    setTimeout(() => check(tries - 1), 3000);
  };
  check(20);
})

実際に返ってくる中身がこれです。幅と高さと alt まで GitHub が埋めてくれます。

HTML
<img width="1645" height="839" alt="before-search" src="https://github.com/user-attachments/assets/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />

そして7。コメントを投稿せずに帰るので、下書きが残ったままだと次に人が開いたときに驚かれます。ところがこの textarea は React の管理下にあるので、ta.value = '' では消えません。ネイティブの setter を取り出して呼んで、input イベントを自分で飛ばす必要があります。

JavaScript
() => {
  const ta = document.getElementById('new_comment_field');
  const setter = Object.getOwnPropertyDescriptor(
    window.HTMLTextAreaElement.prototype, 'value'
  ).set;
  setter.call(ta, '');
  ta.dispatchEvent(new Event('input', { bubbles: true }));
  return { cleared: true, value: ta.value };
}

ちなみに最初のバージョンは、PR 本文の編集フォームに直接アップロードしていました。これだとプレースホルダ入りの本文が一度公開されてしまうので、「コメント欄に上げて、投稿せず URL だけ抜く」方式に変えています。

ひとつ補足しておくと、プライベートリポジトリに貼った画像は、表示のときに private-user-images.githubusercontent.com のほうに変換されます。URL の見た目が変わるので一瞬ぎょっとしますが、ちゃんと表示されます。

読める通り、動くけれど重い。ブラウザが要って、ログイン済みセッションが要って、GitHub の DOM の id(new_comment_field)に依存しています。

9月1日、公式が出た

GitHub CLI: Media in issues, pull requests, and comments が公開されて、--attach が全プランで GA になりました。必要なのは gh 2.99.0 以上です。

Bash
brew upgrade gh
gh --version   # gh version 2.99.0 (2026-09-01)

使えるコマンドは6つ。

gh issue create / gh issue edit / gh issue comment
gh pr create    / gh pr edit    / gh pr comment

仕様で押さえておくところを表にします。

項目 内容
対応形式 png, jpg, jpeg, gif, webp, svg, mp4, mov, webm
サイズ上限 画像・GIF は 10MB、動画は Free で 10MB、有料プランで 100MB
1コマンドの上限 50ファイル
alt text パスの後ろに # で続ける(--attach './login.png#The login error state'
動画の alt text プレイヤーとして描画されるので付けられない
認証 OAuth トークンか classic PAT。リポジトリへの write 権限が要る
GitHub Enterprise Server 非対応

「1コマンド50ファイル」と「動画に alt は付けられない」は Changelog には書かれていなくて、gh issue comment --help にだけ載っていました。

実際に叩いて確かめた

自分のブログのリポジトリに検証用の issue を1本立てて、挙動を確認しました。一番知りたかったのは、本文に書いたローカルパスがどう扱われるかです。

本文にこう書いて、

Markdown
![ブラウザで貼っていた頃](./gh-before.png)

同じパスを添付しつつ、本文が参照していない画像も1枚渡してみます。

Bash
gh issue create \
  --title "[検証] gh v2.99.0 の --attach の挙動確認" \
  --body-file body.md \
  --attach ./gh-before.png \
  --attach './gh-after.png#After: gh --attach 一行で済む'

できあがった issue 本文を gh issue view --json body で読むと、こうなっていました。

Markdown
![ブラウザで貼っていた頃](https://github.com/user-attachments/assets/16aa4a07-a150-40f4-bd58-a552167c52eb)

(中略)

![After: gh --attach 一行で済む](https://github.com/user-attachments/assets/04e66804-632f-4656-bc57-fc5b20e83f14)

読み取れることが3つあります。

  1. 本文が参照しているローカルパスは、その位置で URL に置き換わる。末尾に飛ばされない
  2. その場合の alt text は本文に書いたものが優先される。ファイル名にはならない
  3. 本文が参照していない添付は、末尾に追記される。alt は # で渡したものが入る

つまり、本文の Markdown をそのまま組み立てておいて、パスを --attach にも渡せばいい。本文とアップロードを別々に考えなくてよくなったのが、回り道との一番の違いです。

エラー側も確かめました。

$ gh issue comment 542 --body x --attach ./no-such-file.png
./no-such-file.png: no such file or directory

$ gh issue comment 542 --body x --attach ./dummy.pdf
./dummy.pdf is not a supported file type
(supported: png, jpg, jpeg, gif, webp, svg, mp4, mov, webm)

PDF が通らないのは、ログや資料をそのまま添付したい場面ではひっかかると思います。そこは従来どおり gist なりリリースアセットなりに逃がすことになりそうです。

中身は見ていない

ひとつ注意しておきたい挙動があって、拡張子しか見ていません。中身がテキストのファイルに .png と名前を付けて渡すと、そのまま通ります。

Bash
printf 'not an image' > fake.png
gh issue comment 542 --body x --attach ./fake.png
# → コメントが作成され、壊れた画像として表示される

エラーにならず、壊れた画像として公開されます。人間が手でドラッグしているぶんには起きない事故ですが、エージェントに任せるなら普通に踏むと思っていて。スクショの生成に失敗して 0 バイトのファイルができていても、--attach は何も言わずに上げてしまう。貼る前に file なりでサイズと種類を見ておくのが無難です。

SVG も試しましたが、こちらは問題なく同じ user-attachments/assets/ 形式で通りました。

何が変わったか

〜2026年8月 ブラウザ経由の回り道 ローカルの画像 渡す ブラウザで開いた PR のコメント欄 待つ textarea に入る user-attachments の URL 読む gh pr edit で本文を差し替え ログイン済みセッションと、GitHub の DOM(new_comment_field)に依存する コメントは投稿しないので、残った下書きを自分で消す 2026年9月〜 gh --attach 本文の Markdown + ローカルの画像 gh issue create --attach ./shot.png 本文のパスが その場で URL に変わる 1コマンドで完結。50ファイルまで。GitHub Enterprise Server では使えない
やっていること自体は変わらない。画像を GitHub のストレージに上げて、返ってきた URL を本文に埋める。違うのは、上段ではその往復をこちらで組み立てる必要があり、ブラウザと GitHub の DOM に依存していたところ。--attach は同じ往復を CLI の中で閉じてしまうので、本文の Markdown をそのまま書いて、同じパスをフラグに渡すだけで済む。

道具としての差はこれだけです。

回り道(〜8月) --attach(9月〜)
必要なもの ブラウザ + ログイン済みセッション + DOM の知識 gh 2.99.0
手数 7ステップ 1コマンド
壊れる要因 GitHub の DOM 変更、React の再実装 (公式なので追従される)
GHES 使える 使えない

貼れることと、読めるように並べることは別

ついでに書いておくと、貼れるようになったあとも、今度は並べ方で同じことを2回言っています。

beforeとafterはmarkdownのテーブルで分けなさいw
修正前と修正後とかを並べる時とかはちゃんとmarkdownのテーブルレイアウトなどを使ってくださいね

縦に2枚並べられても、スクロールしないと見比べられないので、差分が読めないんですよね。

Markdown
| 修正前 | 修正後 |
| --- | --- |
| <img width="480" src="..."> | <img width="480" src="..."> |

--attach は本文のどこにパスを書いてもその位置で置換してくれるので、テーブルのセルの中に書いておけばそのまま並びます。貼れることと、読めるように並べることは別の問題で、後者は今のところ人間が指摘するしかないと思っています。

ただ、自分にとってこの記事の主題は --attach の使い方ではなくて、「できません」と言われたときにそれをどう受け取るかのほうでした。8月15日の時点で「gh にその機能は無い」というのは 100% 正しくて、実際 GitHub は API を公開していなかった。それでも目的は達成できた。道具Aに機能が無いことと、目的が達成できないことは、別の話なんですよね。

そして9月1日にその回り道ごと消えた。エージェント側の工夫でしのいでいたものが、プラットフォーム側の実装で要らなくなる。今の時期はこれが短い周期で起きるので、回避策を書いたら日付を添えて残しておくのが結局いちばん効くなと思っています。この記事もそのつもりで書きました。