Skip to content

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:

[bindings.version]
exact = "1.2.0"          # pinned; recommended for enforcing bindings (REQ-PLUGIN-027)
[bindings.version]
at_least = "1.2.0"       # minimum-version floor

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 = true tightens what counts as blocking: it turns error, skipped, and unsupported into 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

$ tovio plugin bindings --json

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