Skip to main content
Integrate Veto into your CI/CD pipeline to enforce policy coverage, catch rule errors, and prevent uncovered tools from reaching production.

Why CI/CD Integration?

Prevent Uncovered Tools

Fail builds if new tools are added without rules.

Catch Rule Errors

Validate YAML syntax and rule logic before deployment.

Policy Diff in PRs

Show what rules changed and their impact on historical calls.

Enforce Standards

Require tests and coverage thresholds for all policy changes.

Using veto scan in CI

The veto scan command discovers tools in your codebase and checks if they have rules.

Basic Scan

veto scan
Output:
Veto Scan Coverage Audit
========================

Project directory: /home/user/my-agent
Rules loaded: 5 (global: 0)
Framework hints: langchain
Coverage: 4/5 (80.0%)

Discovered tools:
  [COVERED] transfer_funds(amount, from_account, to_account)
    locations: src/tools/financial.ts
    sources: source-ts
  [COVERED] get_balance(account_id)
    locations: src/tools/financial.ts
    sources: source-ts
  [UNCOVERED] send_email(to, subject, body)
    locations: src/tools/communication.ts
    sources: source-ts

Fail on Uncovered Tools

Use --fail-uncovered to exit with code 1 if any tools lack rules:
veto scan --fail-uncovered
Exit code:
  • 0: All tools covered
  • 1: Uncovered tools found

JSON Output

For programmatic parsing:
veto scan --format json > coverage-report.json
Output:
{
  "timestamp": "2026-03-04T14:23:45.123Z",
  "projectDir": "/home/user/my-agent",
  "summary": {
    "total": 5,
    "covered": 4,
    "uncovered": 1,
    "coveragePercent": 80.0
  },
  "discoveredTools": [
    {
      "name": "transfer_funds",
      "covered": true,
      "coverageReason": "tool-rule",
      "matchedRuleIds": ["block-large-transfers", "block-external-transfers"]
    },
    {
      "name": "send_email",
      "covered": false,
      "coverageReason": "none",
      "matchedRuleIds": []
    }
  ]
}

Generate Suggestions

Get starter rule snippets for uncovered tools:
veto scan --suggest
Output:
Suggested starter rules:

  send_email (@veto/communication)
  Rationale: Tool name matches communication keywords (e.g. email/message/notify).
  Snippet:
    rules:
      - id: guard-send-email
        name: Guard send_email
        description: Restrict sensitive outbound communication
        enabled: true
        severity: high
        action: block
        tools:
          - send_email
        conditions:
          - field: arguments.to
            operator: not_contains
            value: '@company.com'

GitHub Actions

Example Workflow

.github/workflows/veto-ci.yml
name: Veto Policy CI

on:
  pull_request:
    paths:
      - 'veto/**'
      - 'src/**'
  push:
    branches: [main]

jobs:
  policy-check:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Veto CLI
        run: npm install -g veto-cli

      - name: Scan for uncovered tools
        run: veto scan --fail-uncovered

      - name: Run policy tests
        run: npm test -- test-policies.test.ts

      - name: Adversarial analysis
        run: veto test --policy ./veto/rules

      - name: Generate policy diff
        if: github.event_name == 'pull_request'
        run: |
          git fetch origin main
          veto diff --old origin/main:veto/rules --new HEAD:veto/rules > policy-diff.txt
          cat policy-diff.txt >> $GITHUB_STEP_SUMMARY

Comment on PR with Coverage Report

.github/workflows/veto-pr-comment.yml
name: Veto Coverage Comment

on:
  pull_request:
    paths:
      - 'veto/**'
      - 'src/**'

jobs:
  coverage-comment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Veto CLI
        run: npm install -g veto-cli

      - name: Run coverage scan
        id: scan
        run: |
          veto scan --format json > coverage.json
          COVERAGE=$(jq -r '.summary.coveragePercent' coverage.json)
          UNCOVERED=$(jq -r '.summary.uncovered' coverage.json)
          echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
          echo "uncovered=$UNCOVERED" >> $GITHUB_OUTPUT

      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          script: |
            const coverage = '${{ steps.scan.outputs.coverage }}';
            const uncovered = '${{ steps.scan.outputs.uncovered }}';
            const body = `## Veto Policy Coverage\n\n` +
              `Coverage: **${coverage}%**\n\n` +
              (uncovered > 0
                ? `⚠️ **${uncovered} uncovered tool(s) detected**\n\nRun \`veto scan\` locally for details.`
                : `✅ All tools covered by rules.`);
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body,
            });

GitLab CI

Example Pipeline

.gitlab-ci.yml
stages:
  - test
  - policy

veto:scan:
  stage: policy
  image: node:20
  script:
    - npm install -g veto-cli
    - veto scan --fail-uncovered
  only:
    changes:
      - veto/**
      - src/**

veto:test:
  stage: policy
  image: node:20
  script:
    - npm install -g veto-cli
    - veto test --policy ./veto/rules --format json > gaps.json
    - |
      CRITICAL=$(jq '.summary.critical' gaps.json)
      if [ "$CRITICAL" -gt 0 ]; then
        echo "Critical policy gaps found!"
        exit 1
      fi
  artifacts:
    reports:
      junit: gaps.json

veto:diff:
  stage: policy
  image: node:20
  script:
    - npm install -g veto-cli
    - git fetch origin main
    - veto diff --old origin/main:veto/rules --new HEAD:veto/rules > diff.txt
  artifacts:
    paths:
      - diff.txt
  only:
    - merge_requests

CircleCI

Example Config

.circleci/config.yml
version: 2.1

jobs:
  veto-scan:
    docker:
      - image: cimg/node:20.0
    steps:
      - checkout
      - run:
          name: Install Veto CLI
          command: npm install -g veto-cli
      - run:
          name: Scan for uncovered tools
          command: veto scan --fail-uncovered
      - run:
          name: Run policy tests
          command: npm test -- test-policies.test.ts

workflows:
  test:
    jobs:
      - veto-scan:
          filters:
            branches:
              only:
                - main
                - develop

Policy Diff in PRs

Show what changed between policy versions with veto diff:
veto diff --old origin/main:veto/rules --new HEAD:veto/rules
Output:
Veto Policy Diff
================

Old: origin/main:veto/rules
New: HEAD:veto/rules

Structural Changes:
  Added rules: 1
  Removed rules: 0
  Modified rules: 2

--- ADDED ---
  [+] require-approval-for-prod-deploy
      Scope: deploy
      Action: require_approval
      Summary: Gate production deployments with human approval

--- MODIFIED ---
  [~] block-large-transfers
      Scope: transfer_funds
      Changes:
        - action: block → require_approval
        - conditions[0].value: 10000 → 5000

With Impact Analysis

Replay historical calls to see how the diff affects them:
veto diff --old origin/main:veto/rules --new HEAD:veto/rules --log audit.jsonl
Output:
Impact Report:
  Total calls: 100
  Changed decisions: 12 (12.0%)
    - allow → deny: 3
    - allow → require_approval: 7
    - deny → allow: 2

Samples:
  [Line 45] transfer_funds: allow → deny
    Old: No rule matched
    New: block-large-transfers (threshold lowered from $10,000 to $5,000)

  [Line 67] deploy: allow → require_approval
    Old: No rule matched
    New: require-approval-for-prod-deploy
Use this to validate policy changes before merging.

Enforcing Coverage Thresholds

Require 100% Coverage

.github/workflows/veto-ci.yml
- name: Enforce 100% coverage
  run: |
    COVERAGE=$(veto scan --format json | jq -r '.summary.coveragePercent')
    if [ "$COVERAGE" != "100.0" ]; then
      echo "Coverage is $COVERAGE%, but 100% is required"
      exit 1
    fi

Require Minimum Coverage

- name: Enforce 80% coverage
  run: |
    COVERAGE=$(veto scan --format json | jq -r '.summary.coveragePercent')
    THRESHOLD=80
    if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
      echo "Coverage is $COVERAGE%, but $THRESHOLD% is required"
      exit 1
    fi

Preventing Regression

Block if Coverage Drops

- name: Check coverage didn't drop
  run: |
    git fetch origin main
    git checkout origin/main
    OLD_COVERAGE=$(veto scan --format json | jq -r '.summary.coveragePercent')
    git checkout -
    NEW_COVERAGE=$(veto scan --format json | jq -r '.summary.coveragePercent')
    if (( $(echo "$NEW_COVERAGE < $OLD_COVERAGE" | bc -l) )); then
      echo "Coverage dropped from $OLD_COVERAGE% to $NEW_COVERAGE%"
      exit 1
    fi

Docker Integration

Run Veto in a Docker container:
Dockerfile.veto
FROM node:20-alpine

RUN npm install -g veto-cli

WORKDIR /workspace

ENTRYPOINT ["veto"]
docker build -f Dockerfile.veto -t veto:latest .
docker run -v $(pwd):/workspace veto:latest scan --fail-uncovered

Pre-Commit Hook

Add a pre-commit hook to validate policies locally:
.git/hooks/pre-commit
#!/bin/bash

if git diff --cached --name-only | grep -q 'veto/'; then
  echo "Validating Veto policies..."
  veto scan --quiet
  if [ $? -ne 0 ]; then
    echo "Policy validation failed. Fix errors before committing."
    exit 1
  fi
fi
chmod +x .git/hooks/pre-commit

Real-World Example: Complete CI Pipeline

1

Install Veto CLI

- name: Install Veto CLI
  run: npm install -g veto-cli
2

Scan for uncovered tools

- name: Scan coverage
  run: veto scan --fail-uncovered
3

Run policy tests

- name: Test policies
  run: npm test -- test-policies.test.ts
4

Adversarial analysis

- name: Find policy gaps
  run: veto test --policy ./veto/rules
5

Generate diff for PR

- name: Policy diff
  if: github.event_name == 'pull_request'
  run: |
    git fetch origin main
    veto diff --old origin/main:veto/rules --new HEAD:veto/rules > diff.txt
    cat diff.txt >> $GITHUB_STEP_SUMMARY

Best Practices

Fail Fast

Run veto scan --fail-uncovered early in the pipeline.
- name: Scan
  run: veto scan --fail-uncovered

Test on Every PR

Validate policies whenever code or rules change.
on:
  pull_request:
    paths:
      - 'veto/**'
      - 'src/**'

Cache Dependencies

Speed up CI by caching Veto CLI installation.
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: veto-${{ runner.os }}

Store Reports

Archive coverage reports for historical tracking.
- uses: actions/upload-artifact@v4
  with:
    name: coverage-report
    path: coverage.json

Troubleshooting

”veto: command not found”

Ensure Veto CLI is installed:
npm install -g veto-cli
veto --version

“No rules loaded”

Verify veto/veto.config.yaml exists and points to the correct rules directory:
rules:
  directory: "./rules"
  recursive: true

“Scan detects no tools”

Veto scans TypeScript/JavaScript/Python source files. Check:
  • Are tools defined in supported languages?
  • Are tool files in excluded directories (e.g., node_modules, dist)?
Use veto scan --include-examples --include-tests to scan all directories.

Next Steps

Testing Policies

Write tests for your rules

Writing Rules

Learn all rule syntax and operators

Audit Trail

Export decisions for compliance

Approval Workflows

Set up human-in-the-loop approval

Build docs developers (and LLMs) love