---
title: "Pin tool definitions"
description: "Verify tool snapshots and publish a measured function listing."
canonical_url: "https://docs.oz0.ai/build/parts/tools/snapshots"
---
# Pin tool definitions

> Verify tool snapshots and publish a measured function listing.

For: plugin authors.

Verify tool snapshots and publish a measured function listing.

## Snapshots: a tool may not change what it says it does

The first node that runs a plugin version lists its tools and takes a snapshot of each function: its description, its input and output schemas, and its annotations, with a hash. For an update with evals, that is the quality gate's test node. The catalog, and the Tools and skills page, show the functions as their snapshot has them.

From then on, nodes call a function only while it lists itself the same way. A tool that starts to describe itself differently, such as a remote MCP server whose owner changed a description or a schema, is refused:

```text
team.hello lists itself differently than when its plugin's tools were approved, so it is not called; check the
change and approve it with: oz0 plugin approve-tools team
```

The plugin's other functions keep working. `oz0 plugin list` and the plugin's page in the console show which functions changed and on which node, and `oz0 catalog --json` has the new listings. Approve a change once you have looked at it:

```bash [Terminal]
oz0 plugin approve-tools team
```

Nodes call the functions again within seconds, without restarting. The audit log records the snapshot, each change and each approval (`plugin.tools.snapshot`, `plugin.tools.changed` and `plugin.tools.approve`). A new version of the plugin gets a snapshot of its own.

A node calls no plugin's function before it holds the snapshots: one that just joined, or started again, waits for the edge's plugin set. A node that starts again takes the snapshots of the plugin versions it already runs from that set before it installs anything, so they are called while another plugin's update installs; the version being replaced, and one no longer assigned, are not called meanwhile (`its plugin's version ... is installing on this node`).

An update that fails leaves the running version under the snapshot it was approved with; if the node started again while that update was pending, the version stays refused until the update is fixed or the version is assigned again.

### What the snapshot shows

Text people do not see cannot hide in a listing. Before a node hashes a function's listing, and before a model reads it, every invisible code point in it (Unicode TAG characters, zero-width characters and joiners, bidi controls, variation selectors, other format and control characters, private-use code points) is written as a visible escape such as `\u{E0041}`. The model reads the same text people approve, the catalog and the console show the escapes, and a server that adds hidden text to a description changes its listing, which waits for approval.

A listing without such characters hashes as it did before. Approval cards in the console, chat threads, the CLI and MCP clients show the arguments of a call the same way.

A node also leaves out a function whose name could pass for another's, and says so in `oz0 plugin list`:

- a name outside MCP's tool-name characters (letters, digits, `_`, `-` and `.`, at most 128), such as one that starts with a byte order mark;
- a name its server lists twice;
- two functions a model would see under one name (`a.b__c` and `a__b.c` both reach models as `a__b__c`).

The tool ID `oz` is the platform's own (`oz.delegate`, `oz.ask_user`, ...): no plugin's tool or harness may take it, and a node refuses one from an older bundle. A call of a function its server did not list is refused too.

### Remote endpoints are pinned

A remote tool (`url:`) is pinned at its origin, `scheme://host[:port]`, the first time a version of its plugin lists it. A later version whose URL names another host, such as an MCP Registry server that moved its endpoint under the same name, gets its other functions at once, and that tool's functions wait for approval:

```text
Tools of weather changed on node-1: wx.today. Nodes do not call them until you approve: oz0 plugin approve-tools weather
  endpoint moved: wx: https://mcp.example.com -> https://mcp.other.example
```

A remote tool whose host is a connection's param is not pinned this way: its host is the one the connection was connected for (see [Remote servers on connections](https://docs.oz0.ai/build/parts/tools#remote-servers-on-connections)).

`oz0 plugin approve-tools` pins the new origin for that version. Each version keeps its own pins: when the version that moved is a candidate (a canary or a rollout), approving it leaves the installed version calling the origin it was approved at, and once the candidate is installed, its origin is where the next version starts. Another path on the same origin is no move, and the MCP client follows a redirect only within the origin, so a server cannot move its calls by redirecting them.

<note>

The snapshot catches change, not a tool that misdescribes itself from the start: read a tool's listing before you install it.

</note>

## The measured listing

`plugin inspect`, `plugin lint`, the studio and the hub run no plugin code, so on their own they cannot say what your servers list. `plugin test` can: it starts them anyway, and writes what each one listed into `tools.listing.json` at the plugin's root, exactly as a node records it for the snapshot (each function's listing and its hash). Commit the file. `plugin pack --list-tools` writes it without running evals:

```bash [Terminal]
oz0 plugin test ./acme-github          # also writes tools.listing.json
oz0 plugin pack ./acme-github --list-tools
```

```text [Output]
Wrote tools.listing.json: 12 functions as the test node listed them; commit it, so inspect and the hub can show them
```

The file also records a digest of the plugin's other files, leaving out the ones the tools write from them (the cost-amplification probe's `tools.amplification.json` and the SBOM `sbom.cdx.json`). When they change, `plugin lint`, `plugin pack` and `plugin inspect` say the listing is stale; run `plugin test` again.

The first node's snapshot stays the authority. When it differs from the version's measured listing, nothing is blocked (the snapshot already decides what nodes call), but `plugin list` and the Plugins page name the functions that differ, and the audit log records `plugin.tools.listing_mismatch`: either the listing is stale, or the server describes itself differently on a node than on your machine.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
