Gh Image
A gh CLI extension that uploads attachments to GitHub from the command line
Drop images and files into GitHub issues, PRs, and READMEs, straight from the command line.
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  $ 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
ghv2.99.0 or later and use--attach— for most people that is enough. It works ongh issue create,gh issue edit,gh issue comment,gh pr create,gh pr editandgh pr comment.
gh-image covers what that flag does not:
- Any file type GitHub accepts — PDFs, zips, logs, CSVs, source files.
--attachtakes 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 downloadpulls auser-attachmentsfile to disk. - CI and automation —
--attachrejects the ActionsGITHUB_TOKEN, so a workflow cannot attach anything with it.gh imageruns 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).
Verify build provenance
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.
Build from source
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.
HackBrowserData cookie backend (prebuilt)
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 Silicon | hbd-darwin-arm64 |
| macOS Intel | hbd-darwin-amd64 |
| Linux x86-64 | hbd-linux-amd64 |
| Linux arm64 | hbd-linux-arm64 |
| Windows x64 | hbd-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:
 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 —  — 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:
|
|
|
|
|
#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):
| Priority | Source | When to use |
|---|---|---|
| 1 | --token <value> flag |
One-off invocations |
| 2 | GH_SESSION_TOKEN env var |
CI/CD, shared machines, non-standard browsers |
| 3 | Browser cookie store | Local 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_sessioncookies 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_sessionmeans 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
- Run
gh image extract-tokenlocally to capture the token (token → stdout, status → stderr), then rungh image check-token --token <token>to confirm it authenticates as the intended user (username → stdout on success, exit code0= valid). - Create a GitHub environment (Settings → Environments → New environment), e.g.
gh-image, and restrict deployment branches to a trusted set (e.g.mainonly). - Add the token as an environment secret named
GH_SESSION_TOKENon 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_sessioncookies expire when GitHub invalidates the session. A scheduledcheck-tokenjob is the cleanest way to detect expiry before it breaks a real run.
#How it works
- When a
ghcommand follows--, hands the job togh --attachand stops there if it succeeds. - Tries a single authenticated upload with the
ghCLI token; on failure falls back to the browser-session flow below, resolving auser_sessioncookie from the configured source (flag → env → browser). - Fetches the target repository's page to obtain an
uploadTokenfrom the embedded JS payload. - Requests an S3 upload policy from
/upload/policies/assets. - Uploads the file directly to S3 using the presigned form fields.
- 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). - Prints the reference to stdout:
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_TOKENfor 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 whoseoriginremote is on GitHub. - The
ghCLI 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.
uploadTokenis 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
ghlogin 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 imageinvocation - 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