scriptable: just the output, no routing UI

September 13, 2026 · View on GitHub

veto

Build and test status Go Report Card Latest release Apache-2.0 license

Veto logo

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 with veto run, or route each step of a plan with veto 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 -p instead of hitting the API. Veto reports billing as UNKNOWN unless the execution mode is directly verifiable; an inherited ANTHROPIC_API_KEY can 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 serve isn'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

CommandWhat it does
veto loginConnect a provider interactively (browser + masked key)
veto logoutRemove 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 setupDiscover and approve skill files from your skill directories
veto providersShow which providers are configured and how
veto models [--json] [--offline]List effective models, runtimes, tool knowledge, and known/unknown prices
veto benchmarkReplay the offline routing corpus and emit JSON metrics
veto verify-modelsVerify catalog IDs against one provider account
veto doctorDiagnose installation and local-state integrity; optionally repair safe problems
veto feedbackSave a redacted report and prepare a GitHub issue
veto analytics statusShow local diagnostics and future remote-sharing preference
veto analytics enable|disableSet 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 versionPrint veto version
veto install-git-hookAdd 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).

FlagDefaultDescription
--kind(auto-detected)Task type (see below)
--riskmediumImpact level: low, medium, high
--max-cost0 (no limit)Estimated preflight cost ceiling in USD
--timeout2hTotal timeout (routing + execution)
--admission-timeout60sPer-model admission timeout
--quietfalseSuppress routing animation — print model output only
--max-output-tokens8192Bounded output budget for the execution response
--output(none)Explicit relative file path for saving output
--forcefalseAllow --output to replace an existing file
--criteria(none)Comma-separated acceptance criteria; a review pass runs after execution
--no-feedbackfalseDisable 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.

FlagDefaultDescription
--dry-runfalsePrint steps without executing
--quietfalseSuppress routing animation — print model output only
--timeout60sPer-step timeout (routing + execution)
--max-output-tokens8192Bounded output budget for each step
--on-failureabort-askWhat to do when a step fails: abort-ask (prompt), abort, or continue
--no-feedbackfalseDisable 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.

FlagDefaultDescription
--kind(auto-detected)Task type (see below)
--riskmediumImpact level: low, medium, high
--max-cost0 (no limit)Estimated preflight cost ceiling in USD
--timeout30sPer-model admission timeout
--quietfalsePrint selected model name only (machine-readable)
--jsonfalsePrint one JSON result line; implies --quiet and --no-resume
--no-resumefalseIgnore saved checkpoint and start fresh
--runtime(all)Restrict candidates to one executable runtime, such as opencode
--dashboardfalseOpen a live routing view in your browser
# scriptable model selection with metadata
veto route --json "summarize this PR"

Task kinds

KindBest for
extractPull structured data from text
summarizeCondense or distill content
code-changeWrite or modify code
debugDiagnose and fix errors
planBreak down work, write specs
reviewCode review, analysis
refactorRestructure 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 executionveto 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 executionveto 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 evaluationveto 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/enableveto 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

ProviderModelsSet up with
Codex (ChatGPT subscription)codexcodex CLI logged in with ChatGPT
Anthropic (subscription or native CLI)haiku, sonnet, opusauthenticated claude CLI; legacy CLAUDE_SUBSCRIPTION=true also supported
Anthropic (API key)haiku, sonnet, opusANTHROPIC_API_KEY
OpenAIgpt-4.1, gpt-4.1-mini, sol, terra, lunaOPENAI_API_KEY
OpenRouterbuilt-in fallback plus the validated dynamic catalogveto login browser OAuth or OPENROUTER_API_KEY
xAI (Grok)grok-4.5, grok-4.3, grok-3, grok-3-miniXAI_API_KEY
OpenCode runtimeconnected provider/model bindingsveto opencode connect
Local / self-hostedany name you chooseveto 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:

ModelSizeBest for
qwen2.5-coder:7b4.7 GBCode tasks — outperforms many larger models on coding
llama3.2:3b2.0 GBQuick tasks, low-RAM machines
mistral:7b4.1 GBGeneral-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.