Why Plotwork draws plain SVG
A Plotwork diagram is drawn once, when it is written, into a static SVG file. Showing it runs no code and adds no package to your pages.
TL;DR
| Question | Answer |
|---|---|
| When is a diagram drawn? | Once, when it is written: by your agent through the MCP server, by the command line, by the GitHub Action in CI, or by the API. The result is an SVG file. |
| What does a reader's page need to show it? | An image tag. No JavaScript library, no npm package, no script from a CDN, and no server that draws on request. |
| Why does that matter for security? | Code that runs in your pages is code an attacker can replace. Every package and script tag you add is one more party you trust. A picture with nothing to run removes that party. |
| Can the file run anything? | No. It holds no <script>, no event handler, no foreignObject, no link, and loads nothing from another address. A browser shows an SVG in an image tag with scripts off and no outside requests anyway. |
| Do light and dark need script? | No. Either two files chosen by a <picture> element, or one file whose own CSS switches colors with prefers-color-scheme. |
| What still uses packages? | The tools that draw: the command line, the MCP package, and the Action. They run where diagrams are written or in CI, never in the reader's browser. Each npm package is one bundled file with no dependencies and no install script, published with npm provenance. The Action is one bundled file, pinned by commit, with a signed build attestation. |
| Can it run with nothing leaving our network? | Yes. The self-hosted engine makes no outbound request and sends no telemetry. |
1. Drawn once, shown as a file
Most diagram-as-code tools draw at view time. A Mermaid fence is text until a script in the page, or the host's own viewer, turns it into a picture in the reader's browser. A PlantUML diagram is drawn by Java, usually behind a server the page asks each time it shows the picture.
Plotwork moves the drawing to the moment the diagram is written. The agent, the command line, the Action or the API lays the diagram out and writes an SVG file next to its source. From then on the file is the diagram:
| Step | Where it happens | What runs |
|---|---|---|
| Write the source | Your editor, your agent, or a generator in CI | The Plotwork engine, in the command line, the MCP server, the Action, the API, or your self-hosted container |
| Draw the picture | The same place, once | The same engine |
| Show the picture | The reader's page, wiki, pull request, or PDF | Nothing. The page loads an image. |
The page that shows the diagram imports nothing from Plotwork and needs no build step, plugin, or script for it.
2. What ships to the reader
| Plotwork | Mermaid in the page | PlantUML and other server renderers | |
|---|---|---|---|
| What the reader's page loads | One SVG file | The Mermaid library, then the picture it draws in the browser | An image from a drawing server, on each view or from a cache |
| Runtime dependencies in the page | None | Mermaid and the packages bundled into it. Mermaid 12.1.0 lists 23 direct dependencies, and its mermaid.min.js is 5.3 MB (about 1.5 MB compressed). | None in the page; the server runs Java or the tool's own runtime |
| Works where scripts are blocked | Yes | Only where the host draws Mermaid itself, such as GitHub | Yes, once the server has drawn the image |
| Content Security Policy it needs | An img-src that allows where the file is served from | A script-src that allows the Mermaid bundle, from your origin or a CDN | An img-src that allows the drawing server |
| Can the picture change after you approve it | No. The file is the bytes you reviewed. | Yes. The same text redraws with whatever Mermaid version the page loads. | Yes, when the server's version changes |
Mermaid can also be drawn ahead of time with its command line, which runs a headless browser to do it. That removes the script from the page, and keeps the browser in your build.
3. Why fewer runtime dependencies matters
A script a page loads runs with the page's full rights: it can read what the reader types, change what they see, and send it anywhere. A package a build installs runs on the developer's machine or the CI runner, often with tokens in reach. Attackers know this, and the incidents below are a few of the public ones. None involved a diagram tool. Each shows a way trusted code changed hands.
| Date | What | What happened | Source |
|---|---|---|---|
| November 2018 | event-stream | The maintainer handed the package to a stranger, who added the dependency flatmap-stream with an encrypted payload. It ran only inside the Copay bitcoin wallet's build and stole wallet keys. | Snyk post-mortem (opens in a new tab), GitHub issue (opens in a new tab) |
| October 2021 | ua-parser-js | A hijacked maintainer account published three versions that installed a cryptocurrency miner and, on Windows, a password stealer. The package had millions of weekly downloads. | CISA alert (opens in a new tab), Rapid7 (opens in a new tab) |
| March 2022 | node-ipc | The maintainer added code that overwrote files on machines with Russian or Belarusian IP addresses (CVE-2022-23812). | GitHub advisory (opens in a new tab), The Register (opens in a new tab) |
| March 2024 | xz-utils | A backdoor hidden in release tarballs of the compression library, after years of patient work as a co-maintainer, targeted OpenSSH (CVE-2024-3094). It was found before it reached most stable Linux releases. | CISA alert (opens in a new tab), oss-security (opens in a new tab) |
| June 2024 | polyfill.io | A new owner of the CDN domain served code that sent some mobile visitors to scam sites. More than 100,000 sites loaded the script in their pages. | Sansec (opens in a new tab), Cloudflare (opens in a new tab) |
| September 2025 | debug, chalk, and 16 more | A phishing email took over a maintainer's account. The versions published hooked fetch and wallet calls in the browser to swap cryptocurrency addresses. The packages had more than 2 billion weekly downloads together. | Aikido (opens in a new tab), The Register (opens in a new tab) |
| September 2025 | Shai-Hulud | A self-spreading worm: an install script stole npm, GitHub and cloud tokens, then used them to publish infected versions of the victim's other packages. More than 500 packages were affected. | CISA alert (opens in a new tab), GitHub (opens in a new tab) |
| November 2025 | Shai-Hulud 2.0 | A second wave ran before install, stole developer and CI secrets, and reached hundreds more packages. | Wiz (opens in a new tab), CIRCL (opens in a new tab) |
| March 2026 | axios | Two versions added a malicious dependency that downloaded a remote access trojan. | CISA alert (opens in a new tab) |
| May 2026 | @tanstack/* router packages | 84 malicious versions across 42 packages stole cloud, npm, GitHub and SSH credentials when installed, and tried to republish the victim's packages. | TanStack post-mortem (opens in a new tab) |
Two lessons apply to diagrams. A script loaded from a CDN, as polyfill.io was, is only as safe as whoever owns that domain next. And a deep install tree, as Shai-Hulud showed, is many maintainers' accounts that each have to stay safe. A static picture asks the reader's page to trust neither.
4. What is in the file
Every picture Plotwork draws is an SVG document with these properties, and every picture on this site is checked against them.
| Property | Detail |
|---|---|
| No script | The renderer writes no <script> element and no event handler attribute such as onload. |
| No HTML | No foreignObject, so no HTML inside the picture. Text is SVG <text>. |
| No links | No <a> element. |
| Nothing from another address | Every reference is a fragment of the same file, such as a gradient or a clip. A vendor or organisation icon is carried inside the file as a data: image. |
| No web fonts | Text uses the reader's system fonts through a font stack. No @font-face and no font download. |
| Themes in CSS | Light and dark are either two files chosen by a <picture> element, or one file whose own <style> switches colors with prefers-color-scheme. Animated lines are CSS animations, and stop for readers who ask for reduced motion. |
| Words for a screen reader | The root has role="img" with a <title> and a <desc> that lists what the diagram shows. |
| Its own size | The root states its width and height, so a page keeps the picture's room while it loads. |
| Source, when you ask | With embedSource on, the diagram's source is stored as text in a <metadata> element, so it can be read back and edited. It is data, not code. Anyone who has the file can read it. |
Browsers add a second wall. An SVG shown through an <img> tag or a CSS background runs no script and loads no outside resource, whatever it holds (MDN: SVG as an image (opens in a new tab)). That is why Plotwork carries icons inside the file: they show in an image tag, where an outside reference would not.
Custom icons
An organisation's own icons go through an allowlist before they can be drawn. Only shapes, groups, gradients, clips, masks and titles are kept, with paint and geometry attributes. A <script>, foreignObject, <style> block, <image>, <a>, <use>, text or filter is refused with a reason. See What a file may hold.
5. Hosted pictures
A published diagram is served from cdn.plotwork.dev with this header:
Content-Security-Policy: default-src 'none'; img-src data:; style-src 'unsafe-inline'; sandboxIf a script ever appeared in a hosted picture opened as a page, this policy and the sandbox would stop it from running. Only the icons the file carries as data: images load.
Embedding a hosted link means the reader's browser fetches an image from Plotwork. To fetch nothing from anyone, commit the SVG file to your repository and serve it with your own pages.
6. What still uses packages, and how they are secured
The tools that draw are software, and you install them. They run when a diagram is written or checked, in an editor, on a developer's machine or in CI, and never in the page a reader opens.
| Tool | What you install | How it is secured |
|---|---|---|
Command line, @buun_group/plotwork | One bundled file | No dependencies and no install script, so installing it fetches one package and runs nothing. Published from its public repository with npm publish --provenance through trusted publishing. Each GitHub release carries checksums.txt. |
MCP package, @buun_group/plotwork-mcp | One bundled file | The same: no dependencies, no install script, npm provenance, and checksums. It makes no network request by default. |
GitHub Action, plotwork-official/plotwork-action | One bundled file, dist/index.js | Pin it by full commit SHA. Each release has dist/checksums.txt and a signed build provenance attestation you can check with gh attestation verify dist/index.js --repo plotwork-official/plotwork-action. Check mode reads no secret and makes no network request. See Pinning and updates. |
| Self-hosted engine | One container image | Distroless, with base images pinned by digest. It makes no outbound request and sends no telemetry. See Self-hosting the engine. |
The bundles contain other people's code, such as the layout engine. It is chosen when the bundle is built, from a locked dependency tree installed with install scripts turned off, and every package in it is listed with its licence in THIRD-PARTY-NOTICES.md. A compromise upstream would have to land before a build and pass its tests to reach a release, and a pinned release never changes after you review it.
7. Other things a plain file gives you
| What | Why |
|---|---|
| It shows where scripts are blocked | GitHub READMEs and pull request comments, wikis, documentation sites with a strict policy, and pages printed to PDF show an SVG image. For chat and email, export a PNG; see Slack, email, Confluence, and Notion. |
| It works offline | A wiki on an air-gapped network shows a committed file with nothing to fetch. |
| It never changes under you | The picture is the bytes you reviewed, not a redraw by whatever library version a page loads today. |
| It diffs in review | The same source gives the same bytes, so a change to the diagram is a small change to a text file in the pull request. |
| Nothing to draw at page load | The page shows a finished picture. There is no render step, and no jump when one finishes. |
See also
| Doc | Why |
|---|---|
| How a diagram is drawn | The steps from source to picture |
| Enterprise rollout | Pinning, checksums, attestations, and air-gapped runners |
| Self-hosting the engine | The engine inside your network, with nothing leaving it |
| Custom icons | What an uploaded icon may hold |
| Secret scanning | A warning before a key in a label reaches a published picture |
| Hosted diagrams | Publishing a diagram and sharing its link |
| Migrating from Mermaid | Moving existing Mermaid diagrams to plain pictures |