{"id":"078d88d9-d663-4635-a34f-001f90e9e0df","shortId":"7f5kwz","kind":"skill","title":"github-upload-image-to-pr","tagline":">-","description":"# Upload Image to PR\n\nUpload local images to a GitHub PR and embed them in the description or comments using browser automation tools.\n\n## How It Works\n\nSince the GitHub API does not support direct image uploads, this skill uses the **PR comment textarea as a staging area for GitHub's image hosting** — uploading files there to obtain persistent `user-attachments/assets/` URLs, then updating the PR description or posting a comment via the `gh` CLI.\n\n## Step 0: Resolve PR context\n\nIf the user didn't specify a PR number or URL, auto-detect it:\n\n```bash\n# Get PR number from the current branch\ngh pr view --json number,url -q '\"\\(.number) \\(.url)\"'\n```\n\nIf multiple repos or branches are involved, confirm with the user which PR to target.\n\nAlso, normalize the image paths to absolute paths. If a path contains special characters (e.g., Unicode narrow spaces from CleanShot X), copy the file to `/tmp/` first:\n\n```bash\n# e.g., to handle glob-matched paths with special chars\ncp /path/to/CleanShot*keyword*.png /tmp/screenshot.png\n```\n\n## Tool Detection and Selection\n\n### Priority Order\n\n1. **Playwright MCP** (MCP connection, `mcp__playwright__*`) — connects to existing browser, login state preserved\n2. **Chrome DevTools MCP** (MCP connection, `mcp__chrome-devtools__*`) — connects to existing browser, login state preserved\n3. **agent-browser** (CLI via Bash — fallback, login state preserved with `--profile`)\n\nMCP-based tools connect to an already-running browser instance, so **GitHub login state is automatically preserved**. agent-browser can persist login state using `--profile ~/.agent-browser-github`.\n\n### Detection\n\n```\n# 1. Search for MCP-based browser tools (preferred)\nToolSearch: \"browser navigate upload\"\n\n# 2. Fall back to agent-browser only if no MCP tools found\nBash: agent-browser --version\n```\n\n## Tool Compatibility Matrix\n\n| Operation | Playwright MCP | Chrome DevTools MCP | agent-browser (CLI/Bash) |\n|-----------|----------------|---------------------|--------------------------|\n| **Navigate** | `browser_navigate` | `navigate_page` | `agent-browser --headed open {url}` |\n| **Snapshot** | `browser_snapshot` | `take_snapshot` | `agent-browser snapshot` |\n| **Screenshot** | `browser_take_screenshot` | `take_screenshot` | `agent-browser screenshot {path}` |\n| **Click** | `browser_click` (ref) | `click` (uid) | `agent-browser click {ref}` |\n| **File Upload** | `browser_file_upload` (paths) | `upload_file` (uid, filePath) | `agent-browser upload {ref} {path}` |\n| **JS Eval** | `browser_evaluate` (function) | `evaluate_script` (function) | `agent-browser eval '{js}'` |\n| **Login State** | Preserved | Preserved | Preserved with `--profile` |\n\n## Steps\n\n### Step 1: Navigate to PR page and check login state\n\nNavigate to the PR page and immediately take a snapshot to verify login state.\n\n```javascript\n// Playwright MCP\nbrowser_navigate({ url: \"https://github.com/{owner}/{repo}/pull/{number}\" })\n\n// Chrome DevTools MCP\nnavigate_page({ url: \"https://github.com/{owner}/{repo}/pull/{number}\", type: \"url\" })\n\n// agent-browser (use --profile to persist login state)\nagent-browser --headed --profile ~/.agent-browser-github open \"https://github.com/{owner}/{repo}/pull/{number}\"\n```\n\n**If SSO authentication screen appears:** Take a snapshot, locate the \"Continue\" button, and click it.\n\n**If NOT logged in (agent-browser only):**\n1. Navigate to `https://github.com/login`\n2. Ask the user to log in manually in the headed browser window.\n3. Wait for user confirmation, then navigate back to the PR page.\n\n### Step 2: Locate the file upload input\n\nTake a snapshot/screenshot and scroll to the bottom to find the comment area.\n\nGitHub renders a file upload input in the comment form. Try these selectors in order (GitHub's UI can change — if one fails, try the next):\n\n```javascript\n// Shared JS for MCP-based tools — tries multiple known selectors\n() => {\n  const selectors = [\n    'input[type=\"file\"][id*=\"comment\"]',\n    'input[type=\"file\"][id=\"fc-new_comment_field\"]',\n    '#new_comment_field',\n    'input[type=\"file\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return { found: true, id: el.id, selector: sel };\n  }\n  return { found: false };\n}\n```\n\nFor Chrome DevTools MCP, you can also take a snapshot to find the `uid` of the file upload element directly.\n\n### Step 3: Upload images one by one\n\nUpload each image file using the detected tool. Wait **2–3 seconds between uploads** to allow GitHub to process each file.\n\nFor multiple images, upload them all to the same comment textarea before extracting URLs — this is more efficient than navigating between uploads.\n\n```javascript\n// Chrome DevTools MCP: upload_file requires the uid of the input element\n// Playwright MCP: browser_file_upload takes the element ref and file path(s) array\n// agent-browser: agent-browser upload {ref} {absolute_path}\n```\n\n**Important:** Always use absolute file paths.\n\n### Step 4: Retrieve uploaded image URLs\n\nWait **3–5 seconds** after the last upload, then read the textarea value. GitHub injects markdown image syntax like `![description](https://github.com/user-attachments/assets/...)` into the textarea:\n\n```javascript\n// Shared JS — tries both known textarea IDs\n() => {\n  const ta = document.getElementById('new_comment_field')\n          || document.querySelector('textarea[id*=\"comment\"]');\n  return ta ? ta.value : 'textarea not found';\n}\n```\n\n```bash\n# agent-browser\nagent-browser eval 'document.getElementById(\"new_comment_field\")?.value || document.querySelector(\"textarea[id*=comment]\")?.value || \"not found\"'\n```\n\nThe response contains URLs in the format:\n```\n![image](https://github.com/user-attachments/assets/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)\n```\n\nExtract all image URLs/markdown from the textarea value before clearing it.\n\n### Step 5: Clear the textarea (do not submit the comment)\n\n```javascript\n// MCP-based tools\n() => {\n  const ta = document.getElementById('new_comment_field')\n           || document.querySelector('textarea[id*=\"comment\"]');\n  if (ta) { ta.value = \"\"; return \"cleared\"; }\n  return \"textarea not found\";\n}\n```\n\n```bash\n# agent-browser\nagent-browser eval 'const ta = document.getElementById(\"new_comment_field\") || document.querySelector(\"textarea[id*=comment]\"); if(ta){ta.value=\"\"} \"cleared\"'\n```\n\n### Step 6: Embed images in the PR\n\n**Option A — Update PR description** (append images to existing body):\n```bash\nEXISTING_BODY=$(gh pr view {PR_NUMBER} --json body -q .body)\n\ngh pr edit {PR_NUMBER} --body \"$(printf '%s\\n\\n## Screenshots\\n\\n%s' \"$EXISTING_BODY\" \"![screenshot](https://github.com/user-attachments/assets/...)\")\"\n```\n\n**Option B — Post as a new comment**:\n```bash\ngh pr comment {PR_NUMBER} --body \"## Screenshots\n\n![screenshot](https://github.com/user-attachments/assets/...)\"\n```\n\nUse Option A by default unless the user explicitly asks for a comment, or if the PR description is already long and a comment would be cleaner.\n\n### Step 7: Verify the result\n\nReload the page and take a screenshot to confirm the images are displayed correctly.\n\n## Tips\n\n- **Image sizing**: Control display size via HTML `<img>` tags: `<img width=\"800\" alt=\"description\" src=\"...\" />`\n- **Multiple images**: Upload all images in one session to the same textarea; extract all URLs before clearing\n- **Prefer MCP tools**: Always prefer Playwright or Chrome DevTools MCP over agent-browser for simpler setup\n- **agent-browser login persistence**: Use `--profile ~/.agent-browser-github` to persist GitHub login across sessions\n\n## Troubleshooting\n\n| Issue | Solution |\n|-------|----------|\n| Not logged in (MCP tools) | SSO screen may appear — take snapshot, find \"Continue\" button, click it |\n| Not logged in (agent-browser) | Use `--headed` mode, navigate to login page, ask user to log in manually |\n| Browser window not visible | For agent-browser, ensure `--headed` flag is used |\n| File path with special characters (e.g., Unicode narrow spaces from CleanShot) | Copy file to `/tmp/` with a simple name: `cp /path/CleanShot*keyword*.png /tmp/screenshot.png` |\n| File upload fails | Ensure the file path is absolute |\n| Textarea doesn't contain URLs yet | Wait 3–5 seconds after upload before running JS eval; retry once if needed |\n| Textarea selector not found | GitHub UI changes occasionally — use the multi-selector JS in Step 2 to find the current element |\n| Chrome DevTools MCP disconnected | Reconnect via `/mcp` command |\n| agent-browser not found | `npm install -g agent-browser && agent-browser install` |\n| No browser tools found | Use `ToolSearch` to search for available browser tools |\n| PR not found / 404 | Private repos return 404 for unauthenticated users — check login state |\n\n## Notes\n\n- GitHub `user-attachments/assets/` URLs are **persistent** — images remain accessible even without submitting the comment\n- Editing the description directly in the browser UI is fragile due to GitHub UI structure changes — updating via `gh pr edit` is strongly preferred\n- Multiple images can be uploaded in a single session before extracting URLs\n- MCP-based tools connect to existing browser instances, preserving cookies and login sessions","tags":["github","upload","image","tonkotsuboy","agent-skills"],"capabilities":["skill","source-tonkotsuboy","skill-github-upload-image-to-pr","topic-agent-skills"],"categories":["github-upload-image-to-pr"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/tonkotsuboy/github-upload-image-to-pr/github-upload-image-to-pr","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add tonkotsuboy/github-upload-image-to-pr","source_repo":"https://github.com/tonkotsuboy/github-upload-image-to-pr","install_from":"skills.sh"}},"qualityScore":"0.464","qualityRationale":"deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 28 github stars · SKILL.md body (9,174 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T19:04:42.226Z","embedding":null,"createdAt":"2026-04-22T13:02:42.737Z","updatedAt":"2026-05-18T19:04:42.226Z","lastSeenAt":"2026-05-18T19:04:42.226Z","tsv":"'/.agent-browser-github':256,443,1036 '/assets':68,1223 '/login':478 '/mcp':1175 '/path/cleanshot':1114 '/path/to/cleanshot':174 '/pull':414,425,448 '/tmp':160,1108 '/tmp/screenshot.png':177,1117 '/user-attachments/assets/...)':746,920,939 '/user-attachments/assets/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)':804 '0':84 '1':184,258,382,473 '2':198,271,479,505,641,1163 '3':215,492,626,642,725,1134 '4':719 '404':1207,1211 '5':726,817,1135 '6':873 '7':968 'absolut':141,710,715,1126 'access':1229 'across':1041 'agent':217,248,276,286,299,308,319,329,340,355,369,430,439,470,703,706,776,779,852,855,1024,1030,1066,1087,1178,1186,1189 'agent-brows':216,247,275,285,298,307,318,328,339,354,368,429,438,469,702,705,775,778,851,854,1023,1029,1065,1086,1177,1185,1188 'allow':647 'alreadi':236,959 'already-run':235 'also':135,611 'alway':713,1015 'api':36 'appear':454,1054 'append':884 'area':53,523 'array':701 'ask':480,949,1075 'attach':67,1222 'authent':452 'auto':100 'auto-detect':99 'autom':28 'automat':245 'avail':1201 'b':922 'back':273,499 'base':230,263,556,829,1273 'bash':103,162,221,284,774,850,889,928 'bodi':888,891,898,900,906,916,934 'bottom':518 'branch':110,124 'browser':27,194,211,218,238,249,264,268,277,287,300,303,309,314,320,323,330,334,341,346,356,362,370,408,431,440,471,490,690,704,707,777,780,853,856,1025,1031,1067,1081,1088,1179,1187,1190,1193,1202,1241,1278 'button':461,1059 'chang':543,1153,1250 'char':172 'charact':148,1098 'check':388,1215 'chrome':199,206,295,416,606,676,1019,1169 'chrome-devtool':205 'cleaner':966 'cleanshot':154,1104 'clear':814,818,845,871,1011 'cli':82,219 'cli/bash':301 'click':333,335,337,342,463,1060 'command':1176 'comment':25,48,78,522,532,568,576,579,662,762,767,784,790,825,835,840,862,867,927,931,952,963,1234 'compat':290 'confirm':127,496,980 'connect':188,191,203,208,232,1275 'const':562,585,589,758,831,858 'contain':146,796,1130 'context':87 'continu':460,1058 'control':989 'cooki':1281 'copi':156,1105 'correct':985 'cp':173,1113 'current':109,1167 'default':944 'descript':23,74,743,883,957,1237 'detect':101,179,257,638 'devtool':200,207,296,417,607,677,1020,1170 'didn':91 'direct':40,624,1238 'disconnect':1172 'display':984,990 'document.getelementbyid':760,782,833,860 'document.queryselector':591,764,787,837,864 'doesn':1128 'due':1245 'e.g':149,163,1099 'edit':903,1235,1255 'effici':670 'el':590,594 'el.id':599 'element':623,687,695,1168 'emb':19,874 'ensur':1089,1121 'eval':361,371,781,857,1142 'evalu':363,365 'even':1230 'exist':193,210,887,890,915,1277 'explicit':948 'extract':665,805,1007,1269 'fail':546,1120 'fall':272 'fallback':222 'fals':604 'fc':574 'fc-new':573 'field':577,580,763,785,836,863 'file':60,158,344,347,351,508,527,566,571,583,621,635,652,680,691,698,716,1094,1106,1118,1123 'filepath':353 'find':520,616,1057,1165 'first':161 'flag':1091 'form':533 'format':800 'found':283,596,603,773,793,849,1150,1181,1195,1206 'fragil':1244 'function':364,367 'g':1184 'get':104 'gh':81,111,892,901,929,1253 'github':2,16,35,55,241,524,539,648,737,1039,1151,1219,1247 'github-upload-image-to-pr':1 'github.com':411,422,445,477,745,803,919,938 'github.com/login':476 'github.com/user-attachments/assets/...)':744,918,937 'github.com/user-attachments/assets/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)':802 'glob':167 'glob-match':166 'handl':165 'head':310,441,489,1069,1090 'host':58 'html':993 'id':567,572,598,757,766,789,839,866 'imag':4,8,13,41,57,138,628,634,655,722,740,801,807,875,885,982,987,996,999,1227,1260 'immedi':397 'import':712 'inject':738 'input':510,529,564,569,581,686 'instal':1183,1191 'instanc':239,1279 'involv':126 'issu':1044 'javascript':405,550,675,750,826 'js':360,372,552,752,1141,1160 'json':114,897 'keyword':175,1115 'known':560,755 'last':730 'like':742 'local':12 'locat':458,506 'log':467,484,1047,1063,1078 'login':195,212,223,242,252,373,389,403,436,1032,1040,1073,1216,1283 'long':960 'manual':486,1080 'markdown':739 'match':168 'matrix':291 'may':1053 'mcp':186,187,189,201,202,204,229,262,281,294,297,407,418,555,608,678,689,828,1013,1021,1049,1171,1272 'mcp-base':228,261,554,827,1271 'mode':1070 'multi':1158 'multi-selector':1157 'multipl':121,559,654,995,1259 'n':909,910,912,913 'name':1112 'narrow':151,1101 'navig':269,302,304,305,383,391,409,419,474,498,672,1071 'need':1146 'new':575,578,761,783,834,861,926 'next':549 'normal':136 'note':1218 'npm':1182 'number':96,106,115,118,415,426,449,896,905,933 'obtain':63 'occasion':1154 'one':545,629,631,1001 'open':311,444 'oper':292 'option':879,921,941 'order':183,538 'owner':412,423,446 'page':306,386,395,420,503,974,1074 'path':139,142,145,169,332,349,359,699,711,717,1095,1124 'persist':64,251,435,1033,1038,1226 'playwright':185,190,293,406,688,1017 'png':176,1116 'post':76,923 'pr':6,10,17,47,73,86,95,105,112,132,385,394,502,878,882,893,895,902,904,930,932,956,1204,1254 'prefer':266,1012,1016,1258 'preserv':197,214,225,246,375,376,377,1280 'printf':907 'prioriti':182 'privat':1208 'process':650 'profil':227,255,379,433,442,1035 'q':117,899 'read':733 'reconnect':1173 'ref':336,343,358,696,709 'reload':972 'remain':1228 'render':525 'repo':122,413,424,447,1209 'requir':681 'resolv':85 'respons':795 'result':971 'retri':1143 'retriev':720 'return':595,602,768,844,846,1210 'run':237,1140 'screen':453,1052 'screenshot':322,325,327,331,911,917,935,936,978 'script':366 'scroll':515 'search':259,1199 'second':643,727,1136 'sel':586,592,601 'select':181 'selector':536,561,563,588,600,1148,1159 'session':1002,1042,1267,1284 'setup':1028 'share':551,751 'simpl':1111 'simpler':1027 'sinc':33 'singl':1266 'size':988,991 'skill':44 'skill-github-upload-image-to-pr' 'snapshot':313,315,317,321,400,457,614,1056 'snapshot/screenshot':513 'solut':1045 'source-tonkotsuboy' 'space':152,1102 'special':147,171,1097 'specifi':93 'sso':451,1051 'stage':52 'state':196,213,224,243,253,374,390,404,437,1217 'step':83,380,381,504,625,718,816,872,967,1162 'strong':1257 'structur':1249 'submit':823,1232 'support':39 'syntax':741 'ta':759,769,832,842,859,869 'ta.value':770,843,870 'tag':994 'take':316,324,326,398,455,511,612,693,976,1055 'target':134 'textarea':49,663,735,749,756,765,771,788,811,820,838,847,865,1006,1127,1147 'tip':986 'tool':29,178,231,265,282,289,557,639,830,1014,1050,1194,1203,1274 'toolsearch':267,1197 'topic-agent-skills' 'tri':534,547,558,753 'troubleshoot':1043 'true':597 'type':427,565,570,582 'ui':541,1152,1242,1248 'uid':338,352,618,683 'unauthent':1213 'unicod':150,1100 'unless':945 'updat':71,881,1251 'upload':3,7,11,42,59,270,345,348,350,357,509,528,622,627,632,645,656,674,679,692,708,721,731,997,1119,1138,1263 'url':69,98,116,119,312,410,421,428,666,723,797,1009,1131,1224,1270 'urls/markdown':808 'use':26,45,254,432,636,714,940,1034,1068,1093,1155,1196 'user':66,90,130,482,495,947,1076,1214,1221 'user-attach':65,1220 'valu':736,786,791,812 'verifi':402,969 'version':288 'via':79,220,992,1174,1252 'view':113,894 'visibl':1084 'wait':493,640,724,1133 'window':491,1082 'without':1231 'work':32 'would':964 'x':155 'yet':1132","prices":[{"id":"7246f3a6-f17e-4074-a33b-9c592af75b5f","listingId":"078d88d9-d663-4635-a34f-001f90e9e0df","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"tonkotsuboy","category":"github-upload-image-to-pr","install_from":"skills.sh"},"createdAt":"2026-04-22T13:02:42.737Z"}],"sources":[{"listingId":"078d88d9-d663-4635-a34f-001f90e9e0df","source":"github","sourceId":"tonkotsuboy/github-upload-image-to-pr/github-upload-image-to-pr","sourceUrl":"https://github.com/tonkotsuboy/github-upload-image-to-pr/tree/main/skills/github-upload-image-to-pr","isPrimary":false,"firstSeenAt":"2026-04-22T13:02:42.737Z","lastSeenAt":"2026-05-18T19:04:42.226Z"},{"listingId":"078d88d9-d663-4635-a34f-001f90e9e0df","source":"skills_sh","sourceId":"tonkotsuboy/github-upload-image-to-pr/github-upload-image-to-pr","sourceUrl":"https://skills.sh/tonkotsuboy/github-upload-image-to-pr/github-upload-image-to-pr","isPrimary":true,"firstSeenAt":"2026-05-07T20:45:11.534Z","lastSeenAt":"2026-05-07T22:43:16.001Z"}],"details":{"listingId":"078d88d9-d663-4635-a34f-001f90e9e0df","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"tonkotsuboy","slug":"github-upload-image-to-pr","github":{"repo":"tonkotsuboy/github-upload-image-to-pr","stars":28,"topics":["agent-skills"],"license":"mit","html_url":"https://github.com/tonkotsuboy/github-upload-image-to-pr","pushed_at":"2026-04-22T11:58:58Z","description":"AI agent skill(e.g., Claude Code, Codex): Upload local images to a GitHub PR and embed them in the description or comments","skill_md_sha":"e90d3507cee48ceba5efe82614fd3e313257d087","skill_md_path":"skills/github-upload-image-to-pr/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/tonkotsuboy/github-upload-image-to-pr/tree/main/skills/github-upload-image-to-pr"},"layout":"multi","source":"github","category":"github-upload-image-to-pr","frontmatter":{"name":"github-upload-image-to-pr","license":"MIT","description":">-"},"skills_sh_url":"https://skills.sh/tonkotsuboy/github-upload-image-to-pr/github-upload-image-to-pr"},"updatedAt":"2026-05-18T19:04:42.226Z"}}