Skip to content

drogers0/gh-image

Repository files navigation

gh-image banner

Drop images into GitHub issues, PRs, and READMEs, straight from the command line.

Latest release GitHub stars Total downloads License: MIT Go Report Card


GitHub has no public API for image uploads. The web UI uses an internal endpoint that produces user-attachments URLs whose visibility is scoped to the repository they were uploaded to. gh-image replicates that flow as a gh CLI extension, so you can drop a screenshot into a bug report, README, or Slack thread without leaving the terminal — and images on private repos stay private.

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

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), and Windows (amd64).

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+.

Usage

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

# Upload multiple images at once
gh image hero.png diagram.png chart.png

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

Each successful upload prints a ready-to-paste markdown reference on its own line:

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

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

Pipe directly into an issue, PR, or comment

From inside the repo's working directory, both gh image and gh issue create infer the target repository automatically:

gh issue create \
  --title "Login button stuck in loading state" \
  --body "Repro on staging:

$(gh image bug.png)

Happens consistently after the third click."

Authentication

gh-image authenticates with your existing GitHub session — no tokens to provision, no OAuth scopes to configure for everyday local use. The tool reads the user_session cookie from your browser's encrypted cookie store.

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

Supported platforms: macOS · Linux · Windows

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

Windows + Chrome 127+: Some versions of Chrome on Windows are not yet supported by the underlying cookie library. Use another browser or investigate potential workarounds.

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
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

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. Resolves a user_session cookie from the configured source (flag → env → browser).
  2. Fetches the target repository's page to obtain an uploadToken from the embedded JS payload.
  3. Requests an S3 upload policy from /upload/policies/assets.
  4. Uploads the file directly to S3 using the presigned form fields.
  5. Calls back to GitHub to finalize the asset.
  6. Prints ![name](url) to stdout.

The final URL is the standard https://github.com/user-attachments/assets/<uuid> format — visibility inherits from the target repository, so a private-repo upload requires authentication to view.

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.
  • Write access to the target repository (uploads require it).
  • 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).

Limitations

  • Uses an undocumented internal GitHub API that may change without notice.
  • uploadToken is only issued to users with write access on the target repository.
  • Session cookies are not scoped credentials; they expire when GitHub invalidates the session.

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

About

A gh CLI extension that uploads images to GitHub from the command line

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages