A contract-first toolchain for CLI interfaces.
CLI Contracts is a contract-first toolchain for CLI interfaces. It does not implement your CLI. Instead, it defines the external contract of your CLI and uses that contract to generate types, documentation, wrappers, contract tests, and breaking-change reports.
CLI tools are APIs too — but most of them are documented manually, tested loosely, and changed without compatibility checks. CLI Contracts brings an OpenAPI-like contract-first workflow to command line interfaces: define the contract once, then validate, generate, test, and diff.
For teams that expose CLIs to users, CI/CD pipelines, automation scripts, and AI agents, CLI Contracts provides a single source of truth for commands, inputs, outputs, exit codes, generated files, streams, tests, and breaking changes.
CLIs are APIs. They have inputs, outputs, errors, compatibility concerns, and consumers — humans, scripts, CI jobs, and AI agents.
But unlike HTTP APIs, most CLIs do not have a contract-first workflow. Documentation drifts from implementation, breaking changes go undetected, and there is no standard way to validate that a CLI conforms to its own specification.
CLI Contracts brings an OpenAPI-like workflow to command line tools: define the contract once, then use it to generate types, docs, wrappers, tests, and breaking-change reports.
CLI Contracts can also provide AI-agent execution policies through option-level effect declarations and runtime introspection (--introspect). Effects declare what a command does to the outside world (file writes, network calls, risk levels, idempotency) at both command and option granularity, enabling deterministic policy derivation without hand-written metadata. The x-agent extension remains available for non-derivable supplementary information (rollback, human review, recommendations).
Use the same contract to:
- Define commands, arguments, options, exit codes, stdout/stderr schemas, files, and streams in a single YAML contract
- Validate contracts for structural correctness, duplicate IDs, invalid refs, and exit code consistency
- Generate TypeScript types, CLI wrappers, Markdown documentation, and custom output via Handlebars templates
- Test real CLI implementations against the contract
- Diff contract versions to detect breaking changes before release
- Declare execution effects — option-level and command-level effect declarations (writes, network, risk level, idempotency) with runtime introspection via
--introspect - Declare AI-agent execution policies — supplementary agent-facing metadata (rollback, human review, recommendations) via
x-agent - Audit contract design quality, propose
x-agentpolicies, generate test cases, explain diffs, check LLM command conformance against the reference specification, and draft contracts from existing sources — all via LLM-backed commands
Key contract features:
- stdout, stderr, and generated files can be contracted per exit code with format and JSON Schema
- stdin/stdout/stderr streaming is modeled with framing, item schemas, and flush policies
- file arguments and options carry media types, encodings, and schemas
$refand shared components allow modular, reusable definitions
npm install --save-dev cli-contractsCLI Contracts is a development-time toolchain. Install it as a devDependency and run commands via npx or npm scripts.
# Initialize a new contract
npx cli-contracts init --name my-tool
# Validate
npx cli-contracts validate
# Generate docs + code (requires cli-contracts.config.yaml)
npx cli-contracts generate
# Generate Markdown docs only
npx cli-contracts docs --output docs/cli-reference.md
# Run contract tests against a real CLI
npx cli-contracts test
# Detect breaking changes
npx cli-contracts diff old.yaml new.yaml
# Introspect derived execution policy without running
npx cli-contracts generate --introspect
# Audit contract design quality (requires agent-contracts-runtime)
npx cli-contracts audit cli-contract.yaml --adapter gemini
# Check LLM command conformance against the reference specification
npx cli-contracts check-reference path/to/cli-contract.yaml --adapter openai
# Generate a contract draft from an existing README
npx cli-contracts suggest --from-readme README.md --adapter cursorAfter running generate, you get:
src/generated/
types.ts # argument/option interfaces, exit code unions, result types
commands.ts # typed CLI execution wrappers
schemas.ts # JSON Schema constants and exit code arrays
program.ts # Commander program definition (createProgram + CommandHandlers + --introspect)
policy.ts # deterministic policy derivation engine (when effects are declared)
docs/
cli-reference.md # human-readable CLI reference documentation
The primary artifact is cli-contract.yaml. It describes one or more CLI executables, their commands, inputs, outputs, and behavior.
cli_contracts: 0.1.0
info:
title: My CLI Contracts
version: 1.0.0
command_sets:
my-tool:
summary: My command line tool.
global_options:
- name: verbose
aliases: [v]
schema:
type: boolean
default: false
commands:
users.import:
summary: Import users from CSV.
arguments:
- name: input
required: true
description: Input CSV file.
schema:
type: string
file:
mode: read
exists: true
media_type: text/csv
options:
- name: dry-run
aliases: [n]
description: Validate only.
schema:
type: boolean
default: false
exits:
'0':
description: Import succeeded.
stdout:
format: json
schema:
$ref: '#/components/schemas/ImportResult'
'2':
description: Invalid argument.
stderr:
format: json
schema:
$ref: '#/components/schemas/Error'
effects:
risk_level: high
requires_confirmation: true
writes:
- target: "user database"
description: "Writes imported users to the database"
idempotent: false
x-agent:
recommended_before_use:
- Validate the CSV schema.
- Run with --dry-run before actual import.
components:
schemas:
ImportResult:
type: object
required: [status, importedCount]
properties:
status:
type: string
importedCount:
type: integer
minimum: 0
Error:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string| Field | Required | Description |
|---|---|---|
cli_contracts |
Yes | Specification version (0.1.0) |
info |
Yes | Title, version, description, license, contact |
artifact_slots |
No | Named artifact slots declaring what the tool reads/writes at an abstract level |
command_sets |
Yes | One or more CLI executables or command groups |
components |
No | Shared schemas, examples, and reusable definitions |
| Field | Required | Description |
|---|---|---|
executable |
No | User-facing executable name. Defaults to the command set key |
summary |
No | Short description |
commands |
Yes | Map of commands keyed by stable command ID |
global_options |
No | Options accepted by all commands in this set |
env |
No | Public environment variables (part of the interface) |
x-stdin |
No | Declares stdin policy for the command set (e.g. no command reads from stdin) |
| Field | Required | Description |
|---|---|---|
summary |
Yes | Short description |
description |
No | Long description |
path |
No | CLI subcommand path override. Defaults to ID with . replaced by spaces |
usage |
No | Human-readable usage examples |
arguments |
No | Positional arguments |
options |
No | Named options and flags |
effects |
No | Base execution effects (always active regardless of options) |
constraints |
No | Input constraints (mutuallyExclusive, requiredOneOf) |
streams |
No | stdin/stdout/stderr contracts during execution |
signals |
No | OS signals the command handles |
exits |
Yes | Exit-code keyed output contracts |
examples |
No | Usage examples |
x-agent |
No | AI agent supplementary metadata (non-derivable info only) |
The CLI invocation path is derived from the command ID by replacing . with spaces:
command set key: my-tool
command ID: users.import → my-tool users import
command ID: init → my-tool init
An explicit path field can override this when the CLI syntax differs from the ID.
| Field | Required | Description |
|---|---|---|
name |
Yes | Argument name |
index |
No | Positional index (defaults to array order) |
required |
No | Whether the argument is required |
description |
No | Description |
schema |
No | JSON Schema for the value |
file |
No | File contract if the value is a file path |
variadic |
No | Accepts multiple values |
| Field | Required | Description |
|---|---|---|
name |
Yes | Long option name without -- |
aliases |
No | Short aliases without - |
required |
No | Whether the option is required |
value_name |
No | Display name for the value |
description |
No | Description |
schema |
No | JSON Schema for the value |
file |
No | File contract if the value is a file path |
effects |
No | Execution effects triggered when the option is active |
repeatable |
No | Can be specified multiple times |
exits is a map keyed by exit code (0–255). Each exit defines what the command outputs.
exits:
'0':
description: Success.
stdout:
format: json
schema:
$ref: '#/components/schemas/Result'
'2':
description: Invalid argument.
stderr:
format: json
schema:
$ref: '#/components/schemas/Error'Each exit can specify stdout, stderr, and files (generated files). Output contracts have:
| Field | Required | Description |
|---|---|---|
format |
Yes | json, yaml, text, ndjson, table, etc. Can be a dynamic expression (e.g. '{options.report-format}') |
required |
No | Whether this output is required (default: true when defined) |
schema |
No | JSON Schema or $ref for the output |
schemaNote |
No | Clarification about when the schema applies (e.g. conditional on an option) |
examples |
No | Named examples |
Arguments or options whose values point to files can declare file contracts:
file:
mode: read # read, write, append, readWrite
exists: true # must exist before execution
media_type: text/csv
encoding: utf-8
schema:
$ref: ./schemas/input.schema.jsonstreams models stdin/stdout/stderr during execution (as opposed to exits, which models output at exit).
streams:
stdin:
required: true
format: ndjson
framing:
type: line-delimited
delimiter: '\n'
item_schema:
$ref: '#/components/schemas/LogEvent'
stdout:
format: ndjson
framing:
type: line-delimited
item_schema:
$ref: '#/components/schemas/FilteredEvent'
flush:
policy: perItemsignals:
SIGINT:
description: Gracefully stops processing and flushes output.
SIGTERM:
description: Immediately terminates.artifact_slots declares what a tool reads and writes at an abstract level, without referencing any specific project artifact. Each slot has a name, optional description, and a direction (read, write, or readwrite).
artifact_slots:
spec-source:
description: "Specification files to lint/verify"
direction: read
design-models:
description: "Design model definitions"
direction: read
implementation-source:
description: "Source code for annotation/traceability check"
direction: read
generated-output:
description: "Files generated from models"
direction: write| Field | Required | Description |
|---|---|---|
description |
No | What this slot represents |
direction |
Yes | read, write, or readwrite |
Slots are domain-agnostic. The binding of slots to concrete project artifacts is handled by artifact_bindings in agent-contracts, not in cli-contracts.
When artifact_slots is declared, command effects can reference slot names instead of free-text targets:
commands:
lint:
effects:
reads: [spec-source, design-models]
writes: []
build:
effects:
reads: [spec-source, design-models]
writes: [generated-output]The validator checks that slot references in effects.reads / effects.writes exist in the document-level artifact_slots. Referencing an undefined slot produces a validation error.
Commands and options can declare execution effects. Effects support two formats for reads and writes:
- Slot references (string array) — references to
artifact_slotsentries, used when slots are declared - Descriptive objects (object array) — free-text targets with metadata, used when slots are not declared
Slot reference format:
effects:
reads: [spec-source, design-models]
writes: [generated-output]Descriptive object format:
effects:
risk_level: high
requires_confirmation: true
writes:
- target: "user database"
description: "Writes imported users to the database"
idempotent: false
network:
description: "Calls external LLM API"
domains: ["api.openai.com"]
idempotent: true
idempotency_key: "request hash"Properties prefixed with x- carry domain-specific metadata. The x-agent extension provides non-derivable agent-facing supplementary information:
x-agent:
expectedDurationMs: 60000
retryableExitCodes: [1, 12]
recommended_before_use:
- Validate the CSV schema.
- Run with --dry-run before actual import.
rollback:
strategy: "git checkout"cli-contracts.config.yaml defines how tooling validates, generates, and tests. The contract defines what the interface is; the config defines how to process it.
version: 0.1.0
input:
files:
- cli-contract.yaml
generators:
typescript:
enabled: true
output: ./src/generated
templates: builtin:typescript
options:
emitTypes: true
emitClient: true
emitValidators: true
emitProgram: true
markdown:
enabled: true
output: ./docs/cli-reference.md
templates: builtin:markdown
options:
includeSchemas: true
includeExtensions: true
execution_profiles:
local:
default: true
command_sets:
my-tool:
command: my-tool
contract_tests:
enabled: true
profile: local
cases_dir: ./tests/cli-contracts
timeout_ms: 30000| Command | Description |
|---|---|
cli-contracts init |
Initialize a contract file or project layout |
cli-contracts validate |
Validate contract syntax, refs, and structure |
cli-contracts generate |
Run code generators from config |
cli-contracts docs |
Generate Markdown documentation |
cli-contracts test |
Run contract tests against a real CLI |
cli-contracts diff [old] [new] |
Detect breaking changes between versions |
cli-contracts extract |
Extract a subset of the contract for specific commands |
cli-contracts propose-agent-policy [contract] |
Detect missing or inconsistent x-agent policies via LLM |
cli-contracts audit [contract] |
Semantic audit of CLI contract design quality |
cli-contracts propose-tests [contract] |
Propose contract test cases via LLM analysis |
cli-contracts explain-diff [old] [new] |
Explain contract diff in human- and agent-readable form |
cli-contracts check-reference [contract] |
Check LLM command conformance against the reference specification |
cli-contracts suggest |
Generate a contract draft from existing CLI sources |
| Option | Alias | Description |
|---|---|---|
--config <file> |
-c |
Path to cli-contracts.config.yaml |
--verbose |
-v |
Enable verbose output |
--format <format> |
-F |
Output format for core commands (yaml or json). Does not apply to LLM commands which use --report-format |
--quiet |
-q |
Suppress informational/verbose output only. Does not suppress primary structured stdout |
--introspect |
Output derived execution policy as JSON without executing the command (generated when effects are declared) | |
--version |
-V |
Print version |
--help |
-h |
Show help |
| Option | Alias | Default | Description |
|---|---|---|---|
--name <name> |
-n |
Executable name for the initial command set | |
--multi-command-set |
-m |
false |
Scaffold multiple command sets |
--output <dir> |
-o |
. |
Output directory |
--with-config |
false |
Also generate cli-contracts.config.yaml |
| Option | Alias | Default | Description |
|---|---|---|---|
--file <files...> |
-f |
config input | Contract file(s) to validate |
--strict |
false |
Treat warnings as errors | |
--resolve-refs |
true |
Resolve and validate $ref targets |
| Argument | Description |
|---|---|
[generators...] |
Generator name(s) to run. If omitted, all enabled generators run |
| Option | Alias | Default | Description |
|---|---|---|---|
--file <files...> |
-f |
config input | Contract file(s) |
--output <dir> |
-o |
Override output directory | |
--dry-run |
-n |
false |
Show what would be generated |
--clean |
false |
Remove output before generating |
| Option | Alias | Description |
|---|---|---|
--file <files...> |
-f |
Contract file(s) |
--output <file> |
-o |
Output file path |
| Option | Alias | Default | Description |
|---|---|---|---|
--profile <name> |
-p |
Execution profile to use | |
--case <ids...> |
Run specific test case(s) by ID | ||
--cases-dir <dir> |
Test case directory | ||
--timeout <ms> |
-t |
30000 |
Timeout per test case |
--bail |
false |
Stop on first failure |
| Argument | Description |
|---|---|
[old] |
Path to the old contract file (can be omitted when using --base/--head) |
[new] |
Path to the new contract file (can be omitted when using --base/--head) |
| Option | Alias | Default | Description |
|---|---|---|---|
--base <ref> |
Git ref for the base version (e.g. main, v1.0.0) |
||
--head <ref> |
Git ref for the head version (e.g. HEAD, feature-branch) |
||
--contract-path <path> |
-p |
cli-contract.yaml |
Contract file path within the repository (used with --base/--head) |
--breaking-only |
false |
Only report breaking changes | |
--text |
false |
Output human-readable text instead of structured data. Disables schema-conformant output |
| Argument | Description |
|---|---|
[contract] |
Contract file to analyze (mutually exclusive with --file) |
| Option | Default | Description |
|---|---|---|
--file <file> |
-f |
Contract file to analyze (alternative to positional argument) |
--adapter <name> |
LLM adapter (mock, cursor, claude, openai, gemini) |
|
--model <name> |
Model name to pass to the adapter | |
--show-prompt |
false |
Output the constructed prompt without calling the LLM API |
--fail-on <level> |
error |
Minimum severity that causes a non-zero exit (warning, error, critical) |
--output <file> |
Write result to a file instead of stdout | |
--report-format <fmt> |
json |
Output format (json, text, or yaml) |
| Argument | Description |
|---|---|
[contract] |
Contract file to audit (mutually exclusive with --file) |
| Option | Default | Description |
|---|---|---|
--file <file> |
-f |
Contract file to audit (alternative to positional argument) |
--checks <check...> |
all | Audit dimension(s) to run (agent-policy, responsibility, exit-code, output-schema, breaking-risk) |
--adapter <name> |
LLM adapter (mock, cursor, claude, openai, gemini) |
|
--model <name> |
Model name to pass to the adapter | |
--show-prompt |
false |
Output the constructed prompt without calling the LLM API |
--fail-on <level> |
error |
Minimum severity that causes a non-zero exit |
--output <file> |
Write result to a file instead of stdout | |
--report-format <fmt> |
json |
Output format (json, text, or yaml) |
| Argument | Description |
|---|---|
[contract] |
Contract file to analyze (mutually exclusive with --file) |
| Option | Default | Description |
|---|---|---|
--file <file> |
-f |
Contract file to analyze (alternative to positional argument) |
--adapter <name> |
LLM adapter (mock, cursor, claude, openai, gemini) |
|
--model <name> |
Model name to pass to the adapter | |
--show-prompt |
false |
Output the constructed prompt without calling the LLM API |
--fail-on <level> |
error |
Minimum severity that causes a non-zero exit |
--output <file> |
Write result to a file instead of stdout | |
--report-format <fmt> |
json |
Output format (json, text, or yaml) |
| Argument | Description |
|---|---|
[old] |
Path to the old (base) contract file |
[new] |
Path to the new (head) contract file |
| Option | Default | Description |
|---|---|---|
--base <ref> |
Git ref for the base version | |
--head <ref> |
Git ref for the head version | |
--contract-path <path> |
cli-contract.yaml |
Contract file path within the repository (used with --base/--head) |
--adapter <name> |
LLM adapter (mock, cursor, claude, openai, gemini) |
|
--model <name> |
Model name to pass to the adapter | |
--show-prompt |
false |
Output the constructed prompt without calling the LLM API |
--fail-on <level> |
error |
Minimum severity that causes a non-zero exit |
--output <file> |
Write result to a file instead of stdout | |
--report-format <fmt> |
json |
Output format (json, text, or yaml) |
At least one --from-* option is required.
| Option | Default | Description |
|---|---|---|
--from-readme <file> |
Path to a README file to extract CLI information from | |
--from-help <file> |
Path to a file containing --help output |
|
--from-source <file> |
Path to CLI source code file | |
--adapter <name> |
LLM adapter (mock, cursor, claude, openai, gemini) |
|
--model <name> |
Model name to pass to the adapter | |
--show-prompt |
false |
Output the constructed prompt without calling the LLM API |
--fail-on <level> |
error |
Minimum severity that causes a non-zero exit (warning, error, critical) |
--output <file> |
Write result to a file instead of stdout | |
--report-format <fmt> |
json |
Output format (json, text, or yaml) |
Verifies whether LLM-powered commands in a target cli-contract.yaml conform to the cli-contracts reference specification. Performs deterministic pre-analysis (option presence, exit code coverage, schema structure, x-agent metadata) and uses LLM for semantic evaluation of overall conformance quality.
| Argument | Description |
|---|---|
[contract] |
Contract file to check (mutually exclusive with --file) |
| Option | Default | Description |
|---|---|---|
--file <file> / -f |
Contract file to check (alternative to positional argument) | |
--adapter <name> |
LLM adapter (mock, cursor, claude, openai, gemini) |
|
--model <name> |
Model name to pass to the adapter | |
--show-prompt |
false |
Output the constructed prompt without calling the LLM API |
--fail-on <level> |
error |
Minimum severity that causes a non-zero exit (warning, error, critical) |
--output <file> |
Write result to a file instead of stdout | |
--report-format <fmt> |
json |
Output format (json, text, or yaml) |
Conformance checks include:
- Standard LLM option set (
--adapter,--model,--show-prompt,--fail-on,--output,--report-format) - Exit code coverage (0, 1, 10, 11, 12)
x-agentmetadata (safe_dry_run_option,sideEffectNote,expectedDurationMs,retryableExitCodes)- Stdout schema conformance to the agent-contracts canonical
agent-audit-result/agent-findingschema (via$refor compatible inline definition) agent-evidencebase property alignment (kind,target,location,excerpt)agent-recommended-actionproperty alignment (kind,title,command,target,rationale)- Deprecated inline handoff schema detection (
x-schema-source: handoff)
For full details on every command, option, exit code, and output schema, see the CLI Reference.
Generated code is intended to keep the CLI interface aligned with the contract. Business logic remains in your application code.
Generates typed interfaces, command wrappers, a Commander program definition, and a policy derivation engine from the contract.
src/generated/
index.ts # re-exports
types.ts # argument/option interfaces, exit code unions, result types
commands.ts # typed CLI execution wrappers
schemas.ts # JSON Schema constants and exit code arrays
program.ts # Commander program definition (createProgram + CommandHandlers + --introspect)
policy.ts # deterministic policy derivation engine (when effects are declared)
Example generated types:
export type UsersImportExitCode = 0 | 2 | 10;
export type UsersImportExitResult =
| { exitCode: 0; stdout: ImportResult }
| { exitCode: 2; stderr: Error }
| { exitCode: 10; stdout?: ImportResult; stderr: Error };The generated program.ts allows contract-driven CLI wiring:
import { createProgram } from "./generated/program.js";
const program = createProgram({
async usersImport(input, options, parentOpts) {
// only implement handlers — interface comes from the contract
},
}, "1.0.0");
program.parse();Generates human-readable CLI reference documentation with command tables, option tables, exit code contracts, schema property tables, and collapsible JSON Schema details.
Custom generators use Handlebars templates with a manifest:
templates/go/
generator.yaml
types.go.hbs
command.go.hbs
name: go
description: Generate Go command wrappers.
entrypoints:
- template: types.go.hbs
output: '{{options.packageName}}/types.go'
- template: command.go.hbs
output: '{{options.packageName}}/{{commandSet.id}}_commands.go'
each: command_setsContract tests verify that a real CLI implementation conforms to the contract by executing the actual command and checking its outputs.
Each test case:
- Invokes the real command via an execution profile
- Asserts the exit code
- Validates stdout/stderr against JSON Schema when a schema is declared
- Checks file outputs for existence, media type, and schema when file contracts are declared
- Supports
absent: trueto assert that a stream produces no output - Respects configurable timeout and working directory per profile
Non-JSON output formats (text, table, ndjson) are validated at the format level; deep schema validation applies to JSON and YAML outputs.
Test cases are YAML files:
id: users.import.success
commandSet: my-tool
command: users.import
args:
input: ./fixtures/users.csv
options:
dry-run: true
expect:
exitCode: 0
stdout:
matchesSchema: '#/components/schemas/ImportResult'
stderr:
absent: truecli-contracts test --profile localcli-contracts diff old.yaml new.yamlDetects breaking changes including:
- Removed commands or command sets
- Changed executable names
- Removed or reordered arguments
- Added required arguments or options
- Removed options or exit codes
- Incompatible schema changes
import {
parseContractFile,
validateContract,
resolveRefs,
normalizeContract,
generateMarkdown,
generateTypeScript,
CliContractsDocumentSchema,
} from "cli-contracts";
const doc = await parseContractFile("cli-contract.yaml");
const validation = validateContract(doc);
if (validation.valid) {
const resolved = resolveRefs(doc);
const ctx = normalizeContract(resolved);
const markdown = generateMarkdown(ctx);
const typescript = generateTypeScript(ctx);
}Zod schemas are also exported for programmatic use:
import { CliContractsDocumentSchema } from "cli-contracts";
const result = CliContractsDocumentSchema.safeParse(rawYamlObject);CLI Contracts provides a layered approach to AI agent interoperability: effect declarations for deterministic policy derivation, runtime introspection for pre-execution queries, and x-agent for non-derivable supplementary metadata.
Effects declare what a command does to the outside world. They can be placed at command level (always active) or option level (active only when the option is specified):
lint:
options:
- name: fix
schema: { type: boolean }
effects:
risk_level: medium
writes:
- target: "source files matching lint rules"
description: "auto-fix lint violations"
overwrite: true
idempotent: true
idempotent_note: "same lint rules + same input = no additional changes on re-run"
build:
effects:
risk_level: low
writes:
- target: "docs/, specs/"
description: "files generated from models"
idempotent: true| Field | Type | Description |
|---|---|---|
risk_level |
low | medium | high | critical |
Risk level contributed by this command/option |
writes |
string[] | EffectWrite[] |
Slot references or descriptive write side effects |
reads |
string[] | EffectRead[] |
Slot references or descriptive read side effects |
network |
boolean | NetworkEffect |
Network call side effects |
execution_mode |
normal | long-running | watch | interactive | background |
Execution mode |
requires_confirmation |
boolean |
Explicit override for confirmation requirement |
EffectWrite fields:
| Field | Type | Description |
|---|---|---|
target |
string |
Description of what is written |
description |
string |
Details about the write operation |
overwrite |
boolean |
Whether existing files may be overwritten |
destructive |
boolean |
Whether the write is destructive |
idempotent |
boolean |
Whether this write is idempotent |
idempotency_key |
string |
Key that determines idempotency |
idempotent_note |
string |
Clarification about idempotency |
NetworkEffect fields:
| Field | Type | Description |
|---|---|---|
description |
string |
Description of the network call |
domains |
string[] |
Target domains |
requires_secrets |
string[] |
Required secret environment variables |
idempotent |
boolean |
Whether this network call is idempotent |
idempotency_key |
string |
Key that determines idempotency |
idempotent_note |
string |
Clarification about idempotency |
When a contract declares effects, the code generator adds a --introspect global option. It returns derived policy as JSON without executing the command:
$ tool lint --fix --introspect
{
"command": "lint",
"active_options": ["fix"],
"policy": {
"risk_level": "medium",
"requires_confirmation": false,
"side_effects": ["file_write"],
"reads": [],
"writes": [
{
"kind": "semantic",
"target": "source files matching lint rules",
"description": "auto-fix lint violations",
"idempotent": true,
"source": "option:fix"
}
],
"idempotent": true
}
}Policy derivation rules:
risk_level=max(command.risk_level, ...active_options.risk_level)requires_confirmation=truewhenrisk_level >= high(with explicit override)side_effects= union offile_write(from writes/file.mode) andnetwork(from network effects)idempotent=trueonly if all semantic writes AND all network effects are explicitlyidempotent: true; implicitlytruefor read-only commands
The x-agent extension carries non-derivable, agent-facing metadata. Fields that overlap with effects are deprecated:
x-agent:
recommended_before_use:
- Run without --fix first to review issues
rollback:
strategy: "git checkout"
human_review:
required: true
reason: "destructive operation"
expectedDurationMs: 120000
retryableExitCodes: [1, 12]| Field | Type | Description |
|---|---|---|
recommended_before_use |
string[] |
Steps an agent should take before executing |
rollback |
object |
Rollback instructions |
human_review |
object |
Human review requirements |
expectedDurationMs |
number |
Expected wall-clock time |
retryableExitCodes |
number[] |
Exit codes safe to retry |
preferAlternative |
string |
Suggested alternative command |
Deprecated x-agent fields (use effects instead):
| Deprecated Field | Replacement |
|---|---|
risk_level |
effects.risk_level + max aggregation |
side_effects |
effects.writes / effects.network + file.mode |
sideEffectNote |
effects.writes[].description |
requires_confirmation |
effects.requires_confirmation or derived from risk_level >= high |
requires_confirmation_when |
Option-level effects.risk_level |
dangerous_options |
Option-level effects.risk_level |
safe_dry_run_option |
Replaced by --introspect |
requires_network |
effects.network |
requires_secrets |
env[].sensitive |
reads / writes |
effects.reads / effects.writes + file.mode |
idempotent |
effects.writes[].idempotent / effects.network.idempotent |
idempotent_note |
effects.writes[].idempotent_note / effects.network.idempotent_note |
Validation produces warnings when deprecated fields are used alongside effects declarations.
Options can also carry x-agent metadata:
options:
- name: text
x-agent:
disablesStructuredOutput: trueHandoff schemas for agent-facing diagnostic output are canonically owned by agent-contracts. The canonical schemas are agent-audit-result, agent-finding, agent-recommended-action, and agent-evidence, defined in agent-contracts components.schemas.
When a CLI command returns a payload intended to be consumed as an agent handoff, cli-contracts should reference the agent-contracts schema via $ref instead of redefining the schema in components.schemas. cli-contracts remains responsible for the CLI interface contract, including command arguments, options, exit codes, stdout/stderr formats, and any CLI-specific envelope schema.
For backward compatibility, AgentAuditResult, AgentFinding, AgentRecommendedAction, and AgentEvidence are still available in cli-contract.yaml components/schemas and exported via the cli-contracts/agent subpath, but they are marked as deprecated (x-deprecated: true). New projects should reference agent-contracts schemas directly.
import type { AgentAuditResult, AgentFinding } from "cli-contracts/agent";
import { XAgentSchema, validateXAgent } from "cli-contracts/agent";AgentAuditResult (canonical: agent-contracts:components.schemas.agent-audit-result) is the standard output format for LLM-backed audit commands across the toolchain:
interface AgentAuditResult {
summary: string;
risk_level: "low" | "medium" | "high" | "critical";
findings: AgentFinding[];
recommendedActions?: AgentRecommendedAction[];
metadata?: { tool?: string; command?: string; version?: string; ... };
}AgentFinding (canonical: agent-contracts:components.schemas.agent-finding):
interface AgentFinding {
severity: "info" | "warning" | "error" | "critical";
category: string;
message: string;
id?: string;
target?: string;
location?: string;
recommendation?: string;
confidence?: number; // 0–1
evidence?: AgentEvidence[];
}AgentEvidence (canonical: agent-contracts:components.schemas.agent-evidence):
interface AgentEvidence {
kind: "file" | "command" | "schema" | "diff" | "stdout" | "stderr" | "text";
target?: string; // source identifier (file path, command ID, schema name)
location?: string; // location within the target (line number, JSON pointer)
excerpt?: string; // relevant content excerpt
}AgentRecommendedAction (canonical: agent-contracts:components.schemas.agent-recommended-action):
interface AgentRecommendedAction {
kind: "run_command" | "edit_file" | "review" | "confirm" | "block" | "ignore";
title: string;
command?: string;
target?: string;
rationale?: string;
}Six commands use LLM integration via agent-contracts-runtime (optional peer dependency) to perform semantic analysis:
| Command | Purpose |
|---|---|
propose-agent-policy |
Detect missing or inconsistent x-agent policies |
audit |
Semantic audit of contract design quality |
propose-tests |
Generate test case proposals from contract definitions |
explain-diff |
Generate human-readable explanations of contract diffs |
check-reference |
Verify LLM command conformance against the reference specification |
suggest |
Draft a contract from existing CLI sources (README, --help, source code) |
# Propose x-agent policies for commands missing them
cli-contracts propose-agent-policy cli-contract.yaml --adapter gemini
# Audit contract design quality
cli-contracts audit cli-contract.yaml --checks agent-policy --adapter claude
# Propose test cases
cli-contracts propose-tests cli-contract.yaml --adapter openai --report-format yaml
# Explain a diff between contract versions
cli-contracts explain-diff old.yaml new.yaml --adapter gemini
# Check LLM command conformance against the reference specification
cli-contracts check-reference path/to/cli-contract.yaml --adapter openai
# Generate a contract draft from an existing README
cli-contracts suggest --from-readme README.md --adapter cursor
# Inspect the prompt without making an LLM call
cli-contracts propose-agent-policy cli-contract.yaml --show-promptThese commands share a common option interface:
| Option | Description |
|---|---|
--adapter <name> |
LLM adapter: mock, cursor, claude, openai, gemini |
--model <name> |
Model name to pass to the adapter |
--show-prompt |
Output the constructed prompt without calling the LLM API |
--fail-on <level> |
Minimum severity that causes a non-zero exit (warning, error, critical) |
--output <file> |
Write result to a file instead of stdout |
--report-format <fmt> |
Output format: json, text, or yaml |
All LLM commands declare effects.network for their API calls, with expectedDurationMs: 120000 and retryableExitCodes: [1, 12] in x-agent.
Install the optional runtime dependency to enable LLM calls:
npm install agent-contracts-runtimeWithout it, use --show-prompt to inspect the prompt context that would be sent to the LLM.
Required environment variables depend on the chosen adapter:
| Adapter | Environment Variable |
|---|---|
cursor |
CURSOR_API_KEY |
gemini |
GEMINI_API_KEY |
openai |
OPENAI_API_KEY |
claude |
ANTHROPIC_API_KEY |
OpenCLI is an important effort toward a standard, language-agnostic description format for command line interfaces.
CLI Contracts started with a similar problem space, and OpenCLI was considered as a possible foundation. However, CLI Contracts intentionally uses its own contract format because it focuses on a broader contract-first workflow:
- per-exit stdout/stderr/file contracts
- JSON Schema-based output validation
- streaming input/output contracts
- contract tests against real CLI implementations
- breaking-change detection
- TypeScript type and wrapper generation
- AI-agent execution policies such as side effects, risk level, idempotency, and confirmation requirements
- LLM-backed semantic audit, test case generation, and contract drafting
OpenCLI primarily describes how a CLI is invoked. CLI Contracts describes how a CLI behaves as an interface.
OpenCLI is closest to a description format. CLI Contracts is closer to a contract lifecycle toolchain.
The goal is not to compete with OpenCLI as a standard, but to provide a practical toolchain for teams that need stronger guarantees around CLI compatibility, automation, and AI-agent execution policies.
OpenCLI is a description format. CLI Contracts is a contract lifecycle toolchain.
OpenCLI focuses on describing CLI shape. CLI Contracts focuses on defining, generating, testing, and evolving CLI behavior.
| Area | OpenCLI | CLI Contracts |
|---|---|---|
| Primary goal | Standard CLI description format | Contract-first CLI toolchain |
| Invocation model | Commands, arguments, options | Commands, arguments, options |
| Exit codes | Basic description | First-class output contracts per exit code |
| stdout/stderr | Limited | Format and schema per exit |
| File I/O | Limited or implementation-dependent | First-class file contracts |
| Streams | Not the main focus | First-class stdin/stdout/stderr stream contracts |
| Validation | Specification validation | Contract validation and $ref resolution |
| Code generation | Possible ecosystem use case | Built-in TypeScript generation |
| Contract testing | Not the core focus | Built-in contract tests against real CLIs |
| Breaking changes | Possible use case | Built-in diff command |
| AI agents | Possible use case | Explicit x-agent policy extension |
| LLM-backed audit | Not applicable | Built-in semantic audit, test proposal, and contract generation via LLM |
| Design stance | Standard-first | Toolchain-first |
CLI Contracts does not replace Commander, oclif, Click, Typer, Cobra, or other CLI frameworks.
You can build your CLI with any framework. CLI Contracts defines the external contract of the CLI and provides validation, generation, testing, and compatibility tooling around it.
In other words:
- CLI frameworks implement command behavior
- CLI Contracts defines and verifies the interface contract
| Tool | Focus | Relationship to CLI Contracts |
|---|---|---|
| Commander | Node.js command parser | CLI Contracts can generate Commander program definitions |
| oclif | Node.js CLI framework | Can be used as the implementation layer |
| Click / Typer | Python CLI frameworks | Can be described by CLI Contracts |
| Cobra | Go CLI framework | Can be described by CLI Contracts |
| OpenCLI | CLI description specification | Different scope; CLI Contracts focuses on the full contract lifecycle |
| OpenAPI | HTTP API contracts | Conceptual inspiration for contract-first workflows |
CLI Contracts is not currently based on OpenCLI.
Import/export support may be considered in the future if there is enough demand and if the mapping can preserve CLI Contracts features such as exit-specific output contracts, stream contracts, contract tests, and agent policies.
- Contract first —
cli-contract.yamlis the canonical source of truth - Commands are a map — stable IDs for diffing, codegen, and references
- Exit codes are first-class — per-exit stdout/stderr/files contracts
- Files are first-class — input/output files with media types and schemas
- Streams are first-class — stdin/stdout/stderr during execution with framing
- JSON Schema compatible — data schemas use JSON Schema
- Template-based generation — Handlebars templates for any language
- Extensible via
x-*— domain metadata without polluting the core schema - Runtime is config, not contract — binary paths, Docker invocation, and env vars belong in config
CLI Contracts uses JSON Schema for data contracts embedded in arguments, options, exit outputs, streams, and file definitions.
Currently supports a practical subset of JSON Schema. Full dialect compatibility (targeting draft 2020-12) will be finalized before 1.0. The contract format itself is validated using Zod, and machine-readable JSON Schema files for the contract and config formats are published in the schemas/ directory:
schemas/cli-contract.schema.json— schema forcli-contract.yamlschemas/cli-contracts.config.schema.json— schema forcli-contracts.config.yaml
These can be used for IDE autocompletion:
# yaml-language-server: $schema=./node_modules/cli-contracts/schemas/cli-contract.schema.json
cli_contracts: 0.1.0repo/
cli-contract.yaml # contract (source of truth)
cli-contracts.config.yaml # tooling config
schemas/ # external JSON Schemas
templates/ # custom generator templates
src/generated/ # generated TypeScript
docs/
cli-reference.md # generated documentation
tests/
cli-contracts/ # contract test cases
users.import.success.yaml
CLI Contracts is currently pre-1.0. The current contract format version is 0.1.0.
Until 1.0:
- The contract format may change based on feedback.
- Breaking format changes will be documented in release notes.
- Migration guidance will be provided when possible.
- The
diffcommand is intended to help detect compatibility impact between contract versions, but does not guarantee automatic migration.
MIT