Skip to content
cccskills
FreePublished on GitHub

github-pr-images

Embed a local image file into an existing GitHub PR — either in the PR body or as a comment. Trigger when a request pairs a local image (screenshot, .png/.jpg, CleanShot capture, before/after) with an existing PR (by #number, URL, branch name, or "the open PR"), regardless of verb — attach, embed, add, put, post, drop, show, document. Also covers visually documenting test runs, bug repros, UI states, or CI failures on an existing PR. Uploads through `gh`'s `--attach` flag — except in the Claude Code web sandbox, which has no `gh` and hosts images through the PR branch's history instead. It also owns the PR's one `## Screenshots` comment — finding, creating, editing and verifying it — so other workflows (the `pr` skill's screenshot phase) hand it a composed body that references local image paths, and it posts it.

The English summary is being prepared. Showing the original description.

bikeindex/bike_index308 stars(GitHub 308 + cccskills 0)
Works withGitHub

Run score Not tested

See how to install

Files included (4)

  • SKILL.md8.1 KB
  • assets/commit_images.sh3.8 KB
  • assets/inline_attachments.rb1.2 KB
  • references/web-sandbox.md3.7 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Upload Image to PR

gh pr comment, gh pr edit and gh pr create take --attach '<file>#<alt text>', up to 50 per command. A markdown ![alt](tmp/x.png) reference to an attached path is rewritten in place to the uploaded user-attachments/assets/ URL. An <img src="tmp/x.png"> isn't — it keeps its path, and the upload is appended to the end as ![<basename>](url), as is any attached file the body doesn't reference.

There's no upload-only call: assets upload as part of posting, so callers hand over a body that references local paths, never URLs they need back first.

Who runs this

From a session with history, hand this skill to a subagent the way AGENTS.md's Who runs this describes — model: "sonnet" (it works out which comment and which upload route) — briefed with the PR number and either a composed body file referencing local image paths or the images plus where they go. It replies with the comment (or PR) URL and Step 4's verification result.

Preflight: which route this run takes

$CLAUDE_CODE_REMOTE decides which, before anything else here.

true — the web sandbox, which has no gh and follows references/web-sandbox.md for the whole flow.

Unset — make sure gh has --attach:

gh help pr comment | grep -q -- --attach || brew upgrade gh

gh help pr comment, not gh pr comment --help — the rtk hook swallows the latter's output. Still missing after the upgrade, or no gh at all → stop and say so, posting nothing.

Step 1: Resolve PR context

If the user didn't specify a PR number or URL, auto-detect it:

gh pr view --json number,url -q '"\(.number) \(.url)"'

Reference each image by its path from the repo root, the same in the body as in --attach. A path with special characters (Unicode narrow spaces from CleanShot X) goes through the project's tmp/ first:

cp /path/to/CleanShot*keyword*.png tmp/screenshot.png

Step 2: The body

A caller's body is posted verbatim — don't recompose it. Composing it yourself:

## Screenshots

<img src="tmp/screenshot.png" width="500">

When a ## Screenshots comment already exists (step 3 finds it), compose from its current body: a recapture replaces the image it supersedes in place — never a new "Updated"/"After merging main" section appended below the old shots. A state the comment doesn't show yet gets its own block, in the shape of the ones there.

Images in table cells want <img … width=…>; elsewhere ![alt](path) is fine. Don't give an <img>'s file a #alt on --attach — step 3's script finds its upload by the basename gh uses as the default alt.

Step 3: Post it

This skill owns the ## Screenshots comment: one per PR, body starting ## Screenshots even when what's under it isn't screenshots — retitle it and the next run can't find it.

gh api --paginate "repos/{owner}/{repo}/issues/$PR_NUMBER/comments" \
  --jq ".[] | {id, login: .user.login, body: .body[:14]}"

The comment whose body starts ## Screenshots is $SCREENSHOT_COMMENT_ID, whoever posted it — the web sandbox posts as a different account than local gh, so filtering on your own login misses a PR's sandbox-posted comment and adds a second. One by another login is never your last comment: update it through the stand-in below. --paginate matters — on a busy PR it often isn't on the first page.

A caller updating one page of a multi-page comment needs the current body to edit — hand it back on request: gh api repos/{owner}/{repo}/issues/comments/$SCREENSHOT_COMMENT_ID --jq .body.

Write the body to a file. With images to upload, attach one --attach per image the body references, and update $SCREENSHOT_COMMENT_ID in place rather than replacing it — a re-capture never posts a second comment. Which command depends on whether it's your last comment on the PR:

ME=$(gh api user --jq .login)
gh api --paginate "repos/{owner}/{repo}/issues/$PR_NUMBER/comments" --jq ".[] | select(.user.login == \"$ME\") | .id" | tail -1
  • No $SCREENSHOT_COMMENT_ID yet — post it: rtk proxy gh pr comment $PR_NUMBER --body-file <file> --attach tmp/a.png --attach tmp/b.png.
  • It's your last comment — --edit-last takes --attach, so the same command with --edit-last edits it in place and keeps its id.
  • You've commented since — --edit-last would hit that later one. Upload through a stand-in: post the body as a new comment with its --attaches, run the script below on it, write its body to a file (gh api …/issues/comments/<stand-in-id> --jq .body), PATCH that into $SCREENSHOT_COMMENT_ID as under With nothing to upload, then gh api -X DELETE the stand-in.

Then, on the comment the uploads landed on:

ruby .claude/skills/github-pr-images/assets/inline_attachments.rb {owner}/{repo} <comment-id>

rtk proxy because the hook reduces the output to "ok commented", and the comment URL it prints carries the id. The script moves each appended upload into its <img src> and PATCHes the comment; it stops without patching if a local src has no upload to take. An image already uploaded stays in the body as its asset URL — attach only what was recaptured.

With nothing to upload, edit in place: gh api -X PATCH repos/{owner}/{repo}/issues/comments/$SCREENSHOT_COMMENT_ID -F body=@<absolute-path> --jq .html_url. -F reads a file from @; -f would post the literal @<file>. The path is absolute because gh resolves @ against the shell's cwd.

Only edit the PR description when the user explicitly asks: gh pr edit $PR_NUMBER --body-file <file> --attach …, referencing images as ![alt](path) — the script patches comments only — with the existing body plus a ## Screenshots section — replace that section if it's already there rather than adding a second. The pr-guardrails hook denies gh pr edit until the pr skill is loaded.

Step 4: Verify

Read the posted body back:

gh api repos/{owner}/{repo}/issues/comments/<id> --jq .body

Pass: no tmp/ path left, an https://github.com/user-attachments/assets/ URL in place of each attached file, and still one ## Screenshots comment on the PR — rerun Step 3's listing. Don't curl those URLs — they 302 to a session-signed S3 URL that 403s unauthenticated.

Troubleshooting

IssueSolution
unknown flag: --attachbrew upgrade gh
no upload matched: <path>The <img>'s file wasn't attached, or its --attach gave a #alt — the script matches on the default alt, the basename
File path with special characters (e.g., Unicode narrow spaces from CleanShot)cp /path/CleanShot*keyword*.png tmp/screenshot.png
Upload refused--attach needs push access to the repo, and takes images and video only

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Read live production data from Bike Index through the admin OAuth token — Sidekiq and PgHero status, and the user-submitted bug reports — the same data as the cookie-gated dashboards, but agent-friendly JSON. Trigger when the user asks about production queue depth, job backlog, retries/dead jobs, running Sidekiq processes, or Postgres health (slow/blocked queries, index usage, unused/invalid indexes, connection counts, table/db sizes, vacuum/transaction-id danger) — and wants the *live* answer from production rather than logs or Honeybadger. Also trigger for anything about bug reports users have emailed in: what's been reported, searching/filtering them by status, tag or membership, or tagging one / linking it to the PR that fixes it. Also trigger when a request returns 401/expired and the AdminData token needs refreshing/re-authorizing. Not for reading log files (use production-log-inspection) or aggregated exception triage (bin/binx_hb).

The English summary is being prepared. Showing the original description.

bikeindex/bike_index308Updated Oct 29, 2026

How to fix a test that fails intermittently in Bike Index — one that passes locally but fails on CI, fails on one shard, passes on re-run, or is already tagged `:flaky`. **Read this before touching any spec that is failing intermittently, and before adding, raising, or relying on a `flaky:` tag or a `wait:` bump.** Trigger on a CI failure the user calls flaky, unreliable, intermittent, "green locally", "passes on retry", "fix CI", or a pasted `gh run`/Actions URL whose failure isn't reproducible; also whenever you catch yourself about to delete, skip, loosen, or retry an assertion to make a red build go green. **Equally: any spec that fails intermittently while you verify your own work** — deciding whether your change caused a flake, or whether to ship past one, is this skill's problem too, and it arrives with no CI run, no `:flaky` tag, and nobody but you calling it flaky.

The English summary is being prepared. Showing the original description.

bikeindex/bike_index308Updated Oct 29, 2026

Bike Index's frontend conventions — Tailwind class prefixing (`tw:`), the standard `twinput`/`twlabel`/`twlink` form/link classes, the `number_display` helper for numbers, `Counts` for site-wide figures (bikes registered, recoveries, organizations), the UI component library rule (every button is `UI::Button`/`UI::ButtonLink`, every typeahead/autocomplete is `UI::Forms::Combobox`, every FAQ is `UI::Accordion`, never hand-rolled markup), that **helpers are deprecated — render a view component taking full keyword arguments instead of adding or extending one**, ViewComponent rules (when to write a partial instead, keyword arguments, instance variables, `helpers.` prefix in templates), and `UI::Time::Component` for every date/time. Trigger when adding or modifying views (`.html.erb`), view components, Stimulus controllers, Tailwind classes, image or icon files under `app/assets/images` (which usually already exist), or any frontend code that touches styling or interactivity — including admin screens, whose unlayered legacy CSS inverts several of these rules. Stimulus.js is the JavaScript framework; SCSS and CoffeeScript files exist but are deprecated.

The English summary is being prepared. Showing the original description.

bikeindex/bike_index308Updated Oct 29, 2026

Capture desktop+mobile screenshots of Bike Index pages from the local `bin/dev` server via Playwright MCP, with a seeded-user identity gate that keeps PII out of uploaded images. Use whenever a task needs screenshots of local pages — PR documentation, bug repros, before/after comparisons across branches, design review, demos — including mid-interaction states like an open dropdown, a modal showing, a form mid-fill, or a hover. Use it even when the user just says "grab a screenshot" or "show me what this looks like" without naming Playwright. For a component that only renders under an env var / feature flag / hard-to-reach state (e.g. the review-app banner), screenshot its ViewComponent/Lookbook preview URL instead of a full page. **Also read the filename rule here before any `mcp__playwright__browser_take_screenshot` call**, including a one-off capture of some other site — it's what keeps PNGs out of the working tree.

The English summary is being prepared. Showing the original description.

bikeindex/bike_index308Updated Oct 29, 2026

Investigate and fix a specific Honeybadger exception in the Bike Index app — pull the fault, read its backtrace, find the offending code, write the fix. Trigger whenever a request names one Honeybadger error: a pasted fault URL (`app.honeybadger.io/projects/…/faults/…`), a fault ID, an error class plus "in production" (`Vips::Error`, `NoMethodError in AfterStolenRecordSaveJob`), or phrasings like "fix this Honeybadger error", "why is this erroring in prod", "what's causing this exception", "look at this HB fault". Use `bin/binx_hb` for fault and notice detail; it is the only Honeybadger reader this repo uses. Not for a broad production health sweep across many sources, and not for reading raw log files (that's production-log-inspection).

The English summary is being prepared. Showing the original description.

bikeindex/bike_index308Updated Oct 29, 2026

Bike Index conventions for browser specs (`type: :system, :js`) — **drive every step through the real UI** (no FactoryBot or `execute_script` shortcuts to skip what a user would do), every example pays a browser boot cost so bias toward fewer, denser examples that walk through state via clicks, prefer named-element matchers over CSS selectors, and combine same-setup work into one `it` even when scenarios feel independent. **Consult this skill any time you create or modify a `:js, type: :system` spec** — that includes everything under `spec/integration/` AND component system specs at `spec/components/**/*_system_spec.rb`. Read alongside the `rspec-testing` skill for the project's general `context`/`let` style.

The English summary is being prepared. Showing the original description.

bikeindex/bike_index308Updated Oct 29, 2026

See all skills by bikeindex

Report a problem with this skill