The runtime gate
skillsvault gate is the enforcement point. It evaluates a single skill invocation against the locally-synced, signed bundle and returns allow, warn, or halt — entirely in-process, with no network call and no model on the path. That is what keeps enforcement instant and working offline.
The decision is local
The gate reads ~/.skillsvault/bundle.json (written by skillsvault sync) and decides from it alone. Shipping the audit event afterwards is best-effort and off the decision path — the decision never waits on the network.
Evaluation
Given a skill name, the gate:
Resolve the skill
Split namespace/name to get the namespace, and look the skill up in the bundle's manifest_index.
Walk every matching policy
Policies arrive priority-ordered (manifest_id → namespace → declared_metadata). For each policy whose matcher matches, the gate considers its action:
namespacematches when the policy's value equals the skill's namespace.declared_metadatamatches when the skill'stargetscontain the value (the part after:in e.g.target:teamleader).
Apply the status / compliance signal
If the skill is known: banned → halt, deprecated → warn. If it is unknown but its namespace is managed, apply the bundle's managed_missing_manifest default (the non-compliance floor for sideloaded or pre-skillsvault folders).
Take the most restrictive outcome
Across every candidate, the most restrictive action wins: halt > warn > allow. A ban is never downgraded by a broader warn. Among equally-restrictive candidates the first considered (highest matcher priority, then status) keeps the explanation — its policy name and reason.
The evaluation is fast and order-independent in its result: the same skill and the same bundle always produce the same decision. The console's policy builder previews "this matches N skills" using the same matching rules, so what you see when authoring a policy is what the gate enforces. See Policies for how those rules are written.
Two modes
gate behaves differently depending on whether a human ran it or a harness hook piped a payload to it.
Human / CI mode
Pass --skill. The decision is rendered and the exit code is the protocol:
skillsvault gate --skill acme/hubspot-crm-entry
# ✓ ALLOW acme/hubspot-crm-entry@1.3.0 (exit 0)
skillsvault gate --skill acme/onboard-customer
# ⚠ WARN acme/onboard-customer — Skill is deprecated. (exit 0, stderr)
skillsvault gate --skill acme/teamleader-crm-entry
# ⛔ HALT acme/teamleader-crm-entry
# policy: Teamleader retired
# Use acme/hubspot-crm-entry instead. (exit 2)
| Decision | Stream | Exit |
|---|---|---|
| allow | stdout (suppressed with --quiet) | 0 |
| warn | stderr | 0 |
| halt | stderr | 2 |
A non-zero exit is what a generic hook treats as a veto. --quiet suppresses the allow line so a passing gate is silent.
Claude Code hook mode
When no --skill is given, the gate reads the harness's PreToolUse payload from stdin and emits a structured permission decision instead of relying on the exit code:
echo '{"tool_name":"Skill","tool_input":{"skill":"acme/teamleader-crm-entry"}}' | skillsvault gate
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "skillsvault blocked this skill — policy: Teamleader retired. Use acme/hubspot-crm-entry instead."
}
}
In hook mode the deny is the veto, not the exit code
Hook mode always exits 0. A halt is signalled by permissionDecision: "deny" with the org's reason (surfaced to the agent); a warn prints to stderr; an allow emits nothing. This is deliberate — Claude Code reads the JSON, not the exit status, for PreToolUse. Wire it with skillsvault install-hook.
Skill extraction from the payload
In hook mode the gate extracts the skill name from tool_input, most-specific first:
- explicit keys, in order:
skill,name,command,skill_name,skill_id— taken when the value matches the namespaced pattern^[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$; - otherwise, any value in
tool_inputthat looks like a namespaced skill name; - as a last resort, a bare
skill/name/skill_nameeven if not namespaced.
If a payload is present but no skill can be extracted, the gate does not block — it exits 0. This robustness is covered by the e2e suite (an unknown field still resolves the skill and denies a banned one).
The byte-clean contract
The gate's output is a protocol, so presentation never corrupts it: ANSI color is emitted only to a real terminal and is suppressed by --no-color, NO_COLOR, or TERM=dumb. Styling never alters a decision, an exit code, the stdout-vs-stderr split, or the hook JSON. Piped and hook output is always plain bytes.
Audit
After deciding, the gate ships one audit event to POST /api/audit/ingest with the skill, resolved version, harness, machine, decision (allowed / warned / blocked), reason, and matched policy. It is fire-and-forget: a failure to ship never changes or delays the decision. See Audit log.