---
title: "Add a screenshot to a GitHub pull request from the CLI"
canonical_url: "https://krowk.com/guides/screenshot-to-github-pr"
last_updated: "2026-09-02T08:10:25.185Z"
meta:
  description: "GitHub has no image upload API, so a headless agent cannot attach one. Push the file to a permalink and post the markdown block the PR comment renders inline."
  "og:description": "GitHub has no image upload API, so a headless agent cannot attach one. Push the file to a permalink and post the markdown block the PR comment renders inline."
  "og:title": "Add a screenshot to a GitHub pull request from the CLI"
---

# **Put an agent's screenshot in a GitHub pull request**

Last updated August 27, 2026

An agent finishes a UI change, takes a screenshot, and the screenshot is sitting in a container that is about to be thrown away. The pull request is where somebody will look for it. Nothing in the GitHub API closes that gap: attaching an image to a comment is a browser action against a logged-in session, so a headless run cannot do it at all.

What does work is hosting the file somewhere with a stable URL and posting a markdown image that points at it. GitHub renders an inline image from any host, and it renders it in the comment body rather than as a link somebody has to decide whether to click.

## The whole thing

Two commands. The first uploads the file and prints the block GitHub renders; the second posts it.

agent shell

```
BLOCK=$(krowk push screenshot.png \
  --caption "Cart total after the fix" \
  --destination github)

gh pr comment 412 --body "$BLOCK"
```

The first push needs no account. The link works immediately and expires 24 hours later, which is usually longer than the review takes; **keeping it** is a separate step, below.

## Step by step

### 1. Put the binary on the path

terminal

```
curl -fsSL https://krowk.com/install | bash
```

One static Go binary, no runtime — which is the whole reason it is a Go binary and not a package. If Node is already there, `npx @krowk/cli push screenshot.png` runs the same binary.

### 2. Push the file

agent shell

```
krowk push screenshot.png --caption "Cart total after the fix"
```

Inside a git checkout the push also records the repository, the commit, the branch, whether the tree was dirty, and which agent was running. None of that is passed by hand — it is read from git and from the environment. On a `pull_request` build in Actions the pull request comes along too.

### 3. Post it on the pull request

`--destination github` prints the form GitHub renders and nothing else, so it goes straight into a comment body:

agent shell

```
gh pr comment 412 --body "$(krowk push shot.png --destination github)"
```

Destinations disagree about what a link is, which is why there is a flag for it. GitHub takes the block; a chat tool wants the bare URL and would render the block's image as nothing. Ask for `--destination slack` and you get the bare URL instead. The CLI does not decide this — it passes through what the registry sends, so the table can grow without you upgrading anything.

## What lands in the comment

For an image the block is the picture inline, a caption line, and a link through to the preview page:

markdown

```
[![Cart total after the fix](https://cdn.krowkusercontent.com/…/shot.png)](https://krowk.com/a/art_00000000000000000000demo)
Cart total after the fix · [View preview ↗](https://krowk.com/a/art_00000000000000000000demo)
```

The image comes off the CDN, so it appears in the comment without a click. The click goes to the preview page rather than to the raw bytes, and that page is what carries the repository, commit, branch and agent — the reviewer can tell which commit produced the picture without asking.

## Keeping the link

A keyless push lands in a shared anonymous workspace and expires after 24 hours. The push prints a one-shot claim token beside the link; spending it moves the artifact into your own workspace and lifts the expiry. It works for 30 days past the expiry and the URL comes back exactly as it was, so the link already sitting in the pull request starts working again. A claim token is a secret — it is the one thing from a push that must never go in a comment.

terminal

```
krowk claim art_00000000000000000000demo krowk_claim_…
```

With a key configured there is nothing to claim: every push lands in your workspace and no link expires. `krowk auth login` approves the machine in a browser, or takes `--token` in CI.

## Letting the agent do it

The adoption step is one instruction, not a tour. This is the line that goes in CLAUDE.md, AGENTS.md, or a Cursor rule:

CLAUDE.md

```
After any visual change, capture it (screenshot if static, a short screen recording if it is an interaction) and publish it with the krowk skill. If the skill is missing or fails, run `krowk help --json`, then `krowk push FILE --title "what changed" --json`, and paste `paste.url` where you report the work.
```

The installer drops an agent skill into `~/.claude/skills`, and `krowk help --json` returns the whole command surface as data, so an agent that has never seen the tool does not have to guess at a flag spelling.

## Two things that will bite you

- **Comment permissions in CI.** A workflow that posts a comment needs `pull-requests: write`. A pull request from a fork does not get it by default, so the comment step fails there; the upload still succeeds and the links land in the job summary.
- **Point the markdown at the image, not the page.** The preview page is an HTML document — a markdown image tag pointing at it renders as a broken image. That is the whole reason the block comes assembled: the picture is the CDN URL, the link is the page, and getting them the wrong way round is the easiest mistake to make by hand.

[Read the CLI reference](https://krowk.com/docs)

The reference for everything above lives in [~~the docs~~](https://krowk.com/docs#pasting).