scriptable: just the output, no routing UI
September 13, 2026 · View on GitHub
veto
Cost-aware AI model routing with explicit model admission.
Stop hardcoding which AI model gets every task.
Website · Latest release · Architecture
Veto is a scriptable model router for developers using multiple AI providers. It filters candidates by tool access, context, task complexity, and estimated cost, then asks the remaining models for a structured accept/reject decision. The first model to accept with at least 70% confidence is selected.
Example routing trace (values depend on the configured providers and their responses):
$ veto route "refactor the auth middleware to use JWT" --kind refactor --risk medium
Routing: "refactor the auth middleware to use JWT"
kind: refactor · risk: medium · complexity: simple
── Filtering candidates ──────────────────────────────────
haiku pass
sonnet pass
opus pass
── Asking models ─────────────────────────────────────────
haiku ✗ TASK_KIND_OUTSIDE_STRENGTHS
sonnet ✓ accepted 94% confident · ~\$0.0023 · ~1200 tokens
→ Selected: sonnet (mid tier)
brew install oleg-koval/tap/veto
veto doctor
veto providers
veto route --json "summarize this pull request"
Features
- Cheapest viable model first — deterministic capability, complexity, and cost filters run before model admission.
- Explicit admission — candidates return structured accept/reject decisions, confidence, estimated usage, and rejection reasons.
- Multi-provider routing — Anthropic, OpenAI, OpenRouter, xAI, Ollama, LM Studio, and other OpenAI-compatible endpoints can participate.
- Route or execute — select a model with
veto route, run a task withveto run, or route each step of a plan withveto exec. - Automation-friendly output — quiet and JSON modes support scripts and agent infrastructure.
- Fail-closed review — optional acceptance criteria reject unavailable, malformed, incomplete, or inconsistent reviews.
Native dispatch experiment
The existing TUI and CLI also expose a deliberately small native-agent experiment. Veto never becomes the coding agent: Claude Code or Codex keeps its own permissions, tools, MCP, sessions, authentication, environment, and stdin/stdout/stderr.
# Manual control: Veto does not choose
veto start --agent claude "Fix the failing parser test"
veto start --agent codex "Fix the failing parser test"
# H2: choose the native agent with a transparent fixed policy
veto start --choose agent "Fix the failing parser test"
# H1: keep the agent fixed and choose only its model
veto start --choose model --agent claude "Fix the failing parser test"
veto start --choose model --agent codex "Fix the failing parser test"
The policy uses task kind, installed/authentication state, explicit settings,
and temporary availability. It does not use model self-admission, historical
quality scores, embeddings, or learned routing. Every proposal prints its
reason, constraints, excluded alternatives, unknowns, and that history was not
used. Use --override-agent or --override-model for an immediate CLI
override; the TUI shows the same proposal before launch.
The native status is intentionally conservative. authenticated,
subscription, API, capacity, and cost are separate facts. Claude billing
is shown as UNKNOWN when Veto cannot verify whether the native CLI uses a
subscription or an inherited ANTHROPIC_API_KEY; Veto preserves that
environment variable. Codex ChatGPT-plan billing is also not treated as free
capacity evidence.
Temporarily exclude an agent without quota scraping:
veto unavailable claude --for 2h
veto unavailable codex --for 30m
veto unavailable
veto unavailable claude --clear
Native-dispatch events are local, bounded, and stored in
~/.veto/experiment.log. They contain timestamps, anonymous local run IDs,
mode, choices, reason metadata, process status, and optional usefulness
feedback. Prompts, source code, repositories, transcripts, responses, and
credentials are not recorded by default. Inspect or delete the log with
veto experiment and veto experiment --clear.
In the TUI, press s or choose Start native task from the palette. The
home/provider views show native executable, authentication, billing, warning,
and temporary-unavailability state; the review screen explains the proposal
before Bubble Tea releases the terminal to the child process.
This experiment does not guarantee task correctness, verified success, known cost, capacity, or product-market fit. A native process exit of zero means only that the process returned successfully; it is not an objective coding outcome.
Why veto exists
As model rosters multiply, every multi-model workflow faces the same decision: which model should get this task? Hardcoded rules, manual selection, and keyword routing miss context that matters, while sending everything to the largest model wastes capacity and money.
Veto combines deterministic filters and ranking with model self-assessment. Each surviving candidate receives the task spec and returns structured admission data: accept/reject, confidence, estimated tokens and cost, and rejection reasons. Veto records the decision trail and can emit it as JSON for scripts and agent infrastructure.
Candidates are ranked cheapest-viable-first, but self-reported confidence and cost are estimates rather than guarantees. Use the offline benchmark to inspect routing mechanics and real-provider trials to evaluate quality for your own workloads.
License
Veto is released under the Apache License 2.0. See the license file for the permissions, conditions, and disclaimer.
Installation
Homebrew (macOS)
On macOS, Homebrew installs the checksum-pinned release:
brew install oleg-koval/tap/veto
veto version
veto doctor
Arch Linux (pacman)
Release assets include an Arch package for x86_64. Download the matching
veto-bin-<version>-1-x86_64.pkg.tar.zst file and PACKAGE_SHA256SUMS from the
same release, verify the package-specific checksum, and install it with pacman:
package="veto-bin-<version>-1-x86_64.pkg.tar.zst"
awk -v file="$package" '\$2 == file { print }' PACKAGE_SHA256SUMS | sha256sum -c -
sudo pacman -U "./$package"
veto version
Debian or Ubuntu (apt)
Release assets include Debian packages for amd64 and arm64. Download the
matching package and PACKAGE_SHA256SUMS from the same release, verify the
package-specific checksum, and install the package locally with apt:
package="veto_<version>_amd64.deb"
awk -v file="$package" '\$2 == file { print }' PACKAGE_SHA256SUMS | sha256sum -c -
sudo apt-get install "./$package"
veto version
These are local release packages. AUR and signed apt-repository publication remain separate registry-maintainer steps.
Release archive
Veto is currently a public beta. Download the latest archive for your operating system and CPU from GitHub Releases, extract veto, and put it on your PATH (for example, ~/.local/bin). Archives cover Darwin, Linux, and Windows on amd64 and arm64.
Download the matching SHA256SUMS file and verify the archive before extracting:
version=VERSION # replace VERSION with the latest release number, without "v"
asset="veto_${version}_linux_amd64.tar.gz"
awk -v asset="${asset}" '\$2 == asset {print}' SHA256SUMS > "${asset}.sha256"
test -s "${asset}.sha256"
sha256sum -c "${asset}.sha256"
tar -xzf "${asset}"
install -m 0755 "veto_${version}_linux_amd64/veto" ~/.local/bin/veto
veto version
veto doctor --json
On macOS, select the matching Darwin archive and pipe the checksum line to
shasum -a 256 -c - instead.
SHA256SUMS covers release archives; BINARY_SHA256SUMS lets veto doctor
verify an extracted official binary. These hashes detect corruption and release
mismatches; they are not cryptographic signatures.
Build with Go
Go 1.26.6 or newer is required:
go install github.com/oleg-koval/veto/cmd/veto@latest
Or clone and build:
git clone https://github.com/oleg-koval/veto
cd veto
go build ./cmd/veto
go install and local builds are supported source-build paths. They report the
module version when available, but veto doctor does not present them as
checksum-verified official release binaries.
Upgrade and uninstall
On every interactive launch, veto consults its update state and refreshes the
latest complete GitHub release at most once every 24 hours. When a newer stable
version exists, it asks before changing anything. Homebrew installs run
brew upgrade oleg-koval/tap/veto; versioned Go installs use the exact
go install version; official standalone binaries require both checksum
manifests before an atomic replacement. The original command does not continue
after an accepted update, so re-run it with the new binary. JSON, quiet,
non-interactive, development, and offline-failed checks never prompt or block.
To upgrade manually, run brew upgrade oleg-koval/tap/veto for Homebrew. For a
release archive, download and verify the newer archive, then replace the binary
in the same PATH directory. For corruption of an official release binary,
veto doctor --fix can reinstall that exact version when the executable is a
writable, unmanaged regular file. It refuses symlinks, package-manager paths,
source/Go-install builds, and unwritable targets. On Windows it leaves a
verified staged replacement and prints the exact manual replacement step. Your
provider credentials, local-model definitions, skills, plans, checkpoints, and
logs under ~/.veto/ are retained.
To uninstall, remove only the binary first (rm "$(command -v veto)"). To also remove veto's local state, back up anything you need and then remove ~/.veto/; this deletes stored credentials, models, skills, plans, checkpoints, and logs.
Quick start
1. Discover existing providers and harnesses:
veto providers
If a provider is not already available, run veto login to connect an
API-key provider or add a local model. Native Claude and Codex sessions are
discovered from their own CLIs and do not require a second Veto login.
If you explicitly choose to connect Anthropic, veto asks whether you use a subscription (Claude Max / Pro) or an API key:
- Subscription mode — if you have Claude Code installed and logged in, veto shells out to
claude -pinstead of hitting the API. Veto reports billing as UNKNOWN unless the execution mode is directly verifiable; an inheritedANTHROPIC_API_KEYcan affect native CLI behavior. - API key mode — standard pay-per-token via the Anthropic API.
For Claude, veto discovers an existing claude auth status --json session and
uses the native CLI directly; it does not ask you to log in again. The legacy
subscription option still saves a CLAUDE_SUBSCRIPTION=true marker for
backward compatibility. For API key mode, it opens the keys page in your
browser and stores the key (masked input) at ~/.veto/credentials.json (mode
0600).
Veto also detects an installed Codex CLI whose codex login status succeeds.
Native CLI sessions are discovered automatically; veto login is only needed
for API-key providers or explicit local-model configuration.
It registers the codex agent automatically. A ChatGPT login is authenticated,
but its billing and capacity remain UNKNOWN to Veto; an API-key or unrecognized
login also keeps cost unknown rather than pretending it is free. OpenAI API models remain
a separate, text-only provider path configured with OPENAI_API_KEY.
For OpenRouter, veto login recommends browser authorization. Veto binds an
ephemeral 127.0.0.1 callback, uses S256 PKCE plus an unguessable callback-path
nonce, exchanges the one-time code, and stores only the returned API key. The
flow times out after two minutes and can be cancelled safely. Manual API-key
paste remains available as option 2. veto logout OPENROUTER_API_KEY removes
only Veto's stored credential; it does not alter other keys in your OpenRouter
account.
To reuse providers already connected in OpenCode, choose option 6 or run:
veto opencode connect # CLI fallback
veto opencode connect --server http://127.0.0.1:4096 # attach
veto opencode connect --managed # Veto-managed server
veto opencode status --json
veto opencode plugin install # OpenCode-side routing
Attach mode accepts only an explicit HTTP loopback URL and never scans ports.
It obtains connected provider/model metadata from OpenCode's local API. CLI
fallback uses the exact opencode executable found on PATH and the documented
--version and models commands. Managed mode starts a password-protected
loopback server to validate compatibility, then records that Veto should own
the server lifecycle. Veto never reads or copies OpenCode's provider credential
files. veto opencode disconnect (or veto logout opencode) removes only the
Veto connection entry.
Once connected, OpenCode models join normal routing as
opencode:<provider>/<model> bindings. veto route, veto run, and plan steps
can admit and execute them without copying provider credentials into Veto. Each
admission and execution starts a fresh internal session with an opaque
veto:admission:* or veto:execution:* title; server sessions are deleted when
the call ends. Server mode streams documented SSE events, while CLI fallback
uses opencode run --format json --model provider/model. Veto never passes
--auto, --yolo, or --dangerously-skip-permissions. Admission denies tools
so a routing probe cannot act. Execution keeps OpenCode's existing permission
policy, but any new approval request is rejected because Veto does not yet
provide an interactive approval surface.
To make Veto the model selector inside OpenCode, install the embedded local integration and restart OpenCode:
veto opencode plugin install
veto opencode plugin status
Each genuine new user turn is routed through the connected OpenCode runtime by
default. The selected exact provider/model is applied before OpenCode sends
the prompt, and a toast shows the decision. /veto-off disables automatic
routing for the current session, /veto-on restores it, /veto-status shows
the current mode, and /veto-route <task> routes one task explicitly even when
automatic mode is off. The veto_status and veto_route tools expose the same
safe inspection path to the agent; status verifies Veto availability/version
without loading providers or returning executable paths.
Veto-owned sessions, inherited routing subprocesses, and synthetic internal
continuations bypass the plugin, preventing recursive admission loops. Routing
has an eight-second default timeout and fails open: on timeout, invalid output,
or no candidate, OpenCode keeps its current model and shows a warning. The
session switch is intentionally in memory and resets when OpenCode restarts.
Set VETO_OPENCODE_AUTO=0 to default all sessions off,
VETO_OPENCODE_TIMEOUT_MS to a value from 1000 through 30000, or
VETO_BINARY to an exact Veto executable. The integration never intercepts or
answers OpenCode permission requests. plugin uninstall removes only files
that still match the running Veto build; modified files are preserved.
To use Veto natively inside Hermes Agent, install the embedded plugin, validate it with Hermes' runtime doctor, and enable it explicitly:
veto hermes plugin install
hermes plugins doctor veto --ci
hermes plugins enable veto --no-allow-tool-override
After restarting Hermes, /veto, /models, /route <objective>,
/cost <objective>, and /veto-off are available. Automatic routing now runs
for external user turns through Hermes' pre-agent turn middleware. Use
/veto-off (or /veto off) to disable it for the current session, /veto on
to re-enable it, and /veto pin <provider> to pin a provider. The plugin also registers
the veto_status, veto_route, veto_run, veto_models, veto_cost, and
veto_cancel tools.
The plugin never reads Hermes credentials or overrides built-in tools. It runs
Veto without a shell, bounds time and output, marks plugin-originated calls to
prevent future recursion, and returns structured errors when Veto is absent or
incompatible. Internal calls and tool continuations bypass automatic routing;
provider-resolution or Veto failures fail open to Hermes' configured route.
Disable it with hermes plugins disable veto before
veto hermes plugin uninstall; uninstall removes only unchanged Veto-owned
files and does not edit Hermes configuration.
For local / self-hosted models, choose option 5. veto guides you through three paths:
- Ollama — veto checks if Ollama is installed, lets you pick a model from a curated list (Qwen 2.5 Coder, Llama 3.2, Mistral), pulls it, and registers the model automatically. At inference time, if
ollama serveisn't running, veto starts it in the background and waits up to 5s for it to become ready — no manual server management needed. Install Ollama from its official distribution instructions; veto does not execute a remote install script. - LM Studio — walks you through starting the server manually, then collects the model id.
- Manual — enter endpoint URL and model id directly (works with any OpenAI-compatible server: vLLM, llama.cpp, etc).
The model is stored in ~/.veto/models.json and participates in all routing calls at $0 inference cost. Local/OpenAI-compatible HTTP transports are text-only: they can return content but cannot read files, write files, run commands, or invoke tools through veto. After each local model is added, veto asks if you want to add another — you can register as many as you like in a single veto login session.
To remove a provider or local model: veto logout (interactive) or veto logout <name> (non-interactive).
You can also set environment variables directly:
# API key mode
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
export OPENROUTER_API_KEY=sk-or-...
export XAI_API_KEY=xai-...
# Subscription mode (Claude Max / Pro — requires claude CLI logged in)
export CLAUDE_SUBSCRIPTION=true
2. Check what's connected:
veto providers
veto providers also shows Grok status when XAI_API_KEY is set.
provider status models
────────────── ────────────── ──────────────────────
Anthropic veto login Claude Haiku, Sonnet, Opus
OpenAI not set run 'veto login'
OpenRouter not set run 'veto login'
xAI (Grok) not set run 'veto login'
To verify that an account can actually use every catalog ID for one provider,
run the opt-in account check (it makes one model-list request and saves the raw
response under artifacts/http/):
veto verify-models --provider openai
veto verify-models --provider anthropic
veto verify-models --provider openrouter
veto verify-models --provider xai
Use --json in automation. A nonzero exit status means the request failed or
at least one catalog ID was not returned by the account. The command never
prints or stores the API key; saved metadata contains only the endpoint,
status, timestamp, and response size. Claude subscription mode cannot be
verified through this API check because it uses the local claude CLI.
3. Diagnose the installation:
veto doctor
veto doctor --json
veto doctor --offline
veto doctor --fix
veto doctor checks the executable, PATH precedence, build version and
provenance, official release checksum, ~/.veto ownership/permissions and
symlink shape, managed JSON, the OpenRouter catalog cache, local-model
definitions, approved skill paths, and configured claude/ollama
dependencies. It also reports whether an optional OpenCode CLI/runtime is
configured and compatible; CLI and managed checks run only opencode --version, while attach mode contacts only its explicit loopback endpoint. It
does not call provider APIs, run model inference, validate credentials, or
print credential values. --offline
skips GitHub integrity lookup. --fix creates missing managed directories,
corrects safe permissions, and can reinstall a corrupted official binary; it
never rewrites malformed configuration, changes login state or skill
approvals, or removes PATH duplicates. Warnings do not fail the command;
unresolved failures exit 1.
4. Run a task:
# route and execute — prints the model's response
veto run "extract all TODO comments from the codebase"
# route only — prints the selected model name
veto route "extract all TODO comments from the codebase" --kind extract
Use Veto from coding agents
Compatible coding agents can discover the repository-local
$veto-routing skill. It teaches agents
when to use route, run, exec, and acceptance criteria while preserving
provider privacy, cost, transport, output, and authorization boundaries.
AGENTS.md contains the repository-wide engineering invariants and
points agents to the skill. Within this checkout, agents with project-skill
discovery can select it automatically or invoke it explicitly as
$veto-routing.
OpenCode
Runtime connection (veto opencode ...) and agent-side skill discovery are
separate. The runtime connection lets Veto discover, route, and execute
OpenCode's connected models; the skill below teaches OpenCode when to call Veto
from the other direction.
For automatic model selection and native commands/tools, install the local
integration with veto opencode plugin install. OpenCode discovers the shared
.agents/skills/veto-routing/SKILL.md directly;
no duplicate .opencode/skills copy or project configuration is required. Run
OpenCode from this checkout and ask it to use the veto-routing skill:
Use the veto-routing skill to route and execute this as a low-risk planning task: ...
Verify discovery without making a model call:
./scripts/agent-skill-smoke.sh
For use outside this checkout, install the catalog into a global skill location
that OpenCode scans, such as ~/.agents/skills. See OpenCode's
Agent Skills documentation for its supported
project and global locations.
Hermes Agent
Hermes loads the native integration from ~/.hermes/plugins/veto after the
operator enables it. Use veto hermes plugin status to compare installed files
with the current Veto build, and /veto inside Hermes to verify plugin API
compatibility. Explicit route/run tools use Veto's configured providers; they
do not copy or inspect Hermes provider credentials. Automatic Hermes model
rewriting is intentionally deferred to the next middleware task.
The same skill is distributed as
olko:veto-routing
in the olko-skill-meta marketplace plugin. For Claude Code:
/plugin marketplace add oleg-koval/agent-skills
/plugin install olko-skill-meta@olko-agent-skills
Codex and other supported agents can install the catalog by following the
agent-skills installation instructions.
After installation, invoke the skill as $veto-routing or olko:veto-routing,
depending on the agent's skill lookup convention.
Loops and goals
Veto provides bounded orchestration rather than a general autonomous goal
loop. veto exec runs a finite plan with ordered dependencies and per-step
routing; veto run --criteria adds one independent acceptance review; and
routing tries a bounded candidate set with checkpoint resume. Veto does not
persist an open-ended goal, invent follow-up steps, or repeatedly revise output
until it passes. An outer coding agent may run such a loop, but it must own the
stop condition, authorization, and validation.
Commands
| Command | What it does |
|---|---|
veto login | Connect a provider interactively (browser + masked key) |
veto logout | Remove a configured provider or local model |
veto run "..." | Route a task and execute it — prints the model's response |
veto exec <plan.md> | Execute a multi-step plan file, routing each step |
veto route "..." | Route only — prints the selected model name, no execution |
veto disable <model...> | Exclude one or more models from all routing |
veto enable <model...> | Re-include a previously disabled model |
veto setup | Discover and approve skill files from your skill directories |
veto providers | Show which providers are configured and how |
veto models [--json] [--offline] | List effective models, runtimes, tool knowledge, and known/unknown prices |
veto benchmark | Replay the offline routing corpus and emit JSON metrics |
veto verify-models | Verify catalog IDs against one provider account |
veto doctor | Diagnose installation and local-state integrity; optionally repair safe problems |
veto feedback | Save a redacted report and prepare a GitHub issue |
veto analytics status | Show local diagnostics and future remote-sharing preference |
veto analytics enable|disable | Set the explicit preference for any future remote analytics export |
veto opencode <connect|status|disconnect> | Manage an OpenCode runtime connection without copying credentials |
veto opencode plugin <install|status|uninstall> | Manage automatic routing, commands, and tools inside OpenCode |
veto hermes plugin <install|status|uninstall> | Manage the explicit native Hermes plugin without changing Hermes provider settings |
veto version | Print veto version |
veto install-git-hook | Add veto to your git workflow |
veto run flags
Route and execute in one step. The winning model's response is printed to stdout. Streaming output is used automatically when the executor supports it (for example, Claude or Codex subscription CLIs).
| Flag | Default | Description |
|---|---|---|
--kind | (auto-detected) | Task type (see below) |
--risk | medium | Impact level: low, medium, high |
--max-cost | 0 (no limit) | Estimated preflight cost ceiling in USD |
--timeout | 2h | Total timeout (routing + execution) |
--admission-timeout | 60s | Per-model admission timeout |
--quiet | false | Suppress routing animation — print model output only |
--max-output-tokens | 8192 | Bounded output budget for the execution response |
--output | (none) | Explicit relative file path for saving output |
--force | false | Allow --output to replace an existing file |
--criteria | (none) | Comma-separated acceptance criteria; a review pass runs after execution |
--no-feedback | false | Disable the opt-in post-run feedback prompt |
# route and execute, full pipeline visible
veto run "summarize the last 10 git commits"
# scriptable: just the output, no routing UI
veto run --quiet "extract all TODO comments" > todos.txt
# save explicitly; paths must stay inside the current directory
veto run --output todos-output.txt "extract all TODO comments"
# increase the bounded response budget and explicitly allow replacement
veto run --max-output-tokens 16000 --output summary.md --force "summarize this PR"
# auto-review the output against criteria (exits 1 if any criterion fails)
veto run "refactor the auth middleware" \
--criteria "no third-party JWT dep,all existing tests pass,function names unchanged"
veto run makes two distinct calls when needed. Admission is a short JSON-only probe capped at 512 output tokens. Claude CLI admission runs in safe mode with tools disabled; Codex CLI admission uses an isolated read-only temporary workspace without user config or rules. Both use native JSON schema output. Execution remains a separate agent run in the caller's working directory. Acceptance-review admission receives only the work shape, criteria count, and approximate payload size; the full generated output is sent once, to the selected reviewer. Codex execution is ephemeral, ignores unrelated user configuration, and uses a 65,536-token automatic-compaction ceiling to bound its active working context. Codex and OpenCode do not expose a portable per-prompt output-token limit, so their adapters use command timeouts and bounded event streams; Codex rejects custom --max-output-tokens values rather than silently ignoring them. If a provider reports that execution reached its output limit, Veto exits non-zero and does not save or review the partial output. Increase --max-output-tokens where the transport supports it and retry.
Codex reports gross input processed across its internal agent turns. This can be much larger than the initial objective because the CLI replays working context for tool calls. When Codex reports cached input, Veto shows it separately as exec reused; exec fresh is gross input minus reused input. These counters describe the last execution only, not admission or acceptance-review usage, and do not imply per-token subscription billing.
Objectives that explicitly ask Veto to modify, commit, or push repository work require an executable agent runtime. Known text-only API and local transports are filtered before admission so they cannot consume the bounded admission attempts. Agent runtimes whose tool set is discovered only at execution time remain eligible.
For tasks that fix pull-request review comments, the execution prompt requires a live inline-thread audit before and after the changes. This avoids false “no findings” results from PR summary views that omit GitHub review threads.
--output is the only way for veto run to write a file. The path must be relative to the current directory, cannot traverse upward or target hidden files/directories, and is created with mode 0600. Existing files are protected; pass --force to replace one. Objective text such as “save as report.md” does not write a file by itself.
veto exec flags
Execute a multi-step plan file. Each step is routed independently to the best model and executed. Steps run sequentially.
| Flag | Default | Description |
|---|---|---|
--dry-run | false | Print steps without executing |
--quiet | false | Suppress routing animation — print model output only |
--timeout | 60s | Per-step timeout (routing + execution) |
--max-output-tokens | 8192 | Bounded output budget for each step |
--on-failure | abort-ask | What to do when a step fails: abort-ask (prompt), abort, or continue |
--no-feedback | false | Disable the opt-in post-run feedback prompt |
# preview what will run
veto exec my-plan.md --dry-run
# run the plan
veto exec my-plan.md
# run silently, keep going even if a step fails
veto exec my-plan.md --quiet --on-failure continue
Plan file format — a Markdown file with YAML frontmatter:
---
title: Refactor auth middleware
version: 1
steps:
- task: "Read current auth middleware and list what each function does"
kind: extract
risk: low
- task: "Rewrite the token validation using the standard library JWT package"
kind: code-change
risk: medium
depends_on: [1]
success_criteria: "Tests pass, no third-party JWT dep, function names unchanged"
- task: "Write unit tests for the new token validation"
kind: code-change
risk: low
depends_on: [2]
---
If the file has no frontmatter or fails validation, veto exec offers to convert it automatically by routing the raw text through the best available model. The converted plan is saved to ~/.veto/plans/.
veto route flags
Route only — select the best model without executing the task. Useful when you want to call the model yourself or verify routing logic.
| Flag | Default | Description |
|---|---|---|
--kind | (auto-detected) | Task type (see below) |
--risk | medium | Impact level: low, medium, high |
--max-cost | 0 (no limit) | Estimated preflight cost ceiling in USD |
--timeout | 30s | Per-model admission timeout |
--quiet | false | Print selected model name only (machine-readable) |
--json | false | Print one JSON result line; implies --quiet and --no-resume |
--no-resume | false | Ignore saved checkpoint and start fresh |
--runtime | (all) | Restrict candidates to one executable runtime, such as opencode |
--dashboard | false | Open a live routing view in your browser |
# scriptable model selection with metadata
veto route --json "summarize this PR"
Task kinds
| Kind | Best for |
|---|---|
extract | Pull structured data from text |
summarize | Condense or distill content |
code-change | Write or modify code |
debug | Diagnose and fix errors |
plan | Break down work, write specs |
review | Code review, analysis |
refactor | Restructure without changing behavior |
Superpowers
Automatic cost preflight — set --max-cost 0.01 and veto filters out models whose estimated cost exceeds your ceiling before they're asked. This is a preflight estimate, not an absolute billing guarantee: admission and execution usage can differ, and some providers do not report usage. Veto reports unknown actual usage/cost as unknown rather than silently treating it as zero.
Checkpoint resume — if routing is interrupted (Ctrl+C, timeout, network blip), veto saves which models already responded. Re-run the same command to pick up where you left off. Use --no-resume to start fresh.
End-to-end execution — veto run routes and then calls the winning model with your task using the separate execution budget, printing the response to stdout. Streaming output is used automatically when the executor supports it: Claude subscription mode streams claude -p; Codex subscription mode consumes codex exec --json, prints agent updates, and records safe tool and usage events; OpenCode server mode streams session text while mapping tool, approval, artifact, usage, cancellation, and failure events into Veto's ledger. HTTP/API and local OpenAI-compatible transports remain text only.
Skill injection — before executing, veto looks up reusable instruction snippets that match the task kind. Skills in ~/.veto/skills/ are always available (hand-written or previously generated via generateSkill). Skills from other directories (e.g. ~/.claude/skills/) can be approved via veto setup. At startup, veto silently checks for unapproved skill files and reminds you to run veto setup if any are found. Kind-specific skills are preferred over generic ones; cap is 2 per execution. Skills are never auto-generated during a routing call — only pre-existing approved files are used, so there is no hidden warm-up cost at the start of each invocation.
Acceptance-criteria review — --criteria "..." on veto run triggers a second routing call after execution. A different model (not the one that did the work) grades the output against each criterion and returns a structured pass/fail. Exits 1 if any criterion fails, or if the review is unavailable, malformed, incomplete, or internally inconsistent — making a requested review a fail-closed quality gate.
Verified Runs — use --criteria-file and --evidence to bind a versioned evidence manifest to every criterion. Veto gives the bounded evidence summaries to an independent reviewer and stores a private, redacted receipt with verified_pass, verified_fail, or inconclusive; it never executes evidence commands itself. Inspect local outcome receipts with veto verified-runs report. See Verified Runs.
Multi-step plan execution — veto exec plan.md runs a sequenced plan where each step is routed to the best model. If a step fails, you're asked whether to continue. Plans are just Markdown files with YAML frontmatter — write them by hand, or let veto convert any existing task list automatically. Use --dry-run to preview what will run before committing.
Quiet mode for scripts — --quiet on veto run suppresses the routing pipeline and prints only model output, making it composable:
# capture model output directly
veto run --quiet "summarize this PR" > summary.txt
# use the selected model name in a shell pipeline
MODEL=$(veto route --quiet "summarize this PR")
echo "Using: $MODEL"
JSON mode for agent infrastructure — --json on veto route suppresses animation and checkpoint resume, then emits one JSON line on stdout:
{"model":"opencode:anthropic/claude-sonnet","source":"opencode","provider":"anthropic","api_model":"claude-sonnet","runtime":"opencode","tier":"mid","kind":"summarize","risk":"medium","complexity":"simple","confidence":0.93,"saved_usd":0.0123}
If no model accepts, the command exits non-zero and emits:
{"error":"no_candidate","kind":"summarize","risk":"medium","complexity":"simple"}
Complexity-aware tier enforcement — veto auto-infers task complexity (simple / moderate / complex) from keywords in the objective and the task kind. Complex tasks (CQRS, microservices, distributed architecture…) are hard-filtered to large-tier models only; moderate tasks require mid or large tier. Small models are removed before the admission gate runs — they never get a chance to self-admit into tasks beyond their capability. Complexity is shown in the task header alongside kind and risk.
Cost-first scoring — candidates are ranked cheapest-viable-first. The scorer uses opus-level input cost ($0.015/1k tokens) as its reference baseline: local/free models score 1.0, haiku/mini score much higher than opus. Expensive models are asked only after cheaper ones reject. The admission gate already enforces kind-fit, so the scorer's job is to order the survivors by cost.
Confidence gating — any model that accepts but reports less than 70% confidence is treated as a rejection. You only get models that are genuinely sure.
Offline evaluation — veto benchmark --corpus internal/eval/testdata/routing_corpus.json replays cheapest, strongest, static, and adaptive policies without credentials or network access. It emits success, quality, cost, latency, admission-attempt, budget-violation, and confidence-calibration metrics. The checked-in corpus validates router mechanics; real-provider outcomes are required before making claims about production calibration.
Multi-provider fallback — if your primary provider is down or all its models reject, veto continues down the ranked list across providers automatically.
Structured rejection reasons — when nothing accepts, you get machine-readable reason codes (COST_CEILING_EXCEEDED, COMPLEXITY_TOO_HIGH, TASK_KIND_OUTSIDE_STRENGTHS, etc.) in both the UI and the log, so you know exactly what to adjust.
Per-model disable/enable — veto disable haiku gpt-4.1 excludes those models from all future routing without removing their credentials. veto enable haiku brings them back. Disabled model names are stored in ~/.veto/config.json under "disabled_models" — edit the file directly for bulk changes.
Local model preferences — optional routing policy in ~/.veto/config.json
filters the full catalog before admission. Pins are exclusive, favorites are
promoted after normal scoring, allowlists constrain eligibility, and
disable/exclude always win:
{
"routing": {
"pinned_models": [],
"pinned_providers": [],
"favorite_models": ["openai/gpt-4.1-mini"],
"favorite_providers": ["openrouter"],
"allowed_models": [],
"allowed_providers": [],
"excluded_models": [],
"excluded_providers": ["example-provider"]
}
}
Model entries accept either the Veto routing name or provider-facing model ID. Every route makes at most three admission calls, including failed transports; checkpoint resume continues with untried candidates in a later invocation.
7-day event ledger — routing, execution, artifact, and review lifecycle
events are logged as versioned JSON lines to
~/.veto/logs/veto-YYYY-MM-DD.log. The ledger omits objectives, prompts, and
responses and redacts bounded error detail. Files older than 7 days are pruned
automatically. See the event schema.
Providers and models
| Provider | Models | Set up with |
|---|---|---|
| Codex (ChatGPT subscription) | codex | codex CLI logged in with ChatGPT |
| Anthropic (subscription or native CLI) | haiku, sonnet, opus | authenticated claude CLI; legacy CLAUDE_SUBSCRIPTION=true also supported |
| Anthropic (API key) | haiku, sonnet, opus | ANTHROPIC_API_KEY |
| OpenAI | gpt-4.1, gpt-4.1-mini, sol, terra, luna | OPENAI_API_KEY |
| OpenRouter | built-in fallback plus the validated dynamic catalog | veto login browser OAuth or OPENROUTER_API_KEY |
| xAI (Grok) | grok-4.5, grok-4.3, grok-3, grok-3-mini | XAI_API_KEY |
| OpenCode runtime | connected provider/model bindings | veto opencode connect |
| Local / self-hosted | any name you choose | veto login → option 5 (guided Ollama install, LM Studio, or manual) |
Subscription mode takes precedence over API key when both are configured. Claude and Codex subscription modes expose their CLI tools, but Veto does not independently verify flat-subscription billing or capacity and therefore reports those fields as UNKNOWN. OpenCode can execute tools already allowed by the user's OpenCode policy; Veto does not infer a tool or browser capability from the model name and never auto-approves a new permission request. Anthropic/OpenAI/OpenRouter APIs and local OpenAI-compatible servers are text-only through Veto, even when the underlying model advertises function calling. Local inference has $0 provider billing, but still consumes your machine's resources. veto providers shows which mode is active and lists all local models.
Veto fetches and safely caches OpenRouter's larger catalog, filters it locally, and sends admission requests to at most three candidates. Models with unknown price or context are not treated as free or unlimited, and unknown quality tier is not invented. See the catalog cache contract.
Ollama models curated for routing:
| Model | Size | Best for |
|---|---|---|
qwen2.5-coder:7b | 4.7 GB | Code tasks — outperforms many larger models on coding |
llama3.2:3b | 2.0 GB | Quick tasks, low-RAM machines |
mistral:7b | 4.1 GB | General-purpose, good speed/quality balance |
File layout
~/.veto/
credentials.json # stored API keys and subscription marker (0600)
models.json # local / self-hosted model definitions (0600)
config.json # settings: routing, feedback, skills, disabled models, analytics preference
feedback/<timestamp>-<slug>.json # redacted local feedback reports (0600)
skills/<kind>.md # cached skill snippets (auto-generated, editable)
checkpoints/<hash>.json # resume state for interrupted routing
plans/<timestamp>-<slug>-converted.md # auto-converted plan files
logs/veto-YYYY-MM-DD.log # JSON-line routing history (7-day rolling)
Analytics and privacy
Veto's diagnostic event ledger is local-only and redacted. It helps inspect routing, execution, cost, latency, approvals, and failures without sending anything to a Veto server. It excludes prompts, objectives, responses, credentials, paths, file contents, terminal history, raw provider events, and browser content.
veto analytics status
veto analytics enable # records opt-in; sends nothing today
veto analytics disable # records opt-out
Remote analytics are not implemented. A future transport must publish its
exact payload, purpose, recipient, retention, deletion process, and network
metadata policy before it can use the stored opt-in. See
docs/analytics.md and the
veto-tui-requirements.md.
Development
make test # go test -race -timeout 120s ./...
make build # build with version injected from git tag (or "dev")
make lint # go vet ./...
make release RELEASE_VERSION=0.0.0 # local release dry run; do not publish
release_dist=$(mktemp -d)
./scripts/package-release.sh v0.0.0 "${release_dist}" # local release dry run
The binary embeds the normalized version without the leading v (veto version). A versioned go install resolves the same public version through Go build metadata while remaining honestly labeled as a source build. CI runs on every push and PR to main, including local release-packaging and Homebrew-formula dry runs. Conventional commits on main maintain a Release Please pull request (fix increments patch, feat increments minor, and a breaking change increments major). Merging that release PR creates the tag and GitHub release, then explicitly starts the existing release workflow. That workflow runs the offline gates before GoReleaser publishes six Darwin/Linux/Windows amd64/arm64 archives plus SHA256SUMS and BINARY_SHA256SUMS. When HOMEBREW_TAP_TOKEN is configured, it also publishes the checksum-pinned Homebrew formula. Every GoReleaser binary is checked against the binary manifest before publication. Nothing is published unless all applicable gates and checksum verification pass.
See docs/architecture.md for how the routing pipeline works internally.
See docs/release-readiness.md for the automated gates and the owner-run provider, trial, license, and publish checklist.
See docs/launch.md for the launch angle, channel-ready copy, share loops, measurement plan, and launch gates.
See CONTRIBUTING.md before opening an issue or pull request.
Project status
Veto is a public beta. CI exercises the router, race detector, onboarding smoke
test, offline benchmark, release packaging, and Homebrew formula rendering.
Published releases include checksum manifests. When HOMEBREW_TAP_TOKEN is
configured, the release workflow updates the Homebrew tap from those verified
artifacts; otherwise it skips the tap update. Real-provider availability,
pricing, and routing quality still depend on the configured accounts and
workloads.
See the latest release and
CHANGELOG.md for shipped changes.