Review skill use and proposals
For: plugin authors.
Inspect skill reads and review changes proposed by agents.
Where reads show
Each oz.skill read is a step of the job, on its home node: the job trace shows it as a skill step with the skill and its hash, and the audit trail records run.skill_read with the skill, its hash, the file it read and whether it was trusted, never its text. A JSON-lines harness session's skill load (harness-claude's Skill) is recorded the same way: the session asks the job before it loads a skill, and the job records the read, never the text.
An ACP harness that reads its skills folder itself (acp.skills) leaves no trace: its reads are not measured. The catalog lists the tenant's skills with their hashes, never their bodies:
oz0 catalog
Skills no job reads
Every skill an agent gets costs its line in the agent's index in every request, and the index shares the start-up budget: a skill nobody reads may push a skill that is read out of the index. Once a day or so, management looks at each installed skill over the last 30 days at its plugin's installed version, and when the agents that list it ran at least 50 jobs and none of them read it, it raises a finding (kind skill_unused) on the Findings page and in oz0 eval findings --kind skill_unused.
The count starts again with each update of the skill's plugin; another plugin's skill counts at the version of its own plugin. Comparisons, eval runs, rollouts' shadow runs and replays are not counted.
The finding says, first, whether the skill pushes another skill past an agent's limits.startup_tokens (the real harm), then the jobs counted, then what its line costs in each request (an estimate of the text) beside the input tokens those jobs measured. It claims no saving. Which skill was pushed out is what the agent's jobs record: each job notes the skills its node left out of the index for the budget, and the finding names one only when those jobs left it out.
For jobs from runtimes that did not note it yet, it is recomputed from the bundles and the grants as the runtime trims the index, and the finding's detail says which (pushes_from).
An agent whose reads are not measured is named, and its jobs are not counted as jobs that did not read. Each job notes whether its reads leave a trace: they always do for an agent that reads with oz.skill and for an ACP session that reads over MCP; never for an ACP harness that reads its skills folder itself; and for a JSON-lines harness only where the platform knows it loads skills with Skill and asks the job first (harness-claude), from the release that records those loads.
A harness agent's jobs from before that release, or of a harness that loads skills some other way, are not counted: the finding says how many, and on the day you upgrade it raises nothing about them.
It stays quiet about:
- a skill its author marks as read seldom, in its
SKILL.mdfront matter (the Agent Skills format'smetadatamap):skills/year-end-close/SKILL.md--- name: year-end-close description: Close the books at year end. Use only for the yearly close. metadata: oz0-usage: rare --- - a skill someone keeps:
oz0 eval resolve <id> --keep --reason "read at year end", or Keep on the finding, until the skill changes; - a skill hidden from every agent that lists it.
What to do depends on whose skill it is. The finding never does it; a person does:
| Whose | What | How |
|---|---|---|
| Your own plugin and its agent | A reviewed change: remove the skill, or narrow the agent's skills: | The finding gives the edit and where the plugin came from. For a plugin installed from the studio, Open a reviewed change writes it into your draft of that workspace folder, to save as a revision and send with Export to Git, Open a pull request; for a plugin from Git it shows the edit with the repository and folder, to change there |
| Another plugin's skill granted to your agent | Narrow that agent's grant: its skills:, or uses.<plugin>.skills, as a reviewed change in your plugin | The same; or oz0 plugin revoke <plugin> --skill <owner>/<skill> at once, for every agent of the plugin |
| Another publisher's agent and its own skill | Hide for this tenant on the plugin's Configure tab, tried first with the plugin's evals | Or oz0 plugin config export and apply with skills.<agent>.hidden |
For another publisher's skill, the finding also holds a draft to send its publisher: the skill, its version, the jobs
counted and the tokens, never a job's content. oz0 eval feedback <id> prints it with the actions; nothing sends it.
Authors see the same without anyone's telemetry: oz0 plugin inspect prints each agent's start-up cost, which the hub
shows, and the build kit's skill-ablation eval (/oz0:ablate, or oz0kit.py ablate <agent> --budget <USD>) runs your
evals once per skill withheld and names the skills that never changed a result.
Skills an agent proposes
An agent can write down what it learnt as a skill, for a person to review. It never changes a skill itself: a proposal is a reviewed change of one skill's folder, and it lands only as a draft pull request a person merges. Give the agent the object form of skills::
skills:
use: [acme-support/*] # optional: the list form's entries; left out, its own plugin's skills
propose:
into: acme-support # the plugin whose skills a proposal changes: its own, or another installed one
apply: oz0_github.push_changes # the function that opens the pull request
repository: acme/support-plugins # where that plugin's source lives
path: plugins/acme-support # the plugin's folder in that repository (the root when left out)
arguments: {branch_prefix: oz0/skill-}
The agent then gets one more tool, oz.propose_skill(name, description, body, files?), whatever its runtime: Pydantic AI and OpenAI Agents SDK agents as a tool, harness sessions as a built-in of the session. A call:
- builds a changeset of
<path>/skills/<name>/and nothing else, on the job's node, against the target plugin's files as that node has them installed:SKILL.md(written by the platform from the name, the description and the body) and at most 32 text files underscripts/,references/orassets/. A new skill adds files; an existing one changes them; nothing is deleted, and nothing is ever executable.
A proposal never touches the plugin's protected paths: evals, outcome, best-of and quality rubrics, verify and anchors files, policy files, CODEOWNERS or CI workflows. It also cannot touch the manifest, agents, their instructions or schemas, flows, or code run by hooks and tools.
The code boundary covers every file or folder named by a tool's, harness's or executor's command or environment. Each entry in a list counts separately, includingPYTHONPATH=skills/lib:vendorand comma-separated rule files. For imported plugins, it also covers hooks and servers configuration and the files they run.
These restrictions apply inside skill folders too. A skill folder containing any such file is refused whole, so no model-written file lands beside that code.
A plugin with more than 512 such files and folders, once a protected folder counts for everything inside it, gets no proposals at all rather than a partial protection; the node's log says so. A proposal never changes the manifest either, so a new skill goes only into a plugin whoseskills:listsskills/(or that skill's own folder); otherwise it is refused, since it would not load once merged. It is refused too when it holds a value of one of the tenant's secrets. - waits for a person's approval of the apply call, with the diff, as any changeset does. No policy decides it:
review: policyis refused byplugin lintand by the runtime, since a skill is text every later job of every agent that loads it reads. The approval cannot be edited: what lands is what the diff showed. Where the tenant's four eyes are on, the person who started the job may reject the proposal but not approve it. - on a yes, calls the apply function with the approval's binding.
oz0_github.push_changesapplies the patch to the repository's default branch as one commit on a branch of its own and opens a draft pull request. It applies only where the skill's files on that branch are still what the node has installed (or, for a new skill, where its folder does not exist yet); a proposal that no longer fits is refused, never forced.
The model writes the skill; the platform writes the pull request's title and body. They say where the skill came from: the job's ID and its page in the console (when the server runs with --public-url; otherwise oz0 job get <job>), the agent, its plugin and version, the plugin it goes into, the files, the job's taint and its labels.
A job that read untrusted input says so first: the title starts with [untrusted input], and the body opens with a warning that the skill may carry text someone else wrote. Read every line of such a proposal: merged, it becomes instructions for later jobs.
**Warning: this skill was proposed by a job that read untrusted input** (open_world oz0_github.get_issue). ...
| Job | `job-0192...` ([open](https://oz0.acme.se/jobs/job-0192...?tenant=acme)) |
| Agent | `support` of plugin `acme-support` 1.4.0 |
| Taint | untrusted: open_world oz0_github.get_issue |
A job proposes one skill. Every way of reviewing it (the console's changes card, oz0 changes show and oz0 changes diff) shows the job's one changeset, so a second call is refused, both while the first is being prepared and after it: the diff a person reads is always the one they approve. A call the node refused (a bad path, a protected file, a secret) does not count, up to five tries. In a routine's fire the tool is offered only when the routine lists oz.propose_skill; a shadow run proposes nothing.
Nothing reaches an agent until the pull request is merged and the plugin is updated through the tenant's usual install, gate and rollout. A skill of the agent's own plugin is as trusted as its instructions, so the review of the pull request is where a proposed skill becomes trusted. Skills that belong to one person rather than a plugin are not part of this release.