Bindings¶
A binding attaches an installed plugin to a lifecycle event under a mode and scope. Without a binding, an installed plugin does nothing.
The shape of a binding¶
Bindings live on disk in .tovio/plugins/bindings.toml as a [[bindings]] array. This is the exact
wire shape that parses into tovio-core's Binding type — pinned by
crates/tovio-cli/tests/plugin_spec_examples.rs.
[[bindings]]
plugin_id = "com.example.license-check"
event = "pre-land"
mode = "enforcing"
scope = "local"
required = true
paths = ["**"]
branches = ["main", "release/**"]
[bindings.version]
exact = "1.2.0"
Fields¶
| Field | Type | Required | Notes |
|---|---|---|---|
plugin_id |
string | ✓ | Must match an installed plugin's manifest id. Not plugin. |
event |
enum | ✓ | Any LifecycleEvent name the plugin's manifest events declares. No reserved names remain, so every promoted event (post-snapshot, post-land, conflict-created, git-imported, …) and transcript-parse are bindable alongside the four MVP events. |
mode |
enum | ✓ | advisory or enforcing. |
scope |
string | ✓ | Non-empty. Everything tovio writes today is local (see below). |
required |
bool | – | Defaults to false. An enforcing binding blocks on fail either way; required = true adds error/skipped/unsupported to the blocking set. |
paths |
array of globs | – | Restrict to changes touching these paths. Empty ⇒ all paths. |
branches |
array of globs | – | Restrict to these target lanes/refs. Empty ⇒ all. |
actors |
array | – | Restrict to specific actor identities/kinds. Empty ⇒ all. |
[bindings.version] |
tagged | ✓ | Version requirement — see below. |
[bindings.grant] |
table | – | The binding's approved capability grant, deny-by-default. Omit it entirely for the default (deny) set; when the table is present it must list every capability field, which is why the CLI writes it for you (--grant, --grant-read-path). |
Version requirement — tagged variant¶
[bindings.version] is a tagged variant. Use one of:
A bare version = "1.2.0" string on the binding is not valid. There is no source or
sha256 on the binding — artifact source and integrity belong on the installed plugin's manifest.
Scopes and precedence¶
Every binding tovio writes today is local
scope is part of the binding model (REQ-PLUGIN-023), and the four scope names below describe
where the model is going. Every binding the CLI writes lands in one store —
.tovio/plugins/bindings.toml, all with scope = "local", from tovio plugin bind and from
tovio policy hook add. tovio plugin bindings says so in its own output. There is a second,
separate store on the server side, but it is provisioned by a Forge operator, not by any client
command. A repo/org binding-management surface is the open piece of the plugin system.
| Scope | Where | Who sets it | Typical use |
|---|---|---|---|
local |
.tovio/plugins/bindings.toml |
The individual developer | Dev-time checks and hook authoring. Never treated as a repo/org/Forge gate (REQ-PLUGIN-025). |
repo |
(no store yet) | Repo owners, via policy | Repo-wide required checks. |
org |
(no store yet) | Org admins, via signed policy | Compliance/license gates across many repos. |
forge |
.tovio/forge/plugin-bindings.vex on the server |
Forge operators, out of band | Server-side execution: the Forge's own runner executes and self-attests these at propose/amend. |
The local store does not travel with a clone
.tovio/ is snapshot-ignored, so bindings.toml is never part of a tracked tree. A binding you
create is yours alone — collaborators' clones start with an empty store, and a client-installed
binding never reaches the Forge. That is also why the server-side store is operator-provisioned
rather than proposer-pushed, and why the scope field is a free label the runtime interprets
rather than a location.
Two more Forge-side gates are not binding scopes at all: an organization plugin allow/deny policy
served from .tovio/forge/plugin-policy.vex decides which plugin ids may back an enforcing gate
(REQ-PLUGIN-072), and a required plugin:<id> check must carry an attested, revision-stamped
passing result before a proposal can be approved (REQ-PLUGIN-069/070/084). A default tovio-forge
build has the attestation gate but no executor; the native runner is an opt-in build.
Precedence rules:
- Local advisory bindings are never a required gate. They may warn, they may block on your own
machine when you opt in, but Forge, CI, and other collaborators do not trust them
(
REQ-PLUGIN-025,REQ-PLUGIN-070). - Enforcing bindings are additive. If any enforcing binding says
fail, the operation blocks. required = truetightens what counts as blocking: it turnserror,skipped, andunsupportedinto blockers as well.- Forge trusts only what its policy trusts. A local plugin result is not an enforced gate on
the server unless a policy-approved attestation or server-side execution backs it
(
REQ-PLUGIN-069/070).
Filters¶
Filters narrow when a binding fires. They compose as an AND:
paths— the change must touch at least one matching path.branches— the target ref must match at least one glob.actors— the invoking actor must match.
Glob syntax follows the TOVIO policy glob rules (** for recursive, * for one segment).
Creating bindings¶
Three ways to create a binding, in increasing formality:
# 1. Ad-hoc, from the CLI. `bind` pins to the installed version by default:
$ tovio plugin bind com.example.license-check \
--event pre-land --mode enforcing --required \
--branches main --grant block_operation
# 2. Directly editing `.tovio/plugins/bindings.toml`, then:
$ tovio plugin bindings # inspect
$ tovio plugin doctor # sanity-check
# 3. Policy-integrated, the hook surface (always enforcing):
$ tovio policy hook add pre-land com.example.license-check \
--required --paths "**" --branches main
tovio policy hook add creates or updates an enforcing binding (REQ-PLUGIN-067), and
tovio policy hook list makes every hook inspectable by humans and agents (REQ-PLUGIN-068). It
takes the same deny-by-default grant flags as plugin bind, and — like plugin bind — it writes
into the same local store, so it is not yet a signed org-policy gate.
Inspecting bindings¶
Enforcing bindings are versioned and inspectable (REQ-PLUGIN-026). Every enforcing execution
records the binding id in the audit log, so "who required this?" is always answerable.
Manual invocation vs. bindings¶
tovio plugin run <plugin-id> --event <event> invokes a plugin on demand rather than waiting for a
host operation to fire the event — but it still goes through the binding: the plugin has to be bound
to that event, and its effective capabilities are the same manifest ∩ binding intersection. An
enforcing binding that reports fail makes run exit non-zero — required is not needed for that,
it only adds error/skipped/unsupported to the blocking set. Add --dry-run to report the
result and what would block without gating anything, which is the mode to use for testing and
one-off scans.
To run a package that is not installed at all — the author's inner loop — use
tovio plugin test <path> --event <event> instead: it runs under the manifest's own declared
capabilities, with no binding, and never gates.
Last reviewed September 9, 2026
Suggest an improvement to this page Not for security reports — see disclosure