Capture, annotate, and include screenshots in pull requests for UI changes. Use when creating or updating PRs that touch frontend components, pages, or any web-facing surface. Also use when asked to add before/after screenshots, visual diffs, or enrich PR descriptions. Triggers on: PR screenshots, before/after, visual diff, PR description, capture screenshot, PR images, enrich PR.
From sharednpx claudepluginhub inkeep/team-skills --plugin sharedThis skill uses the workspace's default tool permissions.
references/pr-templates.mdscripts/annotate.tsscripts/capture.tsscripts/validate-sensitive.tsGuides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Optimizes cloud costs on AWS, Azure, GCP via rightsizing, tagging strategies, reserved instances, spot usage, and spending analysis. Use for expense reduction and governance.
Capture, redact, annotate, and embed screenshots in GitHub PRs for UI changes.
These scripts require the following npm packages. Install them as dev dependencies in your project:
| Package | Purpose | Install |
|---|---|---|
playwright | Browser automation for screenshot capture | npm add -D playwright |
sharp | Image annotation (labels, borders, stitching) | npm add -D sharp |
tsx | TypeScript runner for scripts | npm add -D tsx |
After installing Playwright, download browser binaries: npx playwright install chromium
Before starting any work, create a task for each step using TaskCreate with addBlockedBy to enforce ordering. Derive descriptions and completion criteria from each step's own workflow text.
Mark each task in_progress when starting and completed when its step's exit criteria are met. On re-entry, check TaskList first and resume from the first non-completed task.
Most screenshots require understanding the target page before capture — what state it's in, what popups appear, what content needs to be visible. The default workflow is explore → capture → verify → iterate.
scripts/capture.ts with --pre-scriptscripts/validate-sensitive.tsscripts/annotate.ts (labels, borders, side-by-side)Simple captures (no interaction needed): For static pages where goto + wait is sufficient, skip step 3 and omit --pre-script. Steps 2 (explore) and 5 (verify) still apply — always understand what you're capturing and confirm you got it right.
Analyze the PR diff to determine which UI routes are impacted. Map changed component/page files to their corresponding URLs. If the diff only touches backend code, tests, or non-visual files, skip screenshot capture.
Before writing any pre-scripts or capture commands, visit each target page to understand what you're capturing. Load /browser skill and use its Visual Inspection pattern — navigate to each route, take a temporary screenshot, and read it to see what the page looks like.
For each page, note:
Based on exploration, decide:
Do not proceed to pre-script writing until you understand each page's behavior. Exploration often reveals interaction needs that aren't obvious from the diff alone (popups that appear on first visit, content behind tabs, lazy loading delays).
Load /browser skill for writing pre-scripts. A pre-script is a JS file that receives the Playwright page object and runs interaction before masking + screenshot. Use your findings from Step 2 to write targeted pre-scripts.
The file must export an async function that receives { page, url, route }:
// /tmp/pw-pre-dashboard.js
module.exports = async function({ page, url, route }) {
// Dismiss cookie banner
await page.click('button:has-text("Accept")').catch(() => {});
// Click the "Analytics" tab
await page.click('[data-tab="analytics"]');
await page.waitForTimeout(500);
};
Dismiss popups / modals:
module.exports = async function({ page }) {
// Cookie banner
await page.click('button:has-text("Accept all")').catch(() => {});
// Marketing popup
await page.click('[data-testid="close-modal"]').catch(() => {});
};
Navigate through a login flow:
module.exports = async function({ page }) {
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
};
Scroll to a specific section:
module.exports = async function({ page }) {
await page.locator('#pricing-section').scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
};
Expand collapsed content:
module.exports = async function({ page }) {
await page.click('button:has-text("Show more")');
await page.waitForSelector('.expanded-content', { state: 'visible' });
};
One pre-script per route — if routes need different interaction, write separate scripts and run capture once per route. If all routes share the same interaction (e.g., dismiss the same cookie banner), one script covers all.
| Environment | Base URL | Notes |
|---|---|---|
| Local dev | http://localhost:3000 (or your dev server port) | Start your dev server first |
| Preview deployment | Your preview URL (e.g., Vercel, Netlify, etc.) | Available after PR push |
| Playwright server | Connect via --connect ws://localhost:3001 | See "Reusable server" below |
# With pre-script (default for most captures)
npx tsx scripts/capture.ts \
--base-url http://localhost:3000 \
--routes "/dashboard,/settings" \
--pre-script /tmp/pw-pre-dashboard.js \
--output-dir tmp/screengrabs
# Simple capture (no interaction needed)
npx tsx scripts/capture.ts \
--base-url http://localhost:3000 \
--routes "/landing,/about" \
--output-dir tmp/screengrabs
# Preview deployment with pre-script
npx tsx scripts/capture.ts \
--base-url https://your-preview-url.example.com \
--routes "/dashboard" \
--pre-script /tmp/pw-pre-dismiss-popups.js \
--output-dir tmp/screengrabs
| Option | Default | Description |
|---|---|---|
--base-url <url> | required | Target URL (local dev or preview) |
--routes <paths> | required | Comma-separated route paths |
--pre-script <path> | — | JS file to run on page before capture (for interaction) |
--output-dir <dir> | tmp/screengrabs | Where to save PNGs and DOM text |
--viewport <WxH> | 1280x800 | Browser viewport size |
--connect <ws-url> | — | Connect to existing Playwright server |
--mask-selectors <s> | — | Additional CSS selectors to blur |
--wait <ms> | 2000 | Wait after page load before capture |
--full-page | false | Capture full scrollable page |
--auth-cookie <value> | — | Session cookie for authenticated pages |
Start a server once, reuse across multiple captures:
# Terminal 1: start server
npx tsx scripts/capture.ts --serve --port 3001
# Terminal 2+: connect and capture
npx tsx scripts/capture.ts \
--connect ws://localhost:3001 --base-url http://localhost:3000 \
--routes "/..." --pre-script /tmp/pw-pre-script.js --output-dir tmp/screengrabs
Do not skip this step. After capturing, look at each screenshot to confirm it captured what you intended.
For each captured image, read the PNG file and check:
Use the Read tool to view each captured PNG — it renders images visually. Compare what you see against what you observed during exploration (Step 2).
# Read the captured image to verify
Read tool → tmp/screengrabs/<route-name>.png
If all captures pass verification, proceed to Step 7 (validate sensitive data). If any capture is wrong, go to Step 6.
When a capture doesn't match expectations, diagnose and re-capture. Do not upload incorrect screenshots.
| Problem | Likely cause | Fix |
|---|---|---|
| Spinner or skeleton visible | Insufficient wait time | Increase --wait (e.g., --wait 5000) or add waitForSelector in pre-script |
| Cookie banner or modal blocking content | Pre-script didn't dismiss it | Add dismiss logic to pre-script (.catch(() => {}) for optional popups) |
| Wrong tab or section visible | Pre-script didn't navigate to correct state | Update pre-script to click the right tab/accordion/section |
| Login wall or auth error | Missing auth cookie or expired session | Use --auth-cookie or add login flow to pre-script |
| Content cut off or wrong scroll position | Default viewport insufficient | Adjust --viewport, add scrollIntoViewIfNeeded() in pre-script, or use --full-page |
| Partially loaded images or assets | Network still loading | Add waitForLoadState('networkidle') in pre-script after interaction |
scripts/capture.ts for the affected routes onlyAlways run before uploading to GitHub.
npx tsx scripts/validate-sensitive.ts \
--dir ./screengrabs
The script checks .dom-text.txt files (saved by capture) for:
sk-, sk-ant-, AKIA, sk_live_)Exit code 1 = sensitive data found. Re-capture with additional --mask-selectors or fix the source before proceeding.
The capture script automatically masks these before taking screenshots:
| Selector / Pattern | What it catches |
|---|---|
input[type="password"] | Password fields |
Text matching sk-, Bearer, eyJ, ghp_, PEM headers | In-page tokens/keys |
Add more with --mask-selectors "selector1,selector2".
# Add "Before" label with red border
npx tsx scripts/annotate.ts \
--input before.png --label "Before" --border "#ef4444" --output before-labeled.png
# Add "After" label with green border
npx tsx scripts/annotate.ts \
--input after.png --label "After" --border "#22c55e" --output after-labeled.png
# Side-by-side comparison
npx tsx scripts/annotate.ts \
--stitch before.png after.png --labels "Before,After" --output comparison.png
Images in PR markdown need permanent URLs.
Primary: Bunny Edge Storage via /media-upload skill (programmatic, permanent CDN URLs):
Load the /media-upload skill, then use uploadToBunnyStorage():
const result = await uploadToBunnyStorage(
'./tmp/screengrabs/dashboard-labeled.png',
`pr-${prNumber}/dashboard-before.png`
);
// result.url → "https://{cdn-hostname}/pr-123/dashboard-before.png" (permanent)
Requires BUNNY_STORAGE_API_KEY, BUNNY_STORAGE_ZONE_NAME, BUNNY_STORAGE_HOSTNAME env vars. Setup: ./secrets/setup.sh --skill media-upload.
Fallback: GitHub drag-and-drop — drag images into the PR description editor on GitHub. GitHub generates permanent CDN URLs automatically.
gh pr edit {pr-number} --body "$(cat pr-body.md)"
Use the templates in references/pr-templates.md for consistent formatting. Include: