What you will build
A reusable Claude Code skill for a job you repeat. The worked example is the check this site runs before any UI change counts as done: capture pages at 390 and 1440 pixels wide, in light and dark mode, and report page errors, console errors, bad HTTP status and sideways scrolling. You will write a SKILL.md with a description that makes Claude pick it up at the right moment, clear steps and checks, a report format, and a bundled script that does the capturing. At the end, /page-captures runs the whole routine, and Claude also reaches for it when you ask whether a page looks right.
Before you start
- Claude Code installed and working in your project.
- A web project with a local dev server. Any framework works.
- Playwright in the project:
npm i -D playwright, thennpx playwright install chromiumto download a browser. - A routine you have typed into chat at least twice. Skills pay off on repeats.
- About 45 minutes.
How it works
A skill is a folder with a SKILL.md file. The top of the file is YAML frontmatter between --- lines; the rest is Markdown instructions. Per the Claude Code docs, the skill's description stays in Claude's context so it knows what exists, and the full instructions load only when the skill is used. That is the difference from CLAUDE.md, which loads every session. The docs put it simply: create a skill when you keep pasting the same instructions, checklist or multi-step procedure into chat.
Skills can bundle files. Scripts in the folder are run, not read into context, so a long script costs almost nothing. The ${CLAUDE_SKILL_DIR} variable points at the skill's folder, so the instructions can run the script wherever the skill is installed.
- Decide what the skill does and when it should trigger.
- Create
.claude/skills/page-captures/in the project. - Write the frontmatter: name, description, argument hint and pre-approved tools.
- Write the body: steps, checks and a report format.
- Add the capture script to
scripts/. - Test it: direct call, natural request, fresh session.
- Measure it against no skill, tune the description, then commit it.
Step 1: Decide the job and its trigger
Write two sentences before any file. What does the skill do, and when should Claude use it? For this example: "Capture pages at 390 and 1440 in light and dark and report errors. Use before saying a UI change is done, or when asked to screenshot, check or QA pages."
Then decide who may run it. The docs describe two switches. disable-model-invocation: true means only you can run it, which suits anything with side effects, such as deploying or sending a message. user-invocable: false means only Claude can use it, for background knowledge. Page captures are read-only and useful for Claude to reach for on its own, so this skill keeps the default: both.
Check
- The job fits in two sentences.
- You know whether the skill has side effects. If it does, plan for
disable-model-invocation: true.
Step 2: Create the folder
Project skills live in .claude/skills/<name>/SKILL.md and are shared through Git. Personal skills live in ~/.claude/skills/<name>/SKILL.md and follow you to every project on that machine. The folder name becomes the command.
mkdir -p .claude/skills/page-captures/scriptsUse a project skill here. The script imports Playwright, and Node finds packages by walking up from the script's folder, so a script inside the project finds the project's node_modules. The same script in your home folder would not.
Check
- The path is
.claude/skills/page-captures/SKILL.mdonce you save the file. - The folder name is not
synced, which the docs reserve.
Step 3: Write the frontmatter
---
name: page-captures
description: Capture pages at 390 and 1440 wide in light and dark mode with Playwright, and report page errors, console errors, bad HTTP status and horizontal scroll. Use before saying a UI change is done, or when asked to screenshot, check or QA pages.
argument-hint: [base-url] [path ...]
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/capture.mjs *)
---What each field does, from the docs' frontmatter reference:
name: the display name. It defaults to the folder name.description: what the skill does and when to use it. Claude uses it to decide when to load the skill. Put the main use first: the description andwhen_to_usetogether are cut at 1,536 characters in the listing.argument-hint: shown in autocomplete so you remember what to pass.allowed-tools: tools Claude may use without asking during the turn that runs the skill. The docs show exactly this pattern: the same${CLAUDE_SKILL_DIR}in the rule and in the body, so the bundled script runs without a prompt. The grant ends when you send your next message.
The frontmatter only counts if the opening --- is the very first line. Field names must match exactly; unknown fields are ignored without an error.
Check
- The description contains the words people actually say: "screenshot", "check", "QA", "done".
- The
allowed-toolsrule names the script, not all of Bash.
Step 4: Write the body: steps, checks, report
Keep it short. The docs note that once a skill loads, its text stays in the conversation, so every line is a recurring cost. State what to do, not why.
# Page captures
Capture evidence for UI changes. Do not say a page works without it.
## Steps
1. Base URL: the first argument, or http://127.0.0.1:3000 if none. If the server does not respond, say so and stop.
2. Run: node ${CLAUDE_SKILL_DIR}/scripts/capture.mjs <base-url> captures/<yyyymmdd-hhmm> <paths>
Arguments given: $ARGUMENTS
3. Open the 390 dark and 1440 light screenshot of every path and look at them.
4. Report in the format below.
## Checks
- Every path has four screenshots: 390 and 1440, light and dark.
- No page errors, console errors, 4xx or 5xx status, or horizontal scroll.
- In the screenshots: no clipped text, no invisible text in dark mode, nothing overlapping.
## Report
- The command you ran and its last line of output.
- One line per problem: path, theme, width, what is wrong.
- What you looked at in the screenshots and what you saw.
- What you could not check, and why.$ARGUMENTS is replaced with whatever follows the command, so /page-captures http://127.0.0.1:4310 / /library passes the base URL and two paths. The report section matters most. It forces evidence into the answer instead of "looks good".
Check
- The body fits on one screen.
- Every check is something a person could verify by opening a file.
Step 5: Add the capture script
Save this as .claude/skills/page-captures/scripts/capture.mjs. It is built from the capture scripts used on this site, cut down to the shared core.
import { chromium } from "playwright";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const [base, outDir, ...paths] = process.argv.slice(2);
if (!base || !outDir || !paths.length) { console.error("usage: capture.mjs <baseUrl> <outDir> <path> [path...]"); process.exit(2); }
mkdirSync(outDir, { recursive: true });
const browser = await chromium.launch();
const results = [];
for (const theme of ["light", "dark"]) for (const [w, h] of [[390, 844], [1440, 900]]) {
const page = await browser.newPage({ viewport: { width: w, height: h }, colorScheme: theme });
const errors = [];
page.on("pageerror", (e) => errors.push("pageerror " + e.message.slice(0, 160)));
page.on("console", (m) => m.type() === "error" && errors.push("console " + m.text().slice(0, 160)));
for (const path of paths) {
const res = await page.goto(base + path, { waitUntil: "load" });
await page.waitForTimeout(800);
const slug = path === "/" ? "home" : path.replace(/^\/|\/$/g, "").replace(/\W+/g, "-");
const file = join(outDir, `${slug}-${theme}-${w}.png`);
await page.screenshot({ path: file, fullPage: w === 1440 });
const overflow = await page.evaluate(() => document.documentElement.scrollWidth > innerWidth);
results.push({ path, theme, width: w, status: res ? res.status() : 0, overflow, file, errors: errors.splice(0) });
}
await page.close();
}
await browser.close();
writeFileSync(join(outDir, "summary.json"), JSON.stringify(results, null, 2));
const bad = results.filter((r) => r.status >= 400 || r.overflow || r.errors.length);
console.log(`captured ${results.length} screenshots, ${bad.length} with problems`);
for (const r of bad) console.log(`- ${r.path} ${r.theme} ${r.width}: status ${r.status}${r.overflow ? ", horizontal scroll" : ""}; ${r.errors.join(" | ")}`);
process.exit(bad.length ? 1 : 0);colorScheme makes the page see prefers-color-scheme: dark or light, and viewport sets the size. The script exits with 1 when anything is wrong, so a failure is impossible to miss.
We ran it against a small test site with three paths: a clean page, a page with a deliberate script error, and a missing page. It printed captured 12 screenshots, 8 with problems, listed the page error and console error on the broken page and the 404 on the missing one at every size and theme, and exited with 1. The clean page produced no lines.
Check
- Run it by hand once:
node .claude/skills/page-captures/scripts/capture.mjs http://127.0.0.1:3000 captures/test /. - A broken page makes it exit with 1 and name the error.
Step 6: Test the skill three ways
- Direct: type
/page-captures http://127.0.0.1:3000 /and confirm it runs the script without a permission prompt. - Natural: ask "does the home page look right on a phone in dark mode?" and see whether Claude loads the skill on its own.
- Fresh: open a new session and repeat. The docs stress a fresh session, because context from writing the skill hides gaps in the instructions.
If the skill does not appear, ask What skills are available?. If it appears but never triggers, the description is missing the words you used. If the frontmatter is broken, the docs say the skill still loads with empty metadata, so /page-captures works but Claude cannot match it. Run claude --debug to see the parse error, or on recent versions claude plugin validate .claude/skills.
Check
- All three tests end with a report in your format, with real command output.
- The screenshots exist in
captures/.
Step 7: Measure it, tune it, share it
Seeing a skill trigger only proves Claude found it. To know it helps, the docs suggest a baseline: run a few realistic prompts in fresh sessions with the skill and without it, and compare. The skill-creator plugin automates this inside Claude Code (/plugin install skill-creator@claude-plugins-official): it stores test prompts, runs each in a clean subagent, grades the output and compares versions.
When it works, commit .claude/skills/page-captures/ so everyone on the project gets it, and add captures/ to .gitignore.
Check
- With the skill, answers include screenshots and error lines; without it, they are claims.
- Teammates see
/page-capturesafter pulling.
Gotchas we hit
- Our first capture scripts only ran on one Mac. They imported Playwright from a global install path. A skill should import
playwrightfrom the project so it runs anywhere the repo does. - Cancelled requests are not errors. On the live site, some "failed" requests were Next.js prefetches cancelled by navigation. The script listens for page errors and console errors instead of every failed request.
- One missing page, two lines. In our test, a 404 page reported both the status and a console error ("Failed to load resource"). Count problems per page, not per line, when you read the report.
Take it further
- Add
pathsfrontmatter so the skill loads automatically only when Claude works on files such assrc/**/*.tsx. - Inject live data with the
!command syntax. The docs warn that a failing or unapproved injected command aborts the whole invocation. - Read AI-assisted visual QA for timed animation captures, reduced motion checks and the review prompt.
- Read Set up a project for Claude Code for permissions and hooks around your skills.
- Read Dainer design skills to see a public skill pack in the same format.
Quick checklist
- The job and its trigger fit in two sentences.
- The skill lives in
.claude/skills/<name>/SKILL.md, with---on line one. - The description leads with the use case and uses words people say.
- Side-effect skills set
disable-model-invocation: true. allowed-toolspre-approves only the bundled script.- The body has steps, checks and a report format, and fits on one screen.
- The script exits non-zero on problems and writes a summary file.
- Tested by command, by natural request and in a fresh session.
- Committed with the project, with output folders ignored.