Tarn is a CLI-first API testing tool written in Rust. Tests are .tarn.yaml files. Output is structured JSON with categorized failures and remediation hints, so an agent — Claude Code, Codex, opencode, Cursor, Windsurf, pi — can write a test, run it, read what broke, and fix it without scraping logs.
# tests/health.tarn.yaml
name: Health check
steps:
- name: GET /health
request:
method: GET
url: "{{ env.base_url }}/health"
assert:
status: 200$ tarn run
TARN Running tests/health.tarn.yaml
● Health check
✓ GET /health (4ms)
Results: 1 passed (15ms)When something breaks, --format json returns the same run as machine-readable data with failure_category, error_code, and the offending request/response. The tarn-mcp companion exposes a tarn_fix_plan tool that turns that report into actionable suggestions an agent can apply directly.
Why Tarn?
- Structured failures, not log scraping — every failure carries a stable category, error code, and remediation hints. Agents branch on taxonomy, not regex.
- MCP-native —
tarn-mcpexposeslist,validate,run, andfix_planas structured tools for Claude Code, Codex, opencode, Cursor, and Windsurf (and pi via the skill + CLI). See AI agent integrations. - YAML the model already knows — no DSL to teach, no test framework to bootstrap. An LLM writes a
.tarn.yamland ships. - One static binary —
curl | shinstall, no runtime, drops into any CI image. - Batteries included — REST + GraphQL, captures, cookies, multipart, includes, polling, Lua, parallel execution, 7 output formats.
Install
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/NazarKalytiuk/tarn/main/install.sh | sh
# from source
cargo install --git https://github.com/NazarKalytiuk/tarn.git --bin tarnPre-built binaries for macOS (Intel + Apple Silicon), Linux (amd64 + arm64), and Windows (amd64 zip) are on the releases page, each with a tarn-checksums.txt for SHA-256 verification and a generated tarn.rb Homebrew formula artifact. The installer also lays down tarn-mcp and tarn-lsp when present in the archive. Set TARN_INSTALL_DIR to install elsewhere. Container path: ghcr.io/<owner>/tarn:<tag> from the release workflow. Manual verification works with shasum -a 256 -c tarn-checksums.txt.
Quick Start
The 60-second path:
tarn init # scaffold tests/ + tarn.env.yaml + advanced templates
# edit tarn.env.yaml so base_url points at your API
tarn run # runs every .tarn.yaml under tests/Layer on the flags you actually need:
tarn run --format json --json-mode compact # structured output for agents and CI
tarn run --env staging # use a named environment
tarn run --only-failed # quiet down a noisy run
tarn run --watch # rerun on file changes
tarn run --parallel # run files in parallel
tarn list --tag smoke # what would run, without running
tarn fmt --check # canonical YAML, CI-gateableDebugging a failed run
Default to the failures-first loop — it keeps agents and humans off the megabyte-scale full report until they actually need it:
tarn validate <path> # syntax/config before running
tarn run <path> # writes .tarn/runs/<run_id>/
tarn failures # root-cause groups; cascades collapsed
tarn inspect last FILE::TEST::STEP # full context for ONE failure
# patch tests or application code
tarn rerun --failed # replay only failing (file, test) pairs
tarn diff prev last # confirm fixed / new / persistenttarn failures groups by root-cause fingerprint and collapses skipped_due_to_failed_capture cascades into their upstream entry — one failing step with five downstream skips surfaces as one entry with cascades: 5, not six. tarn inspect supports run-id aliases last / latest / @latest / prev and drills into one record via FILE[::TEST[::STEP]]. tarn rerun --failed stamps rerun_source onto the new report. tarn diff prev last buckets failure fingerprints into new / fixed / persistent so you can confirm a patch without re-reading the full report.
Reach for .tarn/runs/<run_id>/report.json only when failures + inspect cannot answer the question. See plugin/skills/tarn-api-testing/SKILL.md (Failures-First Loop) and docs/TROUBLESHOOTING.md for the canonical agent-facing guidance, including a worked example of a mutation endpoint whose response shape changed from {"uuid": "..."} to {"request": {"uuid": "..."}} and the $.uuid → $.request.uuid fix.
Hello World
A fully local demo with no external network dependency:
PORT=3000 cargo run -p demo-server &
cargo run -p tarn -- run examples/demo-server/hello-world.tarn.yamlMore local scenarios — redirects, cookies, forms, error responses, authenticated CRUD — live in examples/demo-server/.
Documentation
Full guides, CLI reference, AI workflow walkthroughs, and editor setup live on the docs site:
https://nazarkalytiuk.github.io/tarn/
In-repo docs to start with:
docs/INDEX.md— canonical map of all in-repo docsdocs/AI_WORKFLOW_DEMO.md— end-to-end agent loop walkthroughdocs/MCP_WORKFLOW.md— MCP server usage patternsdocs/TARN_PRODUCT_STRATEGY.md— product directiondocs/TARN_VS_HURL_COMPARISON.mdanddocs/HURL_MIGRATION.md— comparison and migrationdocs/TARN_LSP.md—tarn-lspLanguage Server (LSP 3.17) for Neovim, Helix, Zed, and other compatible clientsdocs/VSCODE_EXTENSION.md— VS Code extension ineditors/vscodeeditors/zed/README.md— Zed extension, published via zed-industries/extensions
The reference sections below mirror what's on the docs site — useful when reading on GitHub directly.
AI agent integrations
Tarn drives any agent that speaks MCP or has a shell. tarn-mcp exposes list / validate / run / fix_plan (plus the failures-first tools); the tarn-api-testing skill teaches the loop. This table is the canonical supported-agents list — per-agent setup kits live under editors/.
| Agent | How Tarn plugs in | Setup |
|---|---|---|
| Claude Code | tarn-mcp + skill plugin, plus the tarn-lsp plugin | marketplace · editors/claude-code/tarn-lsp-plugin |
| OpenAI Codex | tarn-mcp (codex mcp add) + .agents/skills/ skill + AGENTS.md | editors/codex |
| opencode | tarn-mcp + tarn-lsp + skill via opencode.jsonc | editors/opencode |
| pi | tarn-api-testing skill + tarn CLI (no native MCP; optional MCP via adapter) | editors/pi |
| Cursor | tarn-mcp via .cursor/mcp.json | MCP setup |
| Windsurf | tarn-mcp via .windsurf/mcp.json | MCP setup |
| Neovim / Helix / Zed / VS Code | tarn-lsp language server | docs/TARN_LSP.md |
A reproducible write → run → read-failure → fix loop for any of these lives in examples/agent-loop/.
Table of Contents
- Test File Format
- Request Body
- Assertions
- Variables
- Cookies
- Form URL-Encoding
- Multipart / File Upload
- Includes
- GraphQL
- Polling
- Lua Scripting
- CLI Reference
- Output Formats
- Performance Testing
- MCP Server
- Claude Code Plugin
- Claude Code Skill
- Troubleshooting
- GitHub Action
- Configuration
- Step Options
- JSON Schema
- VS Code Extension
- Zed Extension
- Shell Completions
- Development
Test File Format
Test files use .tarn.yaml and can be organized in any directory structure.
Minimal Test
name: Health check
steps:
- name: GET /health
request:
method: GET
url: "http://localhost:3000/health"
assert:
status: 200request.method accepts standard verbs and custom tokens such as PURGE or PROPFIND.
Full Format
version: "1"
name: "User CRUD Operations"
description: "Tests complete user lifecycle"
tags: [crud, users, smoke]
env:
base_url: "http://localhost:3000/api/v1"
defaults:
headers:
Content-Type: "application/json"
timeout: 5000
retries: 1
tests:
create_and_verify:
description: "Create a user, then verify it exists"
tags: [smoke]
steps:
- name: Create user
request:
method: POST
url: "{{ env.base_url }}/users"
body:
name: "Jane Doe"
email: "jane.{{ $random_hex(6) }}@example.com"
capture:
user_id: "$.id"
assert:
status: 201
body:
"$.name": "Jane Doe"
"$.id": { type: string, not_empty: true }
- name: Verify user
request:
method: GET
url: "{{ env.base_url }}/users/{{ capture.user_id }}"
assert:
status: 200
body:
"$.id": "{{ capture.user_id }}"Setup and Teardown
setup runs once before all tests. teardown runs after all tests even if tests fail.
name: "CRUD with auth"
setup:
- name: Login
request:
method: POST
url: "{{ env.base_url }}/auth/login"
body:
email: "{{ env.admin_email }}"
password: "{{ env.admin_password }}"
capture:
auth_token: "$.token"
teardown:
- name: Cleanup
request:
meth
…