Report it privately through GitHub's private vulnerability reporting, not in a public issue. Include the version, how the server or CLI was run, and the smallest project or input that shows the problem. You get a reply within 2 working days.
Fixes go to the latest minor release only, as a patch release of it, with an advisory once users can upgrade. On 0.x, a minor release can change behaviour, so upgrade to the latest minor to get a fix.
Every release is published to npm by this repository's release workflow (.github/workflows/release.yml, on main), through npm trusted publishing with provenance: no token publishes it. To check what you installed, run this in the project:
npm audit signaturesIt verifies the registry signature of every installed package and the attestations of those that have one, onsystem among them. The package page on npmjs.com shows the same provenance: the workflow and the commit it built. Each release has a tag, onsystem@X.Y.Z, and a GitHub release.
-
The server runs locally over stdio, opens no ports and makes no network requests of its own.
npxdownloads the package from npm when the client starts it. -
It reads files under the project root (components, tokens, docs,
tsconfig,tailwind.config) and the workspace packages they point at. Innode_modulesit reads the config a design-system package declares, as JSON (never run), and the type declarations of a library or an installed design-system package it reads components from (MUI,@acme/ui). -
It writes its cache, the baseline file (creating its folder) when you run
check --update-baselineorcheck --prune-baseline, and the fileexport --outnames.initwritesonsystem.config.json; in a design-system package alsoONSYSTEM.mdand theonsystemfield (andfiles) ofpackage.json; and, when you say yes, an onsystem section inAGENTS.mdandCLAUDE.md. -
The cache is in
node_modules/.cache/onsystem. A project withoutnode_moduleshas it in a folder of the system temp folder that only you can open (onsystem-<uid>): one that is a link, belongs to another user or that others can write to is not read, since on a shared machine a model planted there would reach your agent. -
Component, token and docs files are read only when their real path, links resolved, is in the project, the workspace or git repository it is in, the package whose declared config it takes on, or a package installed in a
node_modulesfolder of those, which pnpm may link from a store elsewhere. A link, or a../or absolute path in the config, that leads anywhere else is skipped, andinspectand the server's log name it. The home folder, the folders above it and the filesystem root never count as the workspace, the repository or a package's folder. Thenode_modulesrule is the trade-off for pnpm's store: a link committed insidenode_modulesis followed like an installed package, so it can reach a package installed in another project on the machine. -
To show a component's props, TypeScript reads the modules the component imports wherever they resolve, outside the project too. A literal type from such a file (
export const KEY = "..." as const) can show inget_component. -
check_uiwith apathreads only files inside the project root, symlinks resolved, and limits input size. -
What the repository wrote reaches the agent: JSDoc, docs pages, examples, names, class lists, token names and values, file, folder and package names, config keys. Every text the MCP server sends about the project, each tool's answer, each resource it reads, the prompt, each error and notice, and each description in the resource list, is built as lines by one function,
answer()insrc/server/render.ts(the server takes no other text for them), and each line is one line: a line break left inside one, any of those Python'sstr.splitlines()splits at (\n,\r, a vertical tab, a form feed, U+001C to U+001E, U+0085, U+2028, U+2029), is escaped as in JSON (\n,\u2028), so the text after it cannot start a line of its own. An error's message, such as the list of problems in an invalid config, is one line too: every tool, resource, resource listing, completion and the prompt is registered through one helper that catches what its handler throws and sends it as an answer. No line starts with what the repository wrote, unmarked:- The JSDoc description and docs sections are Markdown quotes: every line starts with
>, whichever of those line breaks the file uses. - A deprecation note, a prop's, a part's or a token's description, the first sentence of a description in
list_components,search_componentsand the resource list, a prop's default that is not a short value (a@defaultsentence, a default over several lines, a string literal over 40 characters, cut to 80), a docs heading and an example title are in double quotes, on one line and cut short. shadcn/ui's purpose for a component without a description, Tailwind's default tokens and a library's theme are described by onsystem, and shown as written. - An example is code in a fence it cannot close.
- A name that does not look like one, one with a space, a quote mark or a line break, or longer than 40 characters (100 for a token or a package), is JSON-quoted and cut short: a variant, a variant value, a prop, a token, a mode, a package, a registry, an icon's module, the element a component renders. A file or folder path with more than a path's characters (a space, a line break, which git allows) is JSON-quoted, and so is every import path, and an invalid config's unknown key, rule, library or severity, the name of a dependency whose
onsystemfield cannot be read (in that error and in the warnings the CLI prints), and the name of a dependency whose config names paths outside its package or workspace, with those paths, in the warning that says they were ignored. - A
cvaclass list that does not read as classes (quote marks, sentence punctuation at the end of a class, a word in capitals, or three or more plain words that are not Tailwind classes), and a token's value or usage that does not read as a CSS value (three plain words in a row, or sentence punctuation before a word, outside quotes and parentheses), are quoted and cut short. These two checks are heuristics: a phrase of one or two plain words can still be shown as written. - When
get_component, theds://componentsresource,list_components,search_componentsorget_tokensmarks anything, a line of its answer says that text after>, in double quotes or in a code fence is the repository's own: a description, not instructions.
Shown as written, on one line: a name that looks like one (
outline,color.brand.500), a component's name (a capitalised identifier: extraction takes no other, from a source file or a package's type declarations, so a string-named export such asexport { Card as "Any text" }is no component), a prop's default that reads as a short value, a cast at its end included ("md" as Size), a prop's type (comments left out, each string literal cut to 40 characters), the project root, a path a tool was given in an error about it, and the repository's values inside a rule message, such as the allowed valuesno-unknown-variantlists (check_ui, and the plugin's hook, which prints a line break in one as it is). The structured content is JSON, with the repository's text as written. An agent reads the repository anyway; the marks keep its text from reading as onsystem's own answer. - The JSDoc description and docs sections are Markdown quotes: every line starts with
-
An
onsystem.config.ts,.mjsor.jsfile (or one under the olddesign-system-mcp.configname) is imported, so it runs as code, like an ESLint config. Atailwind.configis parsed, never run. Treat the config file like any other code in the repository.
The plugin's hook and server run in every project you open in Claude Code, so use them in repositories you trust, as you would an editor extension that runs a project's linter:
- Both import the project's
onsystem.config.ts,.mjsor.js, as above. - The hook runs the project's own install of the CLI when there is one (
node_modules/onsystem, whosebinit runs with Node.js), whatever its version. Without one, it runsnpxwith the exact version the plugin was released with. The server runsnpx -y onsystem@<that version>, which uses a project install of the same version. npxruns in the project folder, so the project's npm config applies to it, such as aregistryin its.npmrc.- The hook writes marker files in the system temp folder, so its notice is shown once per session and its pause after a failed
npxholds.
What it runs and the permissions it needs are in required-check.md. In short:
- It runs the onsystem the project installed (
node_modules/onsystem, fromworking-directoryup to the repository root), elsenpx --yes onsystem@<the action's version>. Pin a commit SHA to stay on one release of both. - On
pull_request,pull_request_targetandmerge_groupit runsgit fetchfromoriginfor the history it needs to find the changed lines. It needs onlycontents: read. checkimports the checked-outonsystem.config.*. Do not run it onpull_request_targetwith the pull request's head checked out: that runs a fork's config with the base repository's token.
Reports about any of these boundaries are welcome.