Audits a Figma design page read-only — frame membership and bounds, section overlap, naming, arrow geometry and entry direction and pass-through, coverage orphans, [state] dashed links, variants stacked on one another inside a component set, and component default residue carried over by duplication. Reports violations and writes nothing. Rules come from figma-conventions.yaml and the audit code lives in one place under ${CLAUDE_PLUGIN_ROOT}/_common/scripts. It is the single gate that absorbs ...
Scanned 9/2/2026
Install to Claude Code
npx -y skills add byjunyoung/claude-product-skills --skill lint --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Lint?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/byjunyoung-lint)More formats (shields.io, HTML) on the badges page.
---
name: lint
description: Audits a Figma design page read-only — frame membership and bounds, section overlap, naming, arrow geometry and entry direction and pass-through, coverage orphans, [state] dashed links, variants stacked on one another inside a component set, and component default residue carried over by duplication. Reports violations and writes nothing. Rules come from figma-conventions.yaml and the audit code lives in one place under ${CLAUDE_PLUGIN_ROOT}/_common/scripts. It is the single gate that absorbs the checks fig:prep and fig:arrows would otherwise each carry, and it is called again right after those skills write. Triggers - "/fig:lint", "lint this page", "audit the design", "검증해줘", "규칙 위반 검사해줘", "피그마 검수", "화면 복제 후 검수".
allowed-tools: AskUserQuestion, Bash, mcp__plugin_figma_figma__use_figma, mcp__plugin_figma_figma__get_metadata, mcp__plugin_figma_figma__get_screenshot, mcp__claude_ai_Notion__notion-fetch
---
# fig:lint — read-only structure and flow audit (violations only)
**Part of a plugin.** The scripts this skill runs ship beside it under `${CLAUDE_PLUGIN_ROOT}`. If that path does not resolve, this file was installed on its own — stop and say the plugin itself is needed (`claude plugin install fig@byjunyoung`), rather than improvising what the scripts do.
Takes one design page and **audits structure (placement), flow (arrows), and component usage in a single pass**, reporting only violations. Detection is separated from repair, the way a design lint should be — this skill **only detects**; fig:prep fixes structure and fig:arrows fixes flow.
**Why it exists**: to keep verification logic in **one place behind one gate**. Whatever touched the canvas — prep, arrows, a duplicated form, a manual edit — running this one skill at the end covers all of it. When verification is scattered across the working skills you get a blind spot shaped like "I wasn't using that skill this time, so the audit never ran." Removing that blind spot is the reason it is separate.
**Prerequisites**: **zero writes.** `use_figma` is used only for **read-only scripts** that inspect node properties — `return` a report, never mutate a node. Load `figma:figma-use` before calling `use_figma`.
## When to invoke
- "lint this", "audit the design", "check for rule violations" — on its own
- **As the mandatory final gate for fig:prep and fig:arrows** — always confirm through this right after creating, duplicating, moving frames, or generating and syncing arrows
- **Always when clone or move was involved** — the main cause of page-level orphans and out-of-bounds frames, and an isolated screenshot will not catch either
- **Always right after building a screen by duplicating another** — component default residue carried over by the copy is caught by this audit alone; the eye keeps missing it
## When NOT to invoke
- Actually fixing, tidying, or stubbing placeholders → `/fig:prep`
- Creating or syncing arrows → `/fig:arrows`
- Auditing token (variable) bindings → `/fig:tokens`
- Checking and applying work into the canonical page → `/fig:sync`
- Just understanding the structure → `/fig:read`
## Inputs
- `figma_url` (required): the page to audit. For several pages, split the calls per page and run them in parallel
- `scope` (optional): structure only / flow only / components only. Omitted, all three run
- `reference_page` (optional): an existing production page to derive component usage conventions from. Omitted, the page in the same file that uses the most of the same components is chosen
## Where the rules come from — the config file (same source as fig:prep and fig:sync)
Naming patterns, the state list, excluded sections, tolerances, and statistical thresholds all come from **`figma-conventions.yaml`**, which is the single source. No values are written into this document.
```
python3 ${CLAUDE_PLUGIN_ROOT}/_common/scripts/lib/resolve-config.py --js <fileKey>
```
The layers merge bottom-up: bundled defaults → `~/.claude/figma-conventions.yaml` → `./figma-conventions.yaml`. If it ran on bundled defaults alone, **say so in the report**.
- **A `null` pattern means that check is skipped.** This is what keeps a file with no convention from having every frame reported — "not knowing the rule" and "breaking the rule" are different things
- On an unfamiliar file with no config at all, run `/fig:setup` first to infer the conventions and draft one
- If the team keeps a written guide, point `guide_source` at it. **It is not fetched on every run** — it is an input absorbed once to help fill the config
**Checked regardless of config**: frame membership, out-of-bounds frames, section overlap, variant stacking, library sibling overlap, arrow geometry, pass-through, orphans. Coordinates and parentage are geometry, not convention, so they hold true with or without a written rule.
## What it checks
### A. Structure (placement) — fig:prep's territory
| Item | Basis | Frequency |
|---|---|---|
| **Frame membership** | A screen frame's `parent.type === "SECTION"` on strict pages. **Sitting directly on the page means the absorb step was missed** — `clone()` and `createFrame` default their parent to currentPage, so anything not absorbed leaks out here | ★ high |
| **Out of bounds** | A frame outside its own section's bbox. Usually an absolute coordinate written where a section-relative one belongs — a frame that became page-level and shot off to a stray absolute position | ★ high |
| **Frame overlap** | Screen frames within one section whose bboxes intersect (excluding the deliberate adjacency of a variant stack) | medium |
| Section overlap | Section rectangles intersecting horizontally and vertically at once — an unambiguous violation. Usually a section stretched to fit a placeholder that then invades the row beside or below | medium |
| Naming violation | Mismatch against `naming.frame_pattern` / `naming.section_pattern` (skipped when `null`) | low |
| Ordering mismatch | A section's `NN.` number disagreeing with its position on the canvas (row-major) | low |
| Missing required state ✋ | Missing states per `[screen]` group against `naming.required_states` — **unless that state is managed on a common page and the screen's Default carries a reference annotation to it** (see fig:prep, "repeated common elements") | low |
| Split state variants | Variants of one `[screen]` broken apart in a column by an unrelated frame wedged between parent and variant, so the `[state]` dashed line passes through it | medium |
| **Variant stacking** | Variants of one component set whose bboxes intersect. A set with no auto-layout drops a new variant on the last one's coordinates, so several states render as a single component — nothing about it looks broken, the top variant draws fine and the rest are simply not visible | ★ high |
| Library sibling overlap | A set or component overlapping the one placed beside it in the same section. Frame overlap above looks at frames only, so a component page passes that check while its components bury each other — a set that gained a variant or a layout runs past the gap it was placed with | medium |
### B. Flow (arrows) — fig:arrows's territory
| Item | Basis |
|---|---|
| Transition arrow geometry | Start point within `arrows.audit.edge_tolerance` of the source edge / arrowhead within `arrows.audit.gap_range` of the target edge / **the final segment perpendicular to the target edge, so the head enters head-on** / no segment passing through an unrelated frame / both source and target frames exist (orphan) |
| Labels | The pill above its own arrow in z-order / clearing the arrowhead by more than `arrows.audit.label_clear` / not covering another arrow's segment |
| Coverage orphans | Every screen frame appears at least once in a `-->` or a `[state]` (zero orphans) |
| `[state]` dashed lines | Two vertices, vertical, no elbows / name order top to bottom / endpoints within `arrows.audit.edge_tolerance` of the source's bottom edge and the target's top edge / no other frame between them |
### C. Component usage (default residue) — fig:prep's territory
An instance that was newly placed or duplicated **carries the component's defaults with it.** A display toggle that is on in the library — an icon slot, a supplementary area, a caption — stays on even where that screen does not use it, and renders an empty slot with nothing in it. At zoomed-out scale it is barely visible, so eye inspection keeps letting it through. **This is caught by measurement alone.**
| Item | Basis | Frequency |
|---|---|---|
| **Boolean default deviation** | An instance's boolean property on the working page differing from **the convention that same component follows on existing production screens in the same file** | ★ high |
| Empty display toggle | Same axis — a slot, caption, or supplementary area switched on with nothing in it | medium |
The basis is not a written rule but **how the same file actually uses the component** — the same principle as preferring existing assets. A property whose value also varies across production screens is a per-use choice, not a violation, and is not reported; only properties overwhelmingly settled one way count as deviation. The thresholds are `component_audit.min_samples` and `dominance`.
### The ✋ mark — a check the script does not do
**Missing required state**, marked `✋` above, is not done by the audit script. Not because it can't be, but because **it shouldn't be** — which states are required depends on whether the screen is a list, a form, or a search, and that call requires reading the screen's content. Faking it with a regex produces "if the name contains 'list' it's a list screen", which only adds false positives.
So this item is **judged by the agent, in step 3.** Read the target screen as a screenshot, settle its type, compare against the matching list in `naming.required_states`, and write the missing states into the report separately. If the type is ambiguous, do not decide — record it as "type unclear".
Keep **the script's violations and this judgement separate** in the report. They rest on different kinds of evidence.
## Procedure
1. **Settle the target page and resolve the config.** Take fileKey and page from the URL and get `CFG` via `resolve-config.py --js <fileKey>`. For several pages, split per page and **call in parallel** — one `setCurrentPageAsync` per script, per the figma-use rule.
2. **Run the read-only audits** — the scripts below (A structure / B flow / C components) and collect violations. Zero node changes. C is two stages: derive conventions from a reference page (C-1), then compare against the working page (C-2). Different pages, so different calls.
3. **Judge required states (✋) and do the second visual pass** — settle the screen type (list, form, search) from a screenshot and compare against `naming.required_states`. Then, if a violation is suspected or clone/move was involved, confirm with **a screenshot of the whole section node**, not an isolated frame. Isolated renders cannot catch parent or position errors, so they are never grounds for a PASS.
4. **Report** — violating nodes by category, a one-line reason each, and **which skill fixes it** (structure → fig:prep, flow → fig:arrows). Zero violations is `PASS`.
## Running it
The audit code lives in `${CLAUDE_PLUGIN_ROOT}/_common/scripts/`. Snippets are not copied into this document — two copies means one gets fixed and they drift apart.
```
${CLAUDE_PLUGIN_ROOT}/_common/scripts/audit-struct.js A. structure
${CLAUDE_PLUGIN_ROOT}/_common/scripts/audit-flow.js B. flow
${CLAUDE_PLUGIN_ROOT}/_common/scripts/audit-component.js C. components (MODE=collect / compare)
```
All three are called the same way.
1. Get the single `const CFG = {...};` line from `resolve-config.py --js <fileKey>`
2. Concatenate that line and the whole script, and hand it to `use_figma`. If the target page is not the first page, prepend one `setCurrentPageAsync` line (once per script)
3. The return value is either an array of violations or `STRUCT PASS` · `FLOW PASS` · `COMPONENT PASS`
For several pages, split the calls per page and **issue them in one message so they run in parallel**. One script never switches pages twice.
**C is two stages and its calls are split.** `MODE="collect"` derives STAT from the reference page; `MODE="compare"` checks the working page against it. If one reference page does not yield enough samples, take one form-heavy page and one list-heavy page and sum their STAT.
After editing a script, check its syntax with `scripts/lib/check.sh`. `use_figma` wraps scripts in an async function, so top-level `await` and `return` are both legal — a combination `node --check` alone cannot accept. check.sh wraps them the same way before checking.
## Constraints
- **Never substitute eye inspection for the component default check (C)** — empty icon slots and empty supplementary areas are invisible at zoomed-out scale. Always run C after building a screen by duplication, or after duplicating a field. Repairs go through fig:prep as a **"handle the first match, then re-query" while loop** — changing an instance property invalidates sibling handles
- **Zero writes** — `use_figma` is read-only (`return` only). Never create or change a node. Repairs go to fig:prep or fig:arrows behind their own go
- **Never PASS on an isolated screenshot alone** — measured metadata is the first pass, a whole-section screenshot the second. A frame rendered by itself cannot show a parent or canvas-position error
- Convention-dependent checks (naming and the like) are only decided when the config has a pattern. On `null`, skip the check; if it ran on bundled defaults, say so in the report
- **Excluded sections are treated three different ways** — dropped from the audit, dropped from coverage, but **kept as pass-through targets.** If a line actually crosses one, it is a broken line whether the section is excluded or not. The list is `pages.exclude_sections`
- Duplicate frame names make a name lookup return only one of them. Report `[duplicate name]` first, then narrow to a single section and re-run
- **States managed on a common page are not counted as per-screen omissions or orphans** (see fig:prep, "repeated common elements") — (1) a screen whose Default carries a reference annotation to the common page counts that state as covered, (2) canonical state frames on a page matching `naming.common_page_pattern` are expected not to appear in per-screen flow, so they are dropped from coverage. If the setting is `null`, this exception does not apply
## Notes
- fig:prep and fig:arrows contain no audit code. They call this skill as a gate right after writing, and handle only **the repair side** of what it finds
- This skill only answers "what is wrong". The reasoning for "why it should be placed or connected that way" lives in fig:prep, fig:arrows, and the comments in the config file
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!