Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/hook-speed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'onsystem': patch
---

`check` on one file starts sooner, which the Claude Code hook does on every edit. The CLI turns on Node's compile cache (Node.js 22.1 and later), so the CLI and TypeScript are not compiled again on each run; it is kept in the user's own folder in the system temp folder, `onsystem-<uid>`, in a `compile-cache` folder onsystem creates for the user alone. An app whose design system zero config finds in a package's `exports` keeps what it found beside the cached model, and reads it from there until a file in that package, or the app's `package.json` or `components.json`, changes, or another build of onsystem runs: following the package's barrels parsed every file they lead to, on every run. `--no-cache` finds it again. On midday's dashboard and dub's web app, `check` on one file went from 0.51 s to 0.33 s and from 0.77 s to 0.43 s.

In a project without onsystem installed, the hook runs it through npx on the first edit in that project only: the CLI npx started says where it is, and the next edits there run it with Node directly, without starting npm, while it is still the plugin's release. When that CLI gives no results, as when part of npm's copy was removed, the hook forgets it and runs npx in that same edit.

`onsystem-<uid>` is used only while it is the user's alone. One that others can write to is now skipped, for the cached model too, rather than closed and used, and on Windows the folder is used only inside the user's home folder. SECURITY.md lists what is kept there.
4 changes: 3 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ It verifies the registry signature of every installed package and the attestatio
- The server runs locally over stdio, opens no ports and makes no network requests of its own. `npx` downloads 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. In `node_modules` it 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-baseline` or `check --prune-baseline`, and the file `export --out` names. `init` writes `onsystem.config.json`; in a design-system package also `ONSYSTEM.md` and the `onsystem` field (and `files`) of `package.json`; and, when you say yes, an onsystem section in `AGENTS.md` and `CLAUDE.md`.
- The cache is in `node_modules/.cache/onsystem`. A project without `node_modules` has 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.
- The cache is in `node_modules/.cache/onsystem`: the model it built from the design system (`model.json`) and, when zero config found the design system in a dependency's `exports`, where it found it (`detection.json`), which decides what is read as the design system. A project without `node_modules` keeps both in a folder of the system temp folder that only you can open, `onsystem-<uid>` (`onsystem` on Windows). The CLI keeps Node.js's compile cache for its own code there too (`compile-cache`), which Node.js runs, and the Claude Code hook keeps the path of the CLI npx installed there ([below](#the-claude-code-plugin)). `--no-cache` reads neither the model nor the detection.
- onsystem creates that folder, and `compile-cache` in it, for you alone (mode 0700), rather than leave `compile-cache` to Node.js, which creates folders with your umask. It uses each only while it is a folder, not a link, that you own and no one else can write to. One that others can write to is neither read nor written, rather than closed for you, since a file put in it before would stay. On Windows, which has no such owner to check here, it is used only when it is inside your home folder, links resolved, where Windows keeps each user's temp folder: with `TEMP` pointing anywhere else, the CLI runs without these caches and the hook without the remembered path.
- 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_modules` folder 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, and `inspect` and 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. The `node_modules` rule is the trade-off for pnpm's store: a link committed inside `node_modules` is 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 in `get_component`.
- `check_ui` with a `path` reads only files inside the project root, symlinks resolved, and limits input size.
Expand All @@ -46,6 +47,7 @@ The plugin's hook and server run in every project you open in Claude Code, so us
- Both import the project's `onsystem.config.ts`, `.mjs` or `.js`, as above.
- The hook runs the project's own install of the CLI when there is one (`node_modules/onsystem`, whose `bin` it runs with Node.js), whatever its version. Without one, it runs `npx` with the exact version the plugin was released with. The server runs `npx -y onsystem@<that version>`, which uses a project install of the same version.
- `npx` runs in the project folder, so the project's npm config applies to it, such as a `registry` in its `.npmrc`.
- Without a project install, the hook runs `npx` on the first edit in a project. The CLI that `npx` started writes its own path to a file in `onsystem-<uid>`, `npx-cli-<key>`, one per release, `npx` and project folder, and the next edits in that project run that script with Node.js directly, without npm. The hook runs it only while the folder is yours alone, as above, the file is a file, not a link, that no one else can write to, and the script is the `bin` of an `onsystem` package at the plugin's exact version; otherwise it runs `npx` again. When that script gives no results, as when part of npm's copy was removed, the hook removes the file and runs `npx` in the same edit. The CLI writes that file only under that name directly in its own folder, readable by you alone (mode 0600), and replaces a link there rather than writing through it.
- The hook writes marker files in the system temp folder, so its notice is shown once per session and its pause after a failed `npx` holds.

## The GitHub Action
Expand Down
2 changes: 1 addition & 1 deletion docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Run as is, with no config, on public apps: the counts the [corpus](../corpus) sn

## Performance and footprint

Reading the design system is the slow part: one TypeScript program over the component files. `check` and the server cache what they read and read it again only when those files change, so checking a file after that is syntax work. The hook runs one `check` per edit to a UI file. Without onsystem installed in the project, it runs `check` through npx, which starts npm first: a dev dependency ([pin the version](rollout.md#pin-the-version)) takes that out of every edit.
Reading the design system is the slow part: one TypeScript program over the component files. `check` and the server cache what they read and read it again only when those files change, so checking a file after that is syntax work. The hook runs one `check` per edit to a UI file. Without onsystem installed in the project, it runs `check` through npx on the first edit in a project, which starts npm first; later edits there run the CLI npx installed with Node.js directly ([how](plugin.md#what-it-runs-and-what-it-touches)). A dev dependency ([pin the version](rollout.md#pin-the-version)) takes npm out of the first edit too.

Measured with onsystem 0.11.0 on Apple M1 Max (10 cores, 32 GB), macOS 27.0.1, Node.js 24.18.0, on 2026-10-29, with `node scripts/perf.mjs`: each time is the median of 5 runs. The corpus checkouts have no `node_modules`, so the types of the libraries their components wrap are not read: in a project with its dependencies installed, reading the design system takes longer. The demo, where the hook is measured, has its dependencies installed.

Expand Down
4 changes: 2 additions & 2 deletions docs/plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ The plugin is the fastest way to keep Claude on your design system. It bundles t

## What it runs, and what it touches

The hook and the server run the `onsystem` package at the plugin's exact version: the project's own install when there is one (`npm install --save-dev onsystem`), else `npx`. Both run through Node.js directly, never a shell. They read the project's files and write only the cache (`node_modules/.cache/onsystem`, or the system temp folder) and marker files in the system temp folder, so the hook's notice is shown once and its pause holds. onsystem makes no network requests and no model calls. The only network access is npm's, when it runs through npx:
The hook and the server run the `onsystem` package at the plugin's exact version: the project's own install when there is one (`npm install --save-dev onsystem`), else `npx`. Both run through Node.js directly, never a shell. They read the project's files and write only the cache (`node_modules/.cache/onsystem`, or the system temp folder, with the CLI's compile cache) and, in the system temp folder, marker files, so the hook's notice is shown once and its pause holds, and the path of the CLI npx installed. onsystem makes no network requests and no model calls. The only network access is npm's, when it runs through npx:

- **The hook** runs `npx --prefer-offline`, which uses the package in npm's cache without asking the registry: after the first download it checks offline too. If npm itself fails there, as when its cached list of versions predates a new release of the plugin, it runs once more with `--prefer-online`, which asks the registry. When the CLI ran and failed, as with a broken config, the hook says so at once, without a second run.
- **The hook** runs `npx --prefer-offline` on the first edit in a project, which uses the package in npm's cache without asking the registry: after the first download it checks offline too. If npm itself fails there, as when its cached list of versions predates a new release of the plugin, it runs once more with `--prefer-online`, which asks the registry. When the CLI ran and failed, as with a broken config, the hook says so at once, without a second run. The CLI npx started says where it is, and the next edits in that project run it with Node.js directly, without starting npm, while it is still the plugin's release. When npm's cache no longer has it, or it gives no results, as when part of npm's copy was removed, the hook forgets it and runs npx in that same edit. That path is kept in a folder of the system temp folder that only you can use ([SECURITY.md](../SECURITY.md), under "The Claude Code plugin", says what the hook trusts).
- **The server** runs `npx -y onsystem@<version>`, which asks the registry about the package each time it starts. Offline, it starts from npm's cache only after npm gives up on the registry, over a minute later.
- **A project install** needs no network and is the fastest: the hook runs it without npm, and the server's npx uses it when it is the plugin's version.

Expand Down
Loading
Loading