Checkmate
AI-native API testing framework with YAML specs and git-connected history.
Checkmate makes API testing simple for both humans and AI agents. Write declarative test specs in YAML, run assertions with Clove queries, and track results with full git context.
Quick Start
# Build from source
# Initialize in your project
# Create a test spec
# Run tests
Features
- YAML Test Specs: Declarative, readable test definitions
- Clove Query Language: Powerful JSON assertions with
$[field]and$[@prev]scopes - API Version Diff: Compare responses across endpoints to detect structural changes (
cm diff run) - Variable Extraction: Extract values from responses for multi-step auth flows
- Universal Env Vars:
${VAR}expansion in endpoints, headers, bodies, and query params - Git-Connected History: Every test run captures commit, branch, and dirty status
- Auto-Discovery: Run
cm test runfrom any subdirectory - Configuration Hierarchy: Defaults → User config → Project config → Environment
- Hook System: Run custom scripts on test lifecycle events
- AI-Native: Built-in documentation, Claude plugin, and agent-friendly CLI
Installation
From Source
# Binary at target/release/cm
Add to PATH
Initialization
Standard Mode
Creates .checkmate/ directory structure:
.checkmate/
├── config.toml # Project configuration
├── tests/ # Test specifications
├── runs/ # Test run history (JSONL)
└── hooks/ # Custom hook scripts
Stealth Mode
For personal use on shared projects without committing Checkmate files:
This adds .checkmate/ to .git/info/exclude, keeping it local to your machine.
Interactive vs Non-Interactive
- Terminal (TTY):
cm initprompts for base URL interactively - Non-TTY (agents): Requires
--urlflag with clear error message
Test Specifications
Test specs are YAML files in .checkmate/tests/:
name: "User API Tests"
description: "Test user CRUD operations"
env:
base_url: "http://localhost:8080" # Optional, uses config if not set
timeout_ms: 5000
requests:
create_user:
body:
name: "Test User"
email: "test@example.com"
headers:
Authorization: "Bearer ${TOKEN}" # ${VAR} works in headers, bodies, endpoints, query params
tests:
user_creation:
description: "Create a new user"
endpoint: /api/users
method: POST
requests:
expect_status: 201
assertions:
- query: "$[id]"
expect_type: string
message: "User ID should be returned"
- query: "$[name]"
expect: "Test User"
Assertion Types
assertions:
# Exact value match
- query: "$[doc][status]"
expect: "active"
# Type checking
- query: "$[doc][count]"
expect_type: number # string, number, boolean, array, object, null
# Existence check
- query: "$[doc][field]?"
expect: true
# Numeric comparisons
- query: "$[doc][count]"
expect_gte: 1
expect_lte: 100
# Array operations
- query: "$[doc][items].length()"
expect_gte: 1
# Compare to previous response
- query: "$[doc][counter]"
expect_gt: "$[@prev][counter]"
Multi-Request Tests
For testing stateful behavior (counters, rate limits, sessions):
tests:
counter_increments:
description: "Counter should increment on each request"
endpoint: /api/counter
requests:
skip_first: true # Don't assert on baseline request
assertions:
- query: "$[doc][count]"
expect_gte: 1
- query: "$[@prev][count]"
expect_lt: "$[doc][count]"
message: "Count should increase"
Variable Extraction
Extract values from responses and inject them into subsequent requests using extract: and {{var}}:
requests:
login:
body:
username: "${USERNAME}"
password: "${PASSWORD}"
extract:
auth_token: "$[token]"
user_id: "$[user][id]"
get_profile:
headers:
Authorization: "Bearer {{auth_token}}"
tests:
auth_flow:
endpoint: /api/login
method: POST
requests:
assertions:
- query: "$[name]"
expect_type: string
${VAR} expands environment variables. {{var}} injects values extracted from previous responses.
API Version Diff
Compare responses across API versions to detect structural changes. Diffs use the same YAML spec files and discovery as tests, and runs are recorded to history.
env:
base_url: "http://localhost:8080"
requests:
login:
body:
extract:
token: "$[access_token]"
auth_get:
headers:
Authorization: "Bearer {{token}}"
v1_query:
body:
v2_query:
body:
diffs:
user_endpoint_versions:
description: "Compare v1 vs v2 user listing"
setup:
- request: login
endpoint: /auth/login
method: GET
endpoints:
- name: v1
path: /api/v1/users
- name: v2
path: /api/v2/users
requests:
- auth_get # Sent to all endpoints
- name: v1_query
scope: # Only sent to v1
- name: v2_query
scope: # Only sent to v2
assertions:
- query: "$[additions]"
expect: 0
message: "No new fields expected"
- query: "$[changes].any(@[change_type] == 'removed')"
expect: false
message: "No fields should be removed"
Setup (Authentication)
Run auth/login requests before endpoint iteration. Extracted variables persist into diff requests:
setup:
- request: login # Request name from the requests map
endpoint: /auth/login # Own endpoint (not from the endpoints list)
method: POST # Defaults to POST
Named Endpoints
Give endpoints a name for use with scoped requests and assertions. Simple strings and named objects can be mixed:
endpoints:
- /api/v1/users # name defaults to the path
- name: v2
path: /api/v2/users
Scoped Requests
Different API versions often need different payloads. Scope requests to specific endpoints by name:
requests:
- common_request # Unscoped — sent to all endpoints
- name: v1_only
scope: # Only sent to the v1 endpoint
- name: v2_only
scope: # Only sent to the v2 endpoint
Scoped Assertions
When comparing 3+ endpoints, scope assertions to specific pairs:
assertions:
- query: "$[removals]"
expect: 0 # Applies to all pairs
- query: "$[additions]"
expect: 0
scope: # Only for the v1↔v2 comparison
Diff Result Document
Assertions query against a diff result with this structure:
Running Diffs
# Run all diffs
# Run specific spec
# Run specific diff by name
# List available diffs
Output modes: In a TTY (interactive terminal), diffs display with colored structural output. In non-TTY (piped to scripts/agents), diffs output structured JSON. Diff runs are recorded to history — view with cm history.
Running Tests
Run All Tests
Auto-discovers all .yaml files in .checkmate/tests/.
Run Specific Specs
# By name (resolves to .checkmate/tests/users.yaml)
# By path
# Multiple specs
Run Specific Test
Verbose Output
Other Commands
# List all tests
# Validate specs without running
Configuration
Hierarchy (lowest to highest priority)
- Built-in defaults
- User config:
~/.config/checkmate/config.toml - Project config:
.checkmate/config.toml - Environment variables:
CM_*
Project Configuration
.checkmate/config.toml:
[]
= "http://localhost:8080"
= 5000
[]
= false
= 200
Environment Variables
Override any setting with CM_ prefix and double underscore for nesting:
CM_ENV__BASE_URL="http://staging:8080"
CM_ENV__TIMEOUT_MS=10000
CM_DEFAULTS__FAIL_FAST=true
View Configuration
# Show current config
# Show as JSON
# Show config sources
Test History
Every test run is recorded with git context in .checkmate/runs/runs.jsonl.
View History
# Recent runs
# Filter by spec
# Filter by commit
# More results
View Run Details
Output:
Run: cm-run-a3f
Time: 2024-01-15T10:30:00Z
Spec: users.yaml
Results: 5/5 passed, 0 failed, 0 errors (1234ms)
Git Context:
Commit: abc1234
Branch: feature/user-api
Dirty: no
Message: Add user validation
History Format (JSONL)
Each line in runs.jsonl is a complete run record:
Hooks
Run custom scripts at test lifecycle events. Place executable scripts in .checkmate/hooks/:
| Hook | When | Use Case |
|---|---|---|
pre_run |
Before tests start | Setup, notifications |
post_run |
After tests complete | Cleanup, reporting |
on_pass |
All tests passed | Success notifications |
on_fail |
Any test failed | Alerts, CI integration |
Environment Variables
Hooks receive context via environment:
| Variable | Description |
|---|---|
CM_RUN_ID |
Run identifier (e.g., cm-run-a3f) |
CM_SPEC |
Spec file name |
CM_TOTAL |
Total test count |
CM_PASSED |
Passed count |
CM_FAILED |
Failed count |
CM_ERRORS |
Error count |
CM_DURATION_MS |
Duration in milliseconds |
Example Hook
.checkmate/hooks/on_fail:
#!/bin/bash
# Send Slack notification, create issue, etc.
Make executable: chmod +x .checkmate/hooks/on_fail
Clove Query Language
Checkmate uses Clove for JSON assertions. Key features:
Scopes
$[doc]- Current response body$[@prev]- Previous response (for multi-request tests)
Navigation
$[doc][user][name] # Nested object
$[doc][items][0] # Array index
$[doc][items][0][id] # Nested in array
Methods
$[doc][items].length() # Array length
$[doc][name].upper() # String uppercase
$[doc][price].round() # Number rounding
Existence Check
$[doc][optional]? # Returns true/false
For full documentation: cm doc clove
CLI Reference
Core Commands
| Command | Description |
|---|---|
cm init |
Initialize Checkmate in current directory |
cm test run |
Run test specifications |
cm test list |
List available tests |
cm test validate |
Validate specs without running |
cm diff run |
Run diff specifications |
cm diff list |
List available diffs |
cm history |
Show test run history |
cm show <id> |
Show details of a specific run |
cm config show |
Display current configuration |
Documentation
| Command | Description |
|---|---|
cm docs |
Full documentation overview |
cm doc <category> |
Category-specific docs |
cm onboard |
Quick-start guide for AI agents |
cm prime |
Output project context (for hooks) |
Categories for cm doc
assertions- Assertion types and examplesrequests- Request definition and variable extractionenv- Environment variables and configurationdiff- API version diff comparisonscopes- Clove query scopesexamples- Complete examplescli- CLI reference
AI Agent Integration
Claude Plugin
Install the Checkmate Claude plugin to get automatic context injection:
# Plugin auto-runs 'cm prime' on session start
# Provides: test specs, recent runs, quick reference
Agent Onboarding
Outputs a condensed guide for AI agents covering:
- Test spec format
- Running tests
- Interpreting results
- Common patterns
AGENTS.md
Add to your project's AGENTS.md:
This project uses Checkmate for API testing.
- ---
Project Structure
your-project/
├── .checkmate/
│ ├── config.toml # Project settings
│ ├── tests/ # Test specifications
│ │ ├── users.yaml
│ │ ├── orders.yaml
│ │ └── ...
│ ├── runs/ # Test history
│ │ ├── runs.jsonl # Run records
│ │ └── .gitignore # Ignores JSONL by default
│ └── hooks/ # Lifecycle scripts
│ ├── pre_run
│ ├── on_pass
│ └── on_fail
└── ...
Best Practices
- One concern per spec file - Group related tests together
- Use descriptive test names -
user_creation_with_valid_datanottest1 - Include failure messages - Help debugging when assertions fail
- Commit test specs - Track them alongside your code
- Use stealth mode - For personal testing on shared projects
- Review history - Track test stability across commits
Troubleshooting
"No .checkmate/ found"
Run cm init --url <your-api-url> in your project root.
Tests not discovered
Ensure specs are in .checkmate/tests/ with .yaml or .yml extension.
Config not applied
Check priority: env vars > project config > user config > defaults.
Use cm config sources to see what's being loaded.
Hooks not running
- Check file exists in
.checkmate/hooks/ - Ensure it's executable:
chmod +x .checkmate/hooks/on_pass - Check script has correct shebang:
#!/bin/bash
License
MIT