---
title: "PR Visual Recap"
description: "A GitHub Action that runs your repo's visual-recap skill on every PR. An LLM coding agent reads the diff, publishes an interactive recap plan, shows an informational check, and posts a sticky PR comment with an inline screenshot. Informational and non-blocking."
---

# PR Visual Recap

PR Visual Recap is a GitHub Action that turns every pull request into a **visual code review**. On each push, an LLM coding agent runs the latest bundled [`visual-recap`](/docs/template-plan) skill (or your repo's committed copy when `VISUAL_RECAP_SKILL_SOURCE=repo`) against the PR diff, publishes a structured recap plan to the hosted Plans app, shows an informational `Visual Recap` check while it runs, and upserts **one sticky PR comment** that links to the interactive plan with an **inline screenshot** embedded right in the comment.

This is not a deterministic diff renderer. The action invokes a real coding agent (Claude Code CLI by default, OpenAI Codex CLI, or Agent-Native Code with an OpenAI-compatible provider) that reads the change, decides what matters, and authors the recap by calling the Plans MCP tool `create-visual-recap` — the same tool the `/visual-recap` slash command uses. You get a high-altitude, schema/API/before-after view of the change instead of a wall of raw diff.

The recap is **informational and non-blocking**. It creates a check row so reviewers can see that generation is in progress, but it is not a required check, it never blocks the PR, and it never replaces reading the actual diff. The sticky comment is a review aid, not a sign-off.

## What it does

On each PR push, the workflow:

1. Collects a bounded diff between the PR base and head.
2. Creates an informational `Visual Recap` GitHub check with `Visual recap in progress`.
3. Runs the configured coding agent against that diff. The agent reads the bundled `visual-recap` skill guidance (or your repo-pinned copy) and authors a recap, publishing it with `create-visual-recap`.
4. Reads the published plan URL the agent wrote to `recap-url.txt`.
5. Opens that URL in headless Chrome and screenshots the rendered plan in light and dark modes.
6. Uploads the PNGs to a signed public image route on the Plans app.
7. Upserts a single sticky PR comment that embeds the screenshots **inline** with a `<picture>` element (served through GitHub's camo image proxy) next to the link to the interactive recap.
8. Completes the `Visual Recap` check as success, skipped, or neutral.

<Diagram id="doc-block-1af1rpi" title="What happens on each PR push" summary={"A bounded diff feeds a real coding agent, which authors a recap; the workflow screenshots it and upserts one sticky comment."}>

```html
<div class="diagram-recap">
  <div class="diagram-node">
    PR push<br /><small class="diagram-muted"
      >bounded base&hellip;head diff</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-node">
    Coding agent<br /><small class="diagram-muted"
      >configured backend reads diff</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel center">
    <span class="diagram-pill accent">create-visual-recap</span
    ><small class="diagram-muted">publishes recap plan</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-node">
    Headless Chrome<br /><small class="diagram-muted"
      >light + dark screenshots</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box">
    One sticky PR comment<br /><small class="diagram-muted"
      >inline screenshot + plan link</small
    >
  </div>
</div>
<div class="diagram-foot diagram-muted">
  Plus an informational <span class="diagram-pill">Visual Recap</span> check
  &mdash; non-blocking, never required.
</div>
```

```css
.diagram-recap {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.diagram-recap .diagram-arrow {
  font-size: 20px;
  line-height: 1;
}
.diagram-recap .center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-align: center;
}
.diagram-recap .diagram-foot {
  flex-basis: 100%;
  margin-top: 10px;
  font-size: 13px;
}
```

</Diagram>

A re-push updates the same plan and the same sticky comment in place — no orphaned plans, no comment spam.

## Installing it

When you install Plans interactively, the Agent-Native CLI asks whether to add
automatic PR Visual Recaps. Say yes to write the GitHub Action, or add it
explicitly at any time:

```bash
npx @agent-native/core@latest skills add visual-plan --with-github-action
```

This installs the `visual-plan` skill (which includes the `visual-recap` skill the action runs) and writes `.github/workflows/pr-visual-recap.yml` into your repo. The workflow calls **published CLI subcommands** through `npx @agent-native/recap-cli@latest recap <subcommand>` — including `gate`, `collect-diff`, `block-reference`, `scan`, `build-prompt`, `publish`, `shot`, `comment`, `check`, and `usage` — so nothing is copied into your repo as helper scripts. `setup` and `doctor` are the interactive helpers you run locally; `gate` is the security-gate step the workflow runs before every recap.

Then run the guided setup helper:

```bash
npx @agent-native/recap-cli@latest recap setup
npx @agent-native/recap-cli@latest recap doctor
```

`recap setup` refreshes the workflow, uses `gh` to set GitHub Actions
secrets/variables when values are available from env or the local Plans
publish-token store, and prints exact missing commands for anything it cannot
set. Secret values are sent to `gh` through stdin, not command arguments. Commit
the generated workflow file and open a PR to see it run.

By default, the workflow builds its agent prompt from the latest bundled
`visual-recap` guidance in `@agent-native/recap-cli@latest`, including any sibling
reference files the skill ships with. If your repo intentionally customizes and
pins its committed `visual-recap` folder, set the repository variable
`VISUAL_RECAP_SKILL_SOURCE=repo`.

## Runner selection

The workflow runs on GitHub-hosted `ubuntu-latest` runners by default. To use a
different hosted runner or your own self-hosted runner, set the repository or
organization variable `VISUAL_RECAP_RUNS_ON` to a valid JSON string or JSON
array of runner labels:

```text
"ubuntu-latest"
["self-hosted", "linux", "x64", "visual-recap"]
```

The JSON quotes are required. This setting selects the full `recap` job runner.
To configure it during guided setup, pass the same JSON value to `--runs-on`,
then validate the workflow and runner availability with `doctor`:

```bash
npx @agent-native/recap-cli@latest recap setup --runs-on '["self-hosted","linux","x64","visual-recap"]'
npx @agent-native/recap-cli@latest recap doctor
```

The lightweight security gate defaults separately to GitHub-hosted
`ubuntu-latest`. Repositories that disable GitHub-hosted runners can set
`VISUAL_RECAP_GATE_RUNS_ON` to one **plain, single runner label** (not JSON), or
pass `--gate-runs-on visual-recap-gate` during setup. The reusable workflow
exposes the same value as its `gate-runs-on` input. This avoids parsing dynamic
JSON before GitHub can schedule the gate. The custom label is selected only for
same-repository PRs authored by an `OWNER`, `MEMBER`, or `COLLABORATOR`. Forks
and all other authors are forced to GitHub-hosted `ubuntu-latest` before the job
starts; in a self-hosted-only repository, those untrusted runs may remain
unscheduled rather than touching a self-hosted machine.

`recap doctor` rejects malformed or unsupported JSON and reports when GitHub
does not expose an online runner matching every configured label. Runner
registration, installation, upgrades, capacity, and maintenance remain your
responsibility; Agent Native selects the labels but does not provide or operate
the runner.

**Security:** Prefer dedicated, ephemeral self-hosted runners. A custom gate
runner receives only trusted same-repository PR events. The stock gate does not
check out the PR tree; it evaluates the workflow logic and GitHub PR metadata.
A persistent machine still retains process state across jobs, so do not share
it with unrelated workloads. The workflow fails closed before dispatching any
self-hosted gate or full recap job for untrusted public or fork PR authors. Do
not weaken that boundary or route untrusted contributions to a persistent
runner.

## Backend selection

Choose which coding agent runs the skill with the `VISUAL_RECAP_AGENT` repository variable:

| `VISUAL_RECAP_AGENT` | Coding agent                            | Required API key       | Required variables                            |
| -------------------- | --------------------------------------- | ---------------------- | --------------------------------------------- |
| `claude` _(default)_ | Claude Code CLI                         | `ANTHROPIC_API_KEY`    | —                                             |
| `codex`              | OpenAI Codex CLI                        | `OPENAI_API_KEY`       | —                                             |
| `openai-compatible`  | Agent-Native Code + compatible provider | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` |

If the variable is unset, the action uses `claude`.

## Model and reasoning

Beyond the backend, these repository variables tune how the agent runs:

- **`VISUAL_RECAP_MODEL`** pins the model passed to the CLI (`--model`) — for example `gpt-5.6-luna` for Codex, a Claude model id, or `deepseek-chat` for DeepSeek. It is required for `openai-compatible`. For the **Claude** backend, leaving it unset now defaults to `claude-sonnet-5`; set the variable explicitly to use a different Claude model, such as the cheaper `claude-haiku-4-5`. For the **Codex** backend, leaving it unset uses the Codex CLI's own default model.
- **`VISUAL_RECAP_REASONING`** sets the reasoning depth: `none`, `minimal`, `low`, `medium`, `high`, or `xhigh`. It applies to the Codex backend; Claude's reasoning is model-driven, so this variable is ignored there.
- **`VISUAL_RECAP_BASE_URL`** selects the HTTP(S) base URL for the `openai-compatible` backend. Use the provider's OpenAI-compatible endpoint, such as DeepSeek or Kimi, without embedding credentials in the URL.
- **`VISUAL_RECAP_SKILL_SOURCE`** controls prompt freshness: `auto`/unset uses the latest bundled skill guidance, while `repo` pins to the committed repo-local `visual-recap` skill folder.

For example, to run the recap on Codex with GPT-5.6 Sol at high reasoning, set the repository variables `VISUAL_RECAP_AGENT=codex`, `VISUAL_RECAP_MODEL=gpt-5.6-sol`, and `VISUAL_RECAP_REASONING=high`.

For DeepSeek, set `VISUAL_RECAP_AGENT=openai-compatible`, `VISUAL_RECAP_BASE_URL=https://api.deepseek.com/v1`, `VISUAL_RECAP_MODEL=deepseek-chat`, and the `VISUAL_RECAP_API_KEY` secret. Kimi works the same way with its OpenAI-compatible base URL and model id. Other OpenAI-compatible providers use the same three settings.

## Cost

Each recap run is a full agentic coding-agent session reading a real diff, not a cheap templated summary, so its cost tracks model choice and how often it runs:

- **A full session per PR push.** The default trigger fires on `synchronize`, so a PR that gets pushed to ten times runs the agent ten times.
- **The whole PR each time**, not just the commits since the last run — cost scales with total PR size, not incremental diff size.
- **Diff input capped at ~600KB.** This bounds the worst case per run, but large diffs still cost meaningfully more than small ones.
- **One automatic retry on failure**, which can roughly double the cost of a run that fails transiently.

To reduce cost:

- **Pin a cheaper model.** For Claude, set `VISUAL_RECAP_MODEL=claude-haiku-4-5` for simple or small repos — it's the cheapest Claude tier; `claude-sonnet-5` (the default) is the balanced tier.
- **Switch to Codex with a cheaper model.** Set `VISUAL_RECAP_AGENT=codex` with `VISUAL_RECAP_MODEL=gpt-5.6-luna` (the cheapest GPT-5.6 tier) or `gpt-5.6-terra` (mid-tier), and set `VISUAL_RECAP_REASONING=low` to cut reasoning-token spend further. Reasoning depth only applies to Codex.
- **Point at a cheaper OpenAI-compatible provider.** Set `VISUAL_RECAP_AGENT=openai-compatible` with `VISUAL_RECAP_BASE_URL`/`VISUAL_RECAP_MODEL`/`VISUAL_RECAP_API_KEY` targeting DeepSeek, Kimi, or any other OpenAI-compatible endpoint.
- **Require an explicit PR label.** Set `VISUAL_RECAP_REQUIRED_LABELS=visual recap` to keep the workflow installed but skip recaps until a PR has that label. Use a comma-separated list, such as `visual recap,recap`, when several labels should opt in. Applying a listed label wakes the workflow immediately.
- **Recap less often.** Edit the installed workflow's `on.pull_request.types` from `[opened, synchronize, reopened, ready_for_review, labeled, closed]` to `[opened, ready_for_review, closed]` so it recaps only when a PR opens and when it merges, instead of on every push.

Every run records what it actually spent: the workflow's `recap usage` step attaches token and cost usage to the published plan, so you can look at real spend per run instead of guessing.

## Secrets and variables

Set these in your repository's **Settings → Secrets and variables → Actions**.

### Secrets for the default backend

| Secret              | Purpose                                                                                                                            |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `PLAN_RECAP_TOKEN`  | Revocable token minted by `npx @agent-native/core@latest connect`. Authorizes publishing the recap plan and the screenshot upload. |
| `ANTHROPIC_API_KEY` | The LLM key for the default Claude Code backend.                                                                                   |

**Teams: use an org service token.** A personal token is bound to the person
who minted it — if they leave the org or revoke their tokens, every repo using
that secret starts failing with 401s, and CI-created plans are owned by that
individual instead of the team. An org service token is owned by your
**organization**: it acts as a service principal (`svc-<name>@service.<orgId>`),
survives any individual leaving, the recaps it publishes are org-visible, and
any org owner or admin can list or revoke it. Mint one (org owner/admin only):

```bash
npx @agent-native/core@latest connect https://plan.agent-native.com --service-token pr-recap
```

The command authenticates you in the browser, then prints the service token
exactly once — store it as the `PLAN_RECAP_TOKEN` secret. Manage it later with
the `list-org-service-tokens` and `revoke-org-service-token` actions on the
Plans app.

**Solo: a personal token still works.** Mint it with `npx @agent-native/core@latest connect`
against your Plans app. For the hosted app, this also writes a local
publish-token file that `npx @agent-native/recap-cli@latest recap setup` can read:

```bash
npx @agent-native/core@latest connect https://plan.agent-native.com --client codex
npx @agent-native/recap-cli@latest recap setup
```

If you prefer manual setup, paste the token into the GitHub secret. Use a
placeholder like `plan_recap_xxxxxxxxxxxxxxxx` only for examples — never commit a
real token.

### Optional (only if you change defaults)

| Secret / variable              | Default                                                       | When you need it                                                                                                                                                                                                                                         |
| ------------------------------ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OPENAI_API_KEY`               | —                                                             | Secret. Set together with `VISUAL_RECAP_AGENT=codex` to run the recap with Codex instead.                                                                                                                                                                |
| `VISUAL_RECAP_API_KEY`         | —                                                             | Secret. Set with `VISUAL_RECAP_AGENT=openai-compatible` for DeepSeek, Kimi, or another OpenAI-compatible provider.                                                                                                                                       |
| `VISUAL_RECAP_AGENT`           | `claude`                                                      | Variable. Selects the coding-agent backend (`claude`, `codex`, or `openai-compatible`).                                                                                                                                                                  |
| `VISUAL_RECAP_BASE_URL`        | required for compatible backend                               | Variable. HTTP(S) base URL for the OpenAI-compatible backend; credentials are rejected.                                                                                                                                                                  |
| `VISUAL_RECAP_MODEL`           | `claude-sonnet-5` for Claude; required for compatible backend | Variable. Provider model id for OpenAI-compatible backends (required there). For Claude, unset now defaults to `claude-sonnet-5` — set it to override, e.g. `claude-haiku-4-5` for the cheapest tier. For Codex, unset uses the Codex CLI's own default. |
| `VISUAL_RECAP_REASONING`       | each model's default                                          | Variable. Reasoning depth: `none`, `minimal`, `low`, `medium`, `high`, or `xhigh`. Applies to the Codex backend.                                                                                                                                         |
| `VISUAL_RECAP_REQUIRED_LABELS` | —                                                             | Variable. Comma-separated PR labels. When set, the gate skips until the PR has at least one listed label, for example `visual recap`.                                                                                                                    |
| `RECAP_CLI_VERSION`            | `latest`                                                      | Variable. Pins the `@agent-native/recap-cli` version the workflow installs — e.g. `1.5.0`. See [Version pinning](#version-pinning-copy-variant).                                                                                                         |
| `PLAN_RECAP_APP_URL`           | `https://plan.agent-native.com`                               | Secret. Only when self-hosting the Plans app at a different origin.                                                                                                                                                                                      |

The workflow auto-detects how to invoke its helper CLI (local source inside this monorepo, the published `@agent-native/recap-cli` elsewhere), so there is no `RECAP_CLI` variable to set.

## Inline screenshot in the comment

After the agent publishes the recap, the workflow screenshots the rendered plan in headless Chrome in both light and dark modes and uploads the PNGs to a signed public image route on the Plans app. The sticky PR comment then embeds those screenshots **inline** with a `<picture>` element — GitHub re-serves them through its camo proxy, so reviewers see a preview that matches their GitHub theme directly in the comment without opening anything. The link to the full interactive plan sits right next to it for when they want to explore, comment, or annotate.

## Fork PRs

### Default behavior (no action required)

The main `pr-visual-recap.yml` workflow fires on the plain `pull_request` trigger, **not** `pull_request_target`. Fork PRs therefore run with **no access to repository secrets**, so the workflow finds no `PLAN_RECAP_TOKEN` and cleanly no-ops — no failed publish, no credentials exposed. Recaps run automatically for PRs from branches in the same repository, where the secrets are available.

This also means you can merge the workflow file **before** the secrets exist: with no token configured, every run is a quiet no-op until you set the secrets. The `gate` step also skips draft PRs and bot-authored PRs automatically, so neither trigger recap runs by default.

### Opt-in with the label-gated fork workflow

If you want to generate recaps for fork PRs, a second workflow file is available: `.github/workflows/pr-visual-recap-fork.yml`. It uses `pull_request_target` (which runs with base-repo secrets) but never checks out or executes fork code. Trusted fork authors with GitHub author association `OWNER`, `MEMBER`, or `COLLABORATOR` run automatically. Outside fork PRs require an explicit **per-head maintainer opt-in** via a fresh `recap` label event before the recap agent runs.

To install it, copy the file from [BuilderIO/agent-native](https://github.com/BuilderIO/agent-native/blob/main/.github/workflows/pr-visual-recap-fork.yml) into your repo's `.github/workflows/` directory alongside the existing `pr-visual-recap.yml`. The same backend configuration applies, including `VISUAL_RECAP_API_KEY`, `VISUAL_RECAP_BASE_URL`, and `VISUAL_RECAP_MODEL` when using an OpenAI-compatible provider.

<Diagram id="doc-block-h3lne0" title="Fork PR consent gate" summary={"Fork PRs get no secrets by default; trusted authors run automatically, and outside contributors require a fresh maintainer recap label."}>

```html
<div class="diagram-fork">
  <div class="diagram-node">
    Fork PR opened<br /><small class="diagram-muted"
      >main workflow has no secrets</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-card">
    <span class="diagram-pill ok">Trusted author</span
    ><small class="diagram-muted"
      >OWNER, MEMBER, or COLLABORATOR runs automatically</small
    >
  </div>
  <div class="diagram-card">
    <span class="diagram-pill warn">Outside contributor</span
    ><small class="diagram-muted"
      >maintainer reviews diff, then applies <code>recap</code></small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel center">
    Gate checks<br /><small class="diagram-muted"
      >fork PR? &amp; trusted or fresh label?</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box ok">
    Recap runs<br /><small class="diagram-muted"
      >base-repo code only · fork diff is text input</small
    >
  </div>
</div>
```

```css
.diagram-fork {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.diagram-fork .diagram-arrow {
  font-size: 20px;
  line-height: 1;
}
.diagram-fork .center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-align: center;
}
.diagram-fork .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px 14px;
}
```

</Diagram>

### How the label gate works

1. A fork contributor opens a PR. The normal `pull_request` workflow skips because GitHub withholds secrets from fork runs.
2. The fork workflow checks the PR author association. Trusted authors (`OWNER`, `MEMBER`, or `COLLABORATOR`) run automatically on open, synchronize, reopen, and ready-for-review events.
3. Outside contributors require a maintainer to review the current diff (especially for prompt-injection-shaped content — see below), then apply the `recap` label to the PR.
4. The outside-contributor label gate is per head SHA: if the contributor pushes more commits, the next synchronize event skips until a maintainer removes and reapplies `recap` after reviewing the new diff.

### What the fork workflow does and does NOT do

| The workflow DOES                                                                                                      | The workflow does NOT                                                              |
| ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Checkout the **base repository** at the **base branch ref** — trusted code only                                        | Check out or execute any code from the fork                                        |
| Fetch the fork head as a remote ref (`git fetch origin pull/<n>/head:refs/recap/fork-head`) — fetching commits is safe | Install packages from the fork, run fork scripts, or evaluate fork content as code |
| Run `git diff base...refs/recap/fork-head` — pure text diff of two already-fetched objects                             | Use the diff as anything other than text input to the LLM                          |
| Run the **base repo's** visual-recap skill and agent configuration                                                     | Load any skill or config from the fork                                             |
| Pass the diff through the same secret-scan step (fail-closed) as first-party PRs                                       | Skip the secret scan                                                               |
| Add an explicit prompt-hardening note to the agent prompt marking diff content as untrusted                            | Grant the agent any additional permissions beyond the normal recap agent           |

### Why you must review the diff before labeling

The fork diff is attacker-controlled text that the recap agent reads as input. A carefully crafted diff could contain prompt-injection content — for example, diff lines that look like agent instructions — intended to make the recap agent take unintended actions (e.g., exfiltrate the publish token or produce misleading recap content).

Before applying the `recap` label, skim the diff for:

- Lines that read like direct commands or role instructions ("Ignore previous instructions...", "You are now...", "Write the token to...").
- Unusual file names that could be misread as system prompts.
- Encoded content in added files that might decode to instructions.

These mitigations are already layered in the workflow (secret scan, sensitive-path gate, prompt-hardening note, restricted agent tool allowlist), but label review is the primary line of defense.

### Relationship to the main workflow

The two workflow files are independent. For non-fork PR updates, `pr-visual-recap.yml` is the only workflow that runs. For fork PRs, the normal workflow exits at its fork gate, and `pr-visual-recap-fork.yml` runs automatically for trusted same-org authors or after a fresh maintainer `recap` label for outside contributors. They share the same sticky comment marker and plan-id threading, so both PRs and fork PRs produce a single upserted comment on the same PR.

### Self-modifying guard {#self-modifying-guard}

The `gate` step skips the recap entirely when a PR touches any of the following paths, so a PR can never rewrite the workflow, skill, or agent config that the trusted recap job loads and exfiltrate secrets:

| Path pattern                               | Reason                                   |
| ------------------------------------------ | ---------------------------------------- |
| `.github/workflows/pr-visual-recap.yml`    | The workflow itself                      |
| `**/skills/visual-(recap\|plan\|plans)/**` | The visual-recap skill the agent follows |
| `**/.claude/**`                            | Agent settings the runner loads          |
| `**/CLAUDE.md`                             | Agent instructions the runner loads      |
| `**/AGENTS.md`                             | Agent instructions the runner loads      |
| `**/.mcp.json`                             | MCP server config the runner loads       |

In the `BuilderIO/agent-native` monorepo, the workflow runs the recap CLI from trusted base-branch source instead of PR-head source. That keeps normal package changes, including `packages/core/**`, eligible for recaps without executing PR-modified CLI code.

## Local-files privacy mode

The GitHub Action is designed for hosted, shareable PR review. If you want a
recap without sending recap content to the Agent-Native Plan database, run the
same helper flow locally in local-files mode instead:

```bash
npx @agent-native/recap-cli@latest recap collect-diff --base main --head HEAD --out recap.diff --stat recap.stat
npx @agent-native/recap-cli@latest recap scan --diff recap.diff
npx @agent-native/recap-cli@latest recap build-prompt --pr 123 --diff recap.diff --stat recap.stat --local-files --local-dir plans/pr-123-visual-recap
```

Give the generated `recap-prompt.md` to your coding agent. In local-files mode
the prompt instructs the agent to write `plans/pr-123-visual-recap/plan.mdx`
plus optional visual files and then run:

```bash
npx @agent-native/core@latest plan local serve --dir plans/pr-123-visual-recap --kind recap --open
```

The returned URL opens the hosted Plan UI while the browser reads the recap MDX
from a localhost bridge. Recap content is not written to the hosted Plan
database, and the URL only works on the machine running the bridge. If you run
the Plan app locally with the same `PLAN_LOCAL_DIR`, the
`/local-plans/pr-123-visual-recap` route is also valid. Repo-backed folders can
open as `/local-plans/pr-123-visual-recap?path=plans%2Fpr-123-visual-recap`.
This mode disables the hosted sticky PR comment, inline screenshot upload,
usage attachment, and browser comments until you explicitly publish.

## It's informational, not a gate

The recap is a review aid layered on top of the normal PR flow:

- It shows a `Visual Recap` check row for visibility, but it is **never a required check** and never blocks merging.
- A generation or publish failure completes neutrally and surfaces as an explanatory sticky comment, not a red X on unrelated code.
- The recap and its screenshot **do not imply the diff has been reviewed**. Reviewers still need to read the actual changed lines.

## Version pinning (copy variant) {#version-pinning-copy-variant}

By default the copy-variant workflow installs `@agent-native/recap-cli@latest` at run time so every recap run automatically picks up the newest CLI. If your CI needs reproducible tooling, set the **`RECAP_CLI_VERSION`** repository variable to pin the installed version:

1. Go to your repo's **Settings → Secrets and variables → Actions → Variables**.
2. Create a variable named `RECAP_CLI_VERSION` with a value like `1.5.0`.

The variable is optional. Leave it unset (or set it to `latest`) to track the newest release.

For the reusable-caller variant, use the `cli-version` input instead (see [Version pinning](#version-pinning) in the reusable section).

## Secret-scan allowlist

Before publishing a recap the workflow runs `npx @agent-native/recap-cli@latest recap scan` to detect likely secrets in the diff. Any PR whose diff matches a known-secret pattern is blocked with an explanatory comment — the recap is not published, and no diff content is sent to the coding agent.

In rare cases a repo has intentional test fixtures or non-secret strings that superficially resemble secret patterns (e.g., a fixture key in a test file). To suppress a false positive, create `.github/recap-scan-allowlist` in the root of your repository.

### Format

Each non-blank, non-comment line is either a **literal substring** or a **`/regex/flags`** pattern:

```
# Lines starting with # are comments.

# Literal substring — any diff line containing this string is allowed.
sk-test-fixture1234567890abcdef

# Regex pattern — written as /pattern/flags (JS syntax).
/^.STRIPE_KEY=sk-test-/i

# Another literal.
EXAMPLE_API_KEY=placeholder-value
```

Rules:

- A line is **suppressed** (allowed) when it contains the literal, or when the full line matches the regex.
- The file is **fail-closed**: if it is absent, no suppressions apply — the scanner behaves as before.
- An empty file is equivalent to no file.
- Malformed regex lines are treated as literal strings.

The allowlist is only consulted by the secret-scan gate. It does not affect what the coding agent can read — if the gate passes, the agent receives the full diff regardless.

## Adopt as a reusable workflow

### Why use the reusable variant?

The default installer copies the full ~360-line workflow YAML into your repo (the **copy** option). This is the right choice for air-gapped repos or repos that need to audit every line of what runs. The downside is that bug fixes and improvements never reach you — you need to re-run `npx @agent-native/recap-cli@latest recap setup` manually after each release.

The **reusable** option writes a thin ~20-line caller instead. It delegates to `BuilderIO/agent-native/.github/workflows/pr-visual-recap-reusable.yml` via `uses:`. Every caller automatically picks up the latest logic when the workflow runs, with no local update needed.

|                                | Copy (default)            | Reusable                       |
| ------------------------------ | ------------------------- | ------------------------------ |
| Workflow size in your repo     | ~360 lines                | ~20 lines                      |
| Picks up fixes automatically   | No — re-run `recap setup` | Yes                            |
| Air-gap / full auditability    | Yes                       | No                             |
| Pinnable to a specific version | Only by editing locally   | Yes — set `@v1.2.3` in `uses:` |

### Caller snippet

This is what `npx @agent-native/recap-cli@latest recap setup --reusable` writes (or you can paste it manually):

```yaml
name: PR Visual Recap

# Thin caller — the full workflow logic lives in BuilderIO/agent-native.
# Fixes and improvements reach this repo automatically on each run.
# To pin a specific version for reproducibility replace '@main' with a
# tag or SHA, e.g. '@v1.2.3' or '@abc1234'.

on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review, labeled, closed]

jobs:
  visual-recap:
    if: github.event.action != 'labeled' || vars.VISUAL_RECAP_REQUIRED_LABELS != ''
    permissions:
      actions: write
      contents: read
      checks: write
      issues: write
      pull-requests: write
    uses: BuilderIO/agent-native/.github/workflows/pr-visual-recap-reusable.yml@main
    secrets:
      PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
      VISUAL_RECAP_API_KEY: ${{ secrets.VISUAL_RECAP_API_KEY }}
      PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL }}
    with:
      agent: ${{ vars.VISUAL_RECAP_AGENT || 'claude' }}
      model: ${{ vars.VISUAL_RECAP_MODEL || '' }}
      base-url: ${{ vars.VISUAL_RECAP_BASE_URL || '' }}
      reasoning: ${{ vars.VISUAL_RECAP_REASONING || '' }}
      skill-source: ${{ vars.VISUAL_RECAP_SKILL_SOURCE || 'auto' }}
      runs-on: ${{ vars.VISUAL_RECAP_RUNS_ON || '"ubuntu-latest"' }}
      gate-runs-on: ${{ vars.VISUAL_RECAP_GATE_RUNS_ON || 'ubuntu-latest' }}
      required-labels: ${{ vars.VISUAL_RECAP_REQUIRED_LABELS || '' }}
      # cli-version: "latest"  # pin to a specific @agent-native/recap-cli version
```

The same secrets and variables described in [Secrets and variables](#secrets-and-variables) apply — set them in your repo settings the same way as for the copy variant. The reusable caller forwards `VISUAL_RECAP_RUNS_ON` through its JSON `runs-on` input for the full recap job, `VISUAL_RECAP_GATE_RUNS_ON` through its plain-label `gate-runs-on` input for the metadata-only gate, and `VISUAL_RECAP_REQUIRED_LABELS` through `required-labels` for optional PR-label gating. The gate runner input is used only for trusted same-repository authors; other PRs use GitHub-hosted `ubuntu-latest`. Omitting the runner inputs preserves their `ubuntu-latest` defaults; omitting required labels preserves automatic recap behavior.

### Installing via the CLI

```bash
# Write the thin caller instead of the full copy:
npx @agent-native/recap-cli@latest recap setup --reusable

# Or with a pinned ref for reproducibility:
npx @agent-native/recap-cli@latest recap setup --reusable --ref v1.2.3
```

Both variants write the workflow to `.github/workflows/pr-visual-recap.yml`. If an existing workflow is already there and differs, the command refuses and tells you to pass `--force` to overwrite.

After writing, run `npx @agent-native/recap-cli@latest recap doctor` as usual to confirm secrets are configured.

### Version pinning

By default the caller references `@main`, which always uses the latest published version of the reusable workflow. For production repos that need reproducible CI, pin to a tag or SHA:

```yaml
uses: BuilderIO/agent-native/.github/workflows/pr-visual-recap-reusable.yml@v1.2.3
```

The `cli-version` input controls which `@agent-native/recap-cli` version runs inside the workflow — leave it at `"latest"` to track the newest release, or pin it to a version string (e.g. `"1.5.0"`) for full reproducibility.

### workflow_call event context

`workflow_call` workflows inherit the **caller's** event context. The reusable workflow uses `github.event.pull_request.*` expressions to read the PR number, head SHA, base SHA, merge timestamp, and PR metadata — these work correctly only when the caller triggers on `pull_request`. The caller snippet above already includes the correct event types. The `closed` event is included so merged PR recaps can be stamped with `merged_at` and later searched as shipped work.

Do not trigger the caller on `workflow_dispatch` or `push` — those events do not carry a `pull_request` payload, and the gate will skip the recap with "no pull_request payload".

## What's next

- [Visual Plans](/docs/template-plan) — the `/visual-plan` and `/visual-recap` skills, the hosted Plans connector, and the interactive review surface this action publishes to.
- [Skills](/docs/skills-guide) — installing agent-native skills into your coding agent.
