Agent skill that draws architecture, flowchart, sequence, data-flow and lifecycle diagrams as hand-placed SVG, to one linted house style.
# Add to your Claude Code skills
git clone https://github.com/bybit-exchange/svg-diagramsvg-diagram is an open-source ai agents skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by bybit-exchange. Agent skill that draws architecture, flowchart, sequence, data-flow and lifecycle diagrams as hand-placed SVG, to one linted house style. It has 82 GitHub stars.
svg-diagram's catalog security scan is still queued. You can run an instant dependency and prompt-injection check now with the "Scan for vulnerabilities" button above.
Clone the repository with "git clone https://github.com/bybit-exchange/svg-diagram" and add it to your Claude Code skills directory (see the Installation section above). svg-diagram ships a SKILL.md manifest, so compatible agents can discover and load it automatically.
svg-diagram is primarily written in JavaScript. It is open-source under bybit-exchange on GitHub, so you can review or fork the full source.
Yes. SkillsLLM lists many other AI Agents skills you can browse and compare side by side. Open the AI Agents category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh svg-diagram against similar tools.
No comments yet. Be the first to share your thoughts!
Unlocks once the catalog security scan passes (runs nightly).
⚠️ Third-Party Software Notice
This skill is third-party open-source software developed and hosted independently on GitHub. SkillsLLM is an informational directory and does not control or maintain the underlying repository.
Any security checks, ratings, or warnings displayed by SkillsLLM are automated and limited in scope. They do not constitute a security certification or guarantee that the software is safe, error-free, or free from malicious code, vulnerabilities, compromised dependencies, or prompt-injection risks.
Review the source code, permissions, dependencies, and configuration before installing or running any third-party skill. Use is at your own risk. To the maximum extent permitted by applicable law, SkillsLLM is not liable for losses arising from third-party software.
The deep catalog scan for this skill is still queued. Run an instant dependency check now instead.
Diagrams go in SVG, never ASCII art. That rule decides whether to draw; this skill covers how, starting with where the file lands.
assets/ directory next to the document (e.g. docs/foo.md → docs/assets/foo-arch.svg). Do not inline SVG XML.The first SVG you generate must already satisfy every item in the verification checklist at the end of this document. Do not rely on user feedback to fix basic layout problems (spacing, padding, alignment, viewBox clipping). After generating, walk the checklist yourself and fix anything you find before showing the result.
<!-- ✅ Correct: label sits beside the line -->
<path d="M35,200 C 35,120 ..." .../>
<text x="55" y="200">Label text</text> <!-- 20px clearance -->
<!-- ❌ Wrong: label sits on the line -->
<text x="35" y="200">Label text</text>
The left and right halves should carry similar visual weight; add or remove annotation boxes to even them out.
content width = right edge of rightmost element - left edge of leftmost element
left margin = (viewBox width - content width) / 2
Center the title on the content center, not the viewBox center (they differ whenever the content is asymmetric):
<!-- content center = (content left edge + content right edge) / 2 -->
<text x="[content center x]" y="[top_padding + font_ascent]" text-anchor="middle">Title</text>
Only when the content is left-right symmetric does content center x = viewBox width / 2. When it is asymmetric, center the whole group with <g transform="translate(...)"> and update the title's x to the new content center as well.
If you only notice asymmetric left/right (or top/bottom) padding after the SVG is finished, you do not need to touch every coordinate. Wrap the content in one <g transform="translate(dx, dy)">:
<svg viewBox="0 0 800 460" ...>
<defs>...</defs>
<g transform="translate(22, 0)">
<!-- all content here, coordinates unchanged -->
</g>
</svg>
Computing the offset: dx = (right padding - left padding) / 2, where padding is the distance from the viewBox edge to the nearest content element.
dx > 0: shift content right (use when content sits too far left)dx < 0: shift content left (use when content sits too far right)When to use it: the content as a whole is off-center and you do not want to edit every x coordinate. Downside: the coordinates in the file no longer equal rendered positions, so later edits require mentally subtracting the offset — if a larger layout rework is coming, fold the offset into the individual coordinates and drop the <g>.
The title is part of the content, so top and bottom margins are measured from the title:
title y = margin + font ascent (≈ font-size × 0.75)
viewBox height = bottom y of content + margin
Recommended margin: 20–25px
<!-- Example: 16px title, 20px margin -->
<!-- title y = 20 + 16×0.75 ≈ 32 -->
<!-- content bottom 270px, viewBox height = 270 + 25 = 295 -->
<svg viewBox="0 0 680 295" ...>
<text x="340" y="32" font-size="16">Title</text>
...
</svg>
text-anchor="middle")outer size = content size + padding × 2Used to group related boxes together (e.g. "Server", "Client", "Employee instances").
Style consistency: within one diagram, every dashed grouping box must share these attributes:
stroke-dasharray (recommended 6,4)rx (corner radius, recommended 8–12)fill (recommended #f8fafc or none)Title placement: put the group title inside the top-left corner of the box (x = box x + 10, y = box y + 14) using the secondary text color #64748b.
Vertical-centering trap: compute the group box's vertical position from the combined height of title + inner content, not from the box alone. With a title, the top of the box is occupied for roughly 20–25px, and inner boxes start below it:
<!-- ✅ Group box: title + content laid out as a whole -->
<!-- box top y=100, title takes 20px, inner boxes start at y=125 -->
<rect x="50" y="100" width="200" height="120" rx="10"
stroke="#94a3b8" stroke-dasharray="6,4" fill="#f8fafc"/>
<text x="60" y="114" font-size="11" fill="#64748b">Server</text>
<!-- inner boxes -->
<rect x="65" y="125" width="170" height="36" rx="6" .../>
<rect x="65" y="170" width="170" height="36" rx="6" .../>
Connectors pointing at a group: when a connector logically targets a set of elements rather than one of them, terminate it on the group box boundary instead of on a single member. Alternatively, wrap the elements in a grouping box and point the arrow at that box.
Derive box height from the font size so there is enough inner padding:
| Content | Height formula | Example (12px font) |
|---|---|---|
| One line | font-size × 3 | 36px |
| Two lines | font-size × 3 + line height | 36 + 18 = 54px |
| Multiple lines | font-size × 3 + (lines - 1) × line height | +18px per extra line |
Where the formula comes from: font-size × 3 = top padding (≈font-size) + glyph height (≈font-size) + bottom padding (≈font-size). Scale the same ratio for non-standard font sizes.
Recommended line height: font-size × 1.5 (e.g. 18px for a 12px font)
In SVG, <text y=...> is the baseline, not the top of the glyph. To center text optically inside a box:
text y = box center y + font-size × 0.35
= box y + box height/2 + font-size × 0.35
The 0.35 factor is an empirical value for "optical glyph center to baseline" (roughly one third of the font size).
Keep the two factors apart:
font-size × 0.75 (ascent): for elements positioned from the top, such as titles (y = top_padding + ascent)font-size × 0.35 (baseline offset): for vertical centering inside a box<!-- One line: height = 12 × 3 = 36px -->
<rect x="30" y="50" width="110" height="36" rx="6" .../>
<!-- y = 50 + 36/2 + 12×0.35 = 50 + 18 + 4.2 ≈ 72 -->
<text x="85" y="72" ...>Single line</text>
<!-- Two lines: height = 36 + 18 = 54px -->
<rect x="30" y="50" width="110" height="54" rx="6" .../>
<text x="85" y="70" ...>First line</text>
<text x="85" y="88" ...>Second line</text> <!-- 18px apart -->
<svg viewBox="0 0 750 400" width="750" xmlns="http://www.w3.org/2000/svg">
| Diagram type | width |
|---|---|
| Simple list | 400px |
| Flowchart | 600–700px |
| Architecture diagram | 700–800px |
Use a straight L line for co-axial connections; use smooth C/Q curves whenever the path turns or routes around something. Never use right-angle elbows.
<!-- ✅ Horizontally co-axial: straight line -->
<path d="M190,77 L 320,77" .../>
<!-- ✅ Vertically co-axial: straight line -->
<path d="M130,145 L 130,213" .../>
<!-- ❌ Right-angle elbow for a turn -->
<path d="M100,100 L100,200 L200,200" .../>
<!-- ✅ Smooth curve for a turn -->
<path d="M100,100 C 100,150 150,200 200,200" .../>
| Case | Recommended | Syntax |
|---|---|---|
| Co-axial (horizontal/vertical) | L | L endx,endy |
| Simple turn | Q | Q ctrlx,ctrly endx,endy |
| S-curve / complex path | C | C ctrl1x,ctrl1y ctrl2x,ctrl2y endx,endy |
x = (source right edge + target left edge) / 2, together with text-anchor="middle"Fixing arrow direction on curved paths: orient="auto" aligns the arrowhead with the tangent at the end of the path. The end tangent of a C/Q curve can be skewed, which makes the arrowhead look crooked. The fix: place the second control point (cp2) so that the direction cp2 → end point is exactly the direction you want the arrowhead to face.
<!-- ❌ cp2 placed arbitrarily, arrow direction uncontrolled -->
<path d="M100,100 C 100,200 250,200 300,250" marker-end="url(#arrow)"/>
<!-- ✅ cp2 shares x with the end point, tangent naturally points down -->
<path d="M100,100 C 100,200 300,220 300,250" marker-end="url(#arrow)"/>
<!-- ✅ cp2 shares y with the end point, tangent naturally points right -->
<path d="M100,100 C 100,200 270,250 300,250" marker-end="url(#arrow)"/>
Rules for controlling arrow direction:
| Desired direction | cp2 constraint | Example |
|---|---|---|
| ↓ down | cp2.x = end.x, cp2.y < end.y | C ...,... ex,ey-20 ex,ey |
| → right | cp2.y = end.y, cp2.x < end.x | C ...,... ex-20,ey ex,ey |
| ← left | cp2.y = end.y, cp2.x > end.x | C ...,... ex+20,ey ex,ey |
| ↑ up | cp2.x = end.x, cp2.y > end.y | C ...,... ex,ey+20 ex,ey |
Computing path endpoints (5px clearance, arrowhead extends 6px past the end of the line):
Core rule: start 5px away from the source box, end 11px away from the target box (5px clearance + 6px arrowhead extension).
Because we use refX="2" (the notch at the tail of the arrowhead), the tip extends 6px (8-2=6) beyond the end of the line, and the line joins the arrowhead at the center of its notch, so the join reads as continuous.
Formulas for the four directions:
| Direction | Start | End |
|---|---|---|
| → right | source right edge + 5 | target left edge - 11 |
| ← left | source left edge - 5 | target right edge + 11 |
| ↓ down | source bottom edge + 5 | target top edge - 11 |
| ↑ up | source top edge - 5 | target bottom edge + 11 |
<!-- Rightward arrow →: source right edge 180, target left edge 220 -->
<!-- start: 180+5=185, end: 220-11=209, tip reaches 209+6=215 -->
<path d="M185,70 L 209,70" marker-end="url(#arrow)"/>
<!-- Leftward arrow ←: source left edge 220, target right edge 180 -->
<!-- start: 220-5=215, end: 180+11=191, tip reaches 191-6=185 -->
<path d="M215,70 L 191,70" marker-end="url(#arrow)"/>
<!-- Downward arrow ↓: source bottom 140, target top 170 -->
<!-- start: 140+5=145, end: 170-11=159, tip reaches 159+6=165 -->
<path d="M130,145 L 130,159" marker-end="url(#arrow)"/>
<!-- Upward arrow ↑: source top 170, target bottom 140 -->
<!-- start: 170-5=165, end: 140+11=151, tip reaches 151-6=145 -->
<path d="M130,165 L 130,151" marker-end="url(#arrow)"/>
Bidirectional example (two boxes with arrows going both up and down):
<!-- box A bottom=375, box B top=420 -->
<!-- downward arrow: A→B -->
<path d="M320,380 L 320,409" marker-end="url(#arrow)"/> <!-- 375+5, 420-11 -->
<!-- upward arrow: B→A -->
<path d="M480,415 L 480,386" marker-end="url(#arrow)"/> <!-- 420-5, 375+11 -->
Keep detour paths at least 20px outside the obstacle's boundary:
<!-- Gateway right edge=620, detour path runs at x=650 -->
<path d="M600,313 Q 650,313 650,400 Q 650,640 635,685"
fill="none" stroke="#a855f7" stroke-dasharray="6,4" marker-end="url(#arrow)"/>
Spacing between adjacent boxes/blocks is ≥25px, 25–30px recommended.
Breakdown: 5px start clearance + 11px end clearance and arrowhead extension + ≥6px of visible line + safety margin ≈ 25px. At least 6px of visible line, otherwise the arrow degenerates into a dot.
Stay compact: do not exceed 30px. Spacing that is too wide (>40px) makes the whole diagram feel loose and the connectors overly long. Aim for 5px / 11px at the two ends and 6–12px of visible line in between.
Overall density: when you are done, step back and judge the density — if the content area is clearly small relative to the viewBox (large blank regions), tighten spacing or shrink the viewBox. Common causes: box spacing >30px, viewBox margin >25px, excessive padding inside grouping boxes.
This is the only block-spacing standard in this document; both "No overlapping elements" and the verification checklist refer back to it.
SVG has no z-index — elements painted later sit on top. Order them like this:
<!-- 1. Bottom layer: background boxes (dashed grouping boxes, swimlanes) -->
<rect ... stroke-dasharray="6,4" fill="#f8fafc"/>
<!-- 2. Middle layer: connectors and arrows -->
<path d="..." marker-end="url(#arrow)"/>
<!-- 3. Top layer: boxes and text -->
<rect ... fill="#dbeafe"/>
<text ...>Label</text>
Common mistake: painting connectors first and the dashed background box afterwards → the connectors get covered.
Exception: cross-layer loop-back lines: connectors that must span several boxes — iteration loops, cross-region dashed lines — have to be painted after the boxes (layer 4), otherwise the boxes hide them:
<!-- 1. background boxes -->
<!-- 2. ordinary connectors -->
<!-- 3. boxes and text -->
<!-- 4. cross-layer loop-back lines (topmost, painted over the boxes) -->
<path d="..." stroke-dasharray="6,4" marker-end="url(#arrow-purple)"/>
Before placing text, estimate its right edge and confirm it does not intrude on neighboring elements:
| Font size | Latin char width (approx.) | CJK char width (approx.) |
|---|---|---|
| 8px | 4.5px | 8px |
| 9px | 5.0px | 9px |
| 10px | 5.5px | 10px |
| 11px | 6.0px | 11px |
| 12px | 7.0px | 12px |
Estimation formulas:
text-anchor="start": right edge ≈ x + char count × char widthtext-anchor="middle": left/right edge ≈ x ± (char count × char width) / 2text-anchor="end": left edge ≈ x - char count × char widthCheck before placing: text right edge + 10px < left edge of the neighbor to its right.
<!-- ❌ Text "4K tokens (= mini-batch)" starts at x=274 -->
<!-- 24 chars × 5px ≈ 120px, right edge ≈ 394, intrudes on the box at x=350 -->
<text x="274" y="115" font-size="9">4K tokens (= mini-batch)</text>
<!-- ✅ Moved below the box and centered, intrudes on nothing -->
<text x="154" y="138" font-size="9" text-anchor="middle">4K tokens (= mini-batch)</text>
A label describing an arc or curve must not sit on the path itself (the line would run through the text) — put it on the convex or concave side of the curve:
<!-- ❌ Label at y=218 while the curve passes near y=225: the line cuts the text -->
<text x="350" y="218">Skip Connection</text>
<path d="M82,165 C 82,225 620,225 646,225" .../>
<!-- ✅ Label at y=222, lowest point of the curve at y=260, 38px apart -->
<text x="350" y="222">Skip Connection</text>
<path d="M82,149 C 82,260 600,255 640,255" .../>
Rule: keep ≥15px between the label and the nearest point on the curve. For a downward-bending curve put the label above it; for an upward-bending curve put it below.
<!-- Gateway: x=280, y=55, width=340, height=435, right edge=620, bottom edge=490 -->
<rect x="280" y="55" width="340" height="435" .../>
Every SVG must declare a CJK-capable font stack in <style> — this is non-negotiable:
<style>
text { font-family: 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', system-ui, sans-serif; }
</style>
Fallback order: macOS (PingFang SC) → Windows (Microsoft YaHei) → Linux (Noto Sans CJK SC) → system default. Noto Sans CJK SC must be kept, otherwise server-side rendering on Linux falls back to a font with no CJK coverage.
| Element | Size |
|---|---|
| Title | 16px |
| Box body text | 12px |
| Secondary text / annotations | 10px |
Vertical text: <text writing-mode="tb">vertical text</text>
Base colors: primary text #1e293b, secondary text #64748b, muted text / arrows #94a3b8
Semantic colors:
| Meaning | Fill | Stroke | Text |
|---|---|---|---|
| Input / primary | #dbeafe | #3b82f6 | #1e40af |
| Processing / in progress | #fef3c7 | #f59e0b | #b45309 |
| Data / output | #d1fae5 | #22c55e | #166534 |
| AI / analysis | #f3e8ff | #a855f7 | #6b21a8 |
| Sensitive / warning | #fce7f3 | #ec4899 | #9d174d |
Use a notched arrowhead rather than a plain triangle — it reads better:
<defs>
<!-- Default gray arrow (markerUnits="userSpaceOnUse" is mandatory) -->
<marker id="arrow" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#64748b"/>
</marker>
<!-- Semantic-color arrows (matching the box stroke colors) -->
<marker id="arrow-blue" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#3b82f6"/>
</marker>
<marker id="arrow-orange" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#f59e0b"/>
</marker>
<marker id="arrow-green" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#22c55e"/>
</marker>
<marker id="arrow-purple" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#a855f7"/>
</marker>
<marker id="arrow-red" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#ef4444"/>
</marker>
</defs>
Arrow color reference:
| ID | Color | Use |
|---|---|---|
arrow |
#64748b | Default / neutral connection |
arrow-blue |
#3b82f6 | Input / primary flow |
arrow-orange |
#f59e0b | Processing / in progress |
arrow-green |
#22c55e | Data / output / success |
arrow-purple |
#a855f7 | AI / analysis / special |
arrow-red |
#ef4444 | Warning / dangerous operation |
Key parameters:
orient="auto" rotates the arrowhead to follow the path directionmarkerUnits="userSpaceOnUse" must be declared explicitly, otherwise the default strokeWidth scales the arrowhead with line thickness (at stroke-width=2 the arrow doubles in size and blows past the intended clearance)refX="2" aligns the end of the line with the center of the arrowhead's tail notch, so the join looks naturalrefY="4" centers it vertically (arrow height 8, midpoint 4)M0,0 L8,4 L0,8 L2,4 z forms the notched shape, with the tip at x=8target edge - 11Arrows on thick lines: when stroke-width > 1.5 (e.g. heavy dashed lines), an 8×8 arrowhead looks too small. Define a proportionally larger marker:
<!-- Large arrow for thick lines (1.5×): tip extends 12-3=9px -->
<marker id="arrow-red-lg" markerWidth="12" markerHeight="12" refX="3" refY="6"
orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L12,6 L0,12 L3,6 z" fill="#ef4444"/>
</marker>
| Line stroke-width | Marker size | refX | Tip extension | End point formula |
|---|---|---|---|---|
| ≤ 1.5 | 8×8 | 2 | 6px | target edge - 11 |
| 1.5 ~ 2.5 | 12×12 | 3 | 9px | target edge - 14 |
| > 2.5 | 16×16 | 4 | 12px | target edge - 17 |
SVG is XML, so special characters in text must be escaped or the entire SVG fails to render:
| Character | Escape | Example |
|---|---|---|
& |
& |
Validate & Sanitize |
< |
< |
x < 10 |
> |
> |
x > 0 |
" |
" |
inside attributes |
' |
' |
inside attributes |
<!-- ❌ Wrong: unescaped & -->
<text>Load & Save</text>
<!-- ✅ Correct: escaped -->
<text>Load & Save</text>
When generating SVG from Python/JS, text coming from external sources (CSV, database, API) must be escaped before it is concatenated into the SVG:
def svg_escape(text):
return str(text).replace('&','&').replace('<','<').replace('>','>').replace('"','"')
# ❌ Splicing external data directly ("Research&Development" → XML parse failure, image won't render)
svg += f'<text>{team_name}</text>'
# ✅ Escape first
svg += f'<text>{svg_escape(team_name)}</text>'
High-risk data sources: team names (contain &), requirement descriptions (contain <>), user input, file names
| Problem | What to check |
|---|---|
| Elements overlap | Text-to-line clearance ≥10px; does a detour path cross a boundary? |
| Arrowhead invisible | Block spacing below 25px — increase it |
| Label off-center | Confirm text-anchor="middle" and x = center of the gap |
| Arrow points the wrong way | Check the direction of the final path segment (x/y increasing or decreasing) |
| XML parse error | Check whether & < > in text are escaped |
| Content clipped at the bottom | viewBox height too small; it must equal the bottom edge of the lowest element + 25px |
| Dashed group box off-center after adding a title | Vertical centering must use the combined "title + content" height — see "Dashed grouping boxes" |
| Too much blank space overall | Check whether box spacing >30px or viewBox margin >25px |
markerUnits="userSpaceOnUse"text-anchor="middle")width attribute present, viewBox matches the content<g transform="translate(dx,0)"> to shift if not)& → &, < → <)When you receive a request to modify an SVG, do not start editing immediately. First judge:
Goal: fewer round trips, so you are not re-emitting the whole SVG for every single tweak.
Once all edits are done, print the change list first, then show the final SVG:
Change list:
1. Title spacing 20px → 30px
2. Box A fill #dbeafe → #f3e8ff
3. Connector L1 path adjusted (routes around the new box)
Do not show the diagram after each individual edit — the user only needs the final result.
When the user says "move box A 20px to the right", do not mechanically change box A's x alone. Handle the dependents too:
Spacing/alignment requests ("make it tighter", "left-align it", "center it") → read the overall intent and adjust all related elements in one pass, so the user does not have to iterate pixel by pixel.
A house style for hand-written SVG diagrams your agent can follow — the layout arithmetic, the colour system, and a zero-dependency linter that proves it did.
Paste this to your coding agent:
Install the svg-diagram skill from https://github.com/bybit-exchange/svg-diagram
for me by running: npx skills add bybit-exchange/svg-diagram -g
Or run it yourself:
npx skills add bybit-exchange/svg-diagram -g
The CLI detects which agents you have installed and writes each one's path. Drop -g to install into the current project instead.
| Surface | Path |
|---|---|
| Claude Code | ~/.claude/skills/svg-diagram/ |
| Codex, Cursor, Gemini CLI, Copilot, opencode, Antigravity | ~/.agents/skills/svg-diagram/ — they share one directory |
| Pi | ~/.pi/skills/svg-diagram/ |
| Windsurf, Continue, Roo, Goose, Kiro, Trae and 40+ more | ~/.<agent>/skills/svg-diagram/ |
| Claude.ai | Zip the folder and upload it under Settings → Capabilities → Skills (skill text only — svg-lint needs local Node) |
Every path is named from the skill's frontmatter name, not from where the skill sits here, so moving files in this repository doesn't change them.
SKILL.md is at the repository root, so the install copies the root and svg-lint comes along with the skill text. The linter has no dependencies, so it runs from wherever it landed:
node ~/.claude/skills/svg-diagram/tools/svg-lint/bin/svg-lint.mjs diagram.svg
If you only want the skill text, take the one file the agent reads:
mkdir -p ~/.claude/skills/svg-diagram
curl -fsSL https://raw.githubusercontent.com/bybit-exchange/svg-diagram/main/SKILL.md \
-o ~/.claude/skills/svg-diagram/SKILL.md
The skill works on its own that way; svg-lint is what you give up.
Then start a new session and ask for a diagram — the agent should announce that it's using svg-diagram.
Ten diagrams, all drawn under this skill and all lint clean. Each one is here for the rule it demonstrates.
Two dashed containers side by side. Connectors cross the channel between them, and a colour change marks the point where a request matches.
A cross-layer loop-back line, painted after the boxes so they don't cover it.
A self-call that leaves its lifeline and curves back to it, its second control point sharing the end point's y so the arrowhead closes level and points at the participant rather than down the lifeline.
CJK labels sized from the CJK column of the width table — one full font size per character, 12px rather than 7.
One semantic colour triple per state. Each forward arrow carries the colour of the state it leaves.
A dashed border only where the contents are actually drawn — gallery/ and assets/ are directories too, but their members aren't in the figure, so they stay plain boxes.
A one-to-many fan-out whose branch curves keep each second control point level with the end point, so every arrowhead lands pointing right.
A contrast pair built from two semantic triples: the working paths in green, the branch that fails in pink.
A symmetric fan-out and convergence, one triple per tier. Only the middle leg leaves the rule box's bottom edge; the two outer ones leave its sides.
A dashed group box around exactly the steps that repeat. The return edge is painted last, so it stays legible where it crosses the wall.
| Area | What's pinned down |
|---|---|
| Layout | viewBox margins of 20–25px, the title centred on the content centre rather than the viewBox, and off-centre content shifted with a single <g transform="translate(dx,0)"> |
| Boxes | Height derived from the font — one line is font-size × 3, each extra line adds font-size × 1.5 — and boxes sharing a row whose sizes differ by no more than 60px |
| Connectors | A straight L only when the two ends are co-axial, a C or Q curve for anything that turns, and no right-angle elbows |
| Arrowheads | A notched marker with markerUnits="userSpaceOnUse", sized to the line's stroke width, starting 5px clear of the source and ending 11px short of the target |
| Text | Baseline at box y + height/2 + font-size × 0.35 to centre inside a box, and at least 10px of clearance from a straight connector — 15px from a curve, with the label above a downward bend and below an upward one |
| Fonts | 16px titles, 12px box text, 10px annotations, over a stack that keeps Noto Sans CJK SC so Linux rendering doesn't fall back to tofu |
| Colours | Five semantic fill/stroke/text triples applied through presentation attributes — no CSS classes, no media queries |
| Escaping | &, <, >, " and ' escaped in text and attributes, and anything from a database or an API escaped before it's concatenated into the file |
Every diagram also paints its own white canvas rect as the first element, so it reads as a light card on GitHub's dark theme instead of dark text on a dark background. That isn't theme switching. The diagram carries one colour set, and the rect exists only so that set stays readable wherever the file is embedded.
The full rules — the character width tables, the six arrowhead colour variants, the coordinate bookkeeping, and a twenty-item verification checklist — are in SKILL.md, which is the single source of truth.
svg-lint is plain Node with no packages. It runs in a fresh clone, with no install step and no lockfile.viewBox: 11 → 20–25, not "the margin looks wrong".svg-lint reads the finished SVG and reports what the house style forbids. It's a maintainer tool, run by hand. Nothing invokes it for you, so run it before you hand a diagram over.
node tools/svg-lint/bin/svg-lint.mjs diagram.svg
node tools/svg-lint/bin/svg-lint.mjs diagram.svg --json
Try it on a deliberately broken file:
printf '%s' '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 60" width="200"><text x="50" y="20" font-size="12">Load & Save</text></svg>' > /tmp/broken.svg
node tools/svg-lint/bin/svg-lint.mjs /tmp/broken.svg
That reports 3 errors and 7 warnings and exits 1: the unescaped &, the missing font stack, the missing white canvas rect, an off-palette fill, four viewBox margins outside the 20–25px range, and asymmetric margins on both axes. Each finding carries the id of the check that raised it and a repair line:
1:1 error No <style> rule declares font-family for text [font-stack/missing-font-stack]
repair: font-family: absent → 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', system-ui, sans-serif · SKILL.md marks this non-negotiable
The 12 checks cover XML escaping, viewBox clipping, the font stack, box height, baseline offset, block spacing, arrow markers, text overflow, overlap, light-background fallback, palette conformance and connector geometry. A thirteenth id, document-model, raises no findings of its own. It's how the model layer reports what it couldn't read, so the geometry checks never draw c