Awesome Go

gh-image

CategoryUtilities
SubcategoryUtilities
Stars277

A gh CLI extension that uploads images to GitHub issues, PRs, and READMEs from the command line, producing user-attachments URLs that respect repository visibility

About gh-image


gh-image drops a screenshot — or any GitHub-supported file like a PDF, zip, or log — into a bug report, README, or Slack thread without leaving the terminal, and uploads on private repos stay private. Images render as inline embeds, videos as inline players, and other files as download links.

$ gh image screenshot.png
![screenshot.png](https://github.com/user-attachments/assets/88f4599a-…-bc24)

$ gh image report.pdf
[report.pdf](https://github.com/user-attachments/files/123456/report.pdf)

[!NOTE] GitHub shipped first-party attachment support on 2026-09-01. To attach an image or video to an issue or PR on a repository you can push to, update to gh v2.99.0 or later and use --attach — for most people that is enough. It works on gh issue create, gh issue edit, gh issue comment, gh pr create, gh pr edit and gh pr comment.

gh-image covers what that flag does not:

  • Any file type GitHub accepts — PDFs, zips, logs, CSVs, source files. --attach takes nine image and video extensions.
  • Repositories you can only read — attach a screenshot to a bug report on a project you do not have push access to.
  • A link on its own — for a README, a commit message, a Slack thread, or anywhere that isn't an issue or PR.
  • Fetching attachments back down — gh image download pulls a user-attachments file to disk.
  • CI and automation — --attach rejects the Actions GITHUB_TOKEN, so a workflow cannot attach anything with it. gh image runs unattended with a session token (see CI / CD).

It also wraps --attach rather than competing with it: pass a gh command after -- and the eligible files go through gh itself. See Post it in one command.

Installation

gh extension install drogers0/gh-image

That's it. The gh CLI auto-detects your platform and downloads the prebuilt binary. Pre-built releases ship for macOS (arm64, amd64), Linux (amd64, arm64), Windows (amd64), and Android/Termux (arm64).

Release binaries are signed with build provenance attestations, which prove a binary was built by this repository's release workflow from a specific commit. To check one before installing, download it from the releases page and run:

gh attestation verify darwin-arm64 --owner drogers0

Note that gh extension install does not verify attestations itself — this is an explicit check for anyone who wants it.

git clone https://github.com/drogers0/gh-image
cd gh-image
go build -o gh-image
gh extension install .

Requires Go 1.26+.

The cookie backend defaults to kooky; go build -tags hbd uses hackbrowserdata instead. The hbd build pins a hackbrowserdata commit carrying the macOS securityd-dump gate and library log silencing until its next release; override either dep with go get <module>@<ref> or a go.mod replace.

An alternate build swaps the browser-cookie reader from kooky to hackbrowserdata for broader browser coverage (Arc, Vivaldi, Firefox WAL, …). It ships as separate hbd-<os>-<arch> release assets — gh extension install still gets the default kooky build. Download one and run it directly:

gh release download <tag> --repo drogers0/gh-image \
  --pattern hbd-darwin-arm64 --output gh-image --clobber   # pick your platform
chmod +x gh-image
./gh-image screenshot.png
Platform--pattern
macOS Apple Siliconhbd-darwin-arm64
macOS Intelhbd-darwin-amd64
Linux x86-64hbd-linux-amd64
Linux arm64hbd-linux-arm64
Windows x64hbd-windows-amd64.exe

Pre-releases need the explicit tag (gh won't treat them as latest). Verify provenance with gh attestation verify gh-image --owner drogers0, or build it yourself with -tags hbd (see Build from source).

Usage

# Upload an image (infers repo from the current git workspace)
gh image screenshot.png

# Upload multiple files at once (images or anything GitHub accepts)
gh image hero.png diagram.png chart.png

# Upload any GitHub-supported file (PDF, zip, log, …) — renders as a download link
gh image report.pdf

# Target a specific repository
gh image screenshot.png --repo owner/repo

Each successful upload prints a ready-to-paste reference on its own line — an inline embed for images, a bare URL for videos (which GitHub renders as an inline player), and a download link for other files:

![hero.png](https://github.com/user-attachments/assets/…)
https://github.com/user-attachments/assets/…
[report.pdf](https://github.com/user-attachments/files/…/report.pdf)

If any upload fails, the error is printed to stderr and the process exits non-zero — other files in the batch still upload.

Download

# Fetch attachments, named from the URL, into the current directory
gh image download <url>... [--output-dir <dir>] [--no-clobber]

# Or send a single attachment somewhere specific — `-` for stdout
gh image download <url> --output <file>

Existing files are overwritten unless --no-clobber is passed, which writes name.1, name.2 instead. As with upload, a failed URL is reported to stderr and the process exits non-zero — the rest of the batch still downloads.

Post it in one command

Anything after -- is a gh command, forwarded as written. The files upload first and their links land in the body:

gh image bug.png              -- issue comment 42 --body-file repro.md
gh image diagram.pdf          -- pr create --fill --body-file design.md
gh image before.png after.png -- issue create --title "Layout breaks under 600px" --body-file report.md

Point the body at the files on disk — ![before](./before.png) — and those references are repointed at the uploaded assets; a file the body never mentions is appended to the end. A filename beginning with a dash is written ./-name.png, since -- now starts the gh command.

For a link that belongs somewhere other than an issue or PR, compose by hand instead: --body "$(gh image bug.png)".

Use with AI agents

gh-image is packaged as an agent skill, so AI coding agents can upload and embed images or attach files for you — just ask in natural language, e.g. "attach this screenshot to the PR" or "file an issue and attach this log."

npx skills add drogers0/gh-image

The installer detects your local agents automatically; add --agent claude-code codex opencode --global to install for several at once. The open Agent Skills standard is supported by Claude Code, OpenAI Codex, OpenCode, Cursor, GitHub Copilot, and many more. The skill checks that this extension is present (asking you to install it if not), runs the upload, and embeds the resulting user-attachments URL into a PR, issue, or comment. It never installs anything on your behalf.

Who's using gh-image

Shipping visual evidence in production review pipelines:

 NousResearch/hermes-agent  ★ 226k+ Automated visual proof on every single pull request, with no human in the loop: the publish job uploads the E2E screenshots inline using a specially scoped attachment bot account.

 openshift/console ★ 450+  ·  Red Hat Its /qa-verify agent skill hands reviewers full-resolution QA evidence — CDN-hosted, so nothing gets downscaled to fit a comment.

 Simpleyyt/ai-manus ★ 1.6k+ A demo-videos skill publishes the README demo reels — gh image is mandatory, since only user-attachments URLs autoplay inline.

Authentication

gh-image authenticates with credentials you already have — nothing to provision, no OAuth scopes to configure. Images and video going to a repository you can push to are uploaded with your gh CLI token; everything else — other file types, and repositories you cannot push to — falls back to your existing GitHub session, read as the user_session cookie from your browser's encrypted cookie store. Downloads take the same two routes: the gh token first, your browser session as fallback. Posting with -- uses the same credentials — only the files gh turns down reach the session route.

Supported browsers: Chrome · Brave · Chromium · Edge · Firefox · Opera · Safari

Supported platforms: macOS · Linux · Windows · Android (Termux)

On macOS, a Keychain prompt may appear on first use to authorize access to your browser's cookie encryption key. Click Always Allow to skip future prompts.

[!NOTE] When browser cookies aren't available: Chrome 127+ on Windows isn't yet supported by the underlying cookie library (workarounds), and Android (Termux) has no browser cookie store at all. In either case, supply the token explicitly via GH_SESSION_TOKEN (see Session token override below); on Windows you can also just use another browser.

Session token override

For CI, headless environments, or shared machines, you can supply the session token explicitly. Resolution order (first match wins):

PrioritySourceWhen to use
1--token <value> flagOne-off invocations
2GH_SESSION_TOKEN env varCI/CD, shared machines, non-standard browsers
3Browser cookie storeLocal interactive use (default)
# Flag (visible in process listings like `ps aux` — avoid on shared machines)
gh image --token "$MY_TOKEN" screenshot.png --repo owner/repo

# Environment variable (preferred — not visible to `ps aux`)
GH_SESSION_TOKEN="$MY_TOKEN" gh image screenshot.png --repo owner/repo

# Non-standard browser not auto-detected (Firefox forks like Floorp/LibreWolf)?
GH_SESSION_TOKEN="$(sqlite3 ~/path/to/profile/cookies.sqlite "SELECT value FROM moz_cookies WHERE name='user_session' AND host LIKE '%github.com'")" \
  gh image screenshot.png --repo owner/repo

[!WARNING] user_session cookies grant full account access — they are not scoped like personal access tokens. Treat them with the same care as a password. If leaked, sign out of GitHub on the machine that holds the session; if you are not on that machine, revoke it through Settings → Sessions, or change your password (which kills every session in one action).

CI / CD

gh-image runs unattended in GitHub Actions when given a session token via GH_SESSION_TOKEN.

[!CAUTION] Use a dedicated bot account for CI/CD on shared repos. GitHub hides secret values in the UI and masks log emissions, but a determined collaborator with write access can craft a workflow that exfiltrates the value through channels masking doesn't cover. Storing your personal user_session means such a leak compromises your account; a bot account scopes the blast radius to that bot. Decide whose token to extract in step 1 below accordingly.

Setup

  1. Run gh image extract-token locally to capture the token (token → stdout, status → stderr), then run gh image check-token --token <token> to confirm it authenticates as the intended user (username → stdout on success, exit code 0 = valid).
  2. Create a GitHub environment (Settings → Environments → New environment), e.g. gh-image, and restrict deployment branches to a trusted set (e.g. main only).
  3. Add the token as an environment secret named GH_SESSION_TOKEN on that environment.
jobs:
  upload:
    runs-on: ubuntu-latest
    environment: gh-image                                # binds this job to the scoped environment
    steps:
      - name: Upload screenshots
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}              # for gh CLI auth
          GH_SESSION_TOKEN: ${{ secrets.GH_SESSION_TOKEN }}  # for the upload itself
        run: |
          gh extension install drogers0/gh-image
          gh image check-token                                # optional: fail fast if the session expired
          gh image screenshot.png --repo ${{ github.repository }}

[!NOTE] user_session cookies expire when GitHub invalidates the session. A scheduled check-token job is the cleanest way to detect expiry before it breaks a real run.

How it works

  1. When a gh command follows --, hands the job to gh --attach and stops there if it succeeds.
  2. Tries a single authenticated upload with the gh CLI token; on failure falls back to the browser-session flow below, resolving a user_session cookie from the configured source (flag → env → browser).
  3. Fetches the target repository's page to obtain an uploadToken from the embedded JS payload.
  4. Requests an S3 upload policy from /upload/policies/assets.
  5. Uploads the file directly to S3 using the presigned form fields.
  6. Calls back to GitHub to finalize the asset, using the finalize endpoint GitHub returns in the policy (/upload/assets/{id} for images, /upload/repository-files/{id} for other files).
  7. Prints the reference to stdout: ![name](url) for images, the bare URL for videos (GitHub renders it as an inline player), or [name](url) for other files.

The final URL is https://github.com/user-attachments/assets/<uuid> for images and https://github.com/user-attachments/files/<id>/<name> for other files. Until it is referenced in rendered content it resolves only for the uploader, even on a public repo; once referenced, visibility follows the content that references it.

For the full architecture, see documentation/architecture.md. For the reverse-engineered upload protocol, see documentation/github-image-upload-flow.md.

Requirements

  • A supported browser with an active GitHub session — or a GH_SESSION_TOKEN for CI.
  • Read access to the target repository — write access is not required.
  • A target repository — pass --repo owner/repo, or run from a git workspace whose origin remote is on GitHub.
  • The gh CLI must be installed and authenticated (used for repository ID lookup, and for posting through --).

Limitations

  • Uses an undocumented internal GitHub API that may change without notice.
  • uploadToken is usually present on repository pages for any user who can view the repo. An invalid or expired session is the case where it is absent.
  • Session cookies are not scoped credentials; they expire when GitHub invalidates the session.
  • Uploads are attributed to the account that authenticated them, so if your browser session and your gh login are different accounts, images and video may be attributed differently from other files. Supply a session token explicitly to pin every upload to one account.

Contributing

Issues and pull requests are welcome. For bug reports, please include:

  • Your OS and browser
  • The exact gh image invocation
  • The error output (with any session token values redacted)

Before opening a PR, run go test ./... and go vet ./....

Support

If gh-image saves you a few drag-and-drops, a ⭐ helps others find it:

gh api --method PUT user/starred/drogers0/gh-image

(or just click the star at the top of this page)

License

MIT © 2025-2026 drogers0

Frequently Asked Questions

What is gh-image?

gh-image is a Utilities library for the Go programming language. A gh CLI extension that uploads images to GitHub issues, PRs, and READMEs from the command line, producing user-attachments URLs that respect repository visibility

How do I install gh-image?

Install gh-image with the Go module system using `go get drogers0/gh-image`. Check the repository for the current installation instructions.

What category does gh-image belong to?

gh-image is listed under Utilities, specifically Utilities.

← Back to Utilities