Skip to content

Drive the CLI from an AI agent loop

When to use this

You’re building an agent loop (Claude Code, a LangChain tool, a one-off shell harness) that needs to call netstead and parse the result. You don’t want to spin up MCP for a stateless one-shot call.

Quick example

Wrap netstead validate --json as a Python tool the LLM can invoke. The function parses stdout, summarises the report, and caps the issue list to keep agent context cost predictable:

import json, subprocess

def validate(source: str) -> dict:
    """Tool function an LLM agent can call."""
    result = subprocess.run(
        ["netstead", "validate", "--json", source],
        capture_output=True, text=True, check=False,
    )
    report = json.loads(result.stdout)
    return {
        "ok": result.returncode == 0,
        "error_count": sum(1 for i in report["issues"] if i["severity"] == "error"),
        "spec_version": report["spec_version"],
        "issues": report["issues"][:5],   # cap context cost
    }

print(validate("packages/netstead/netstead/fixtures/leavenworth/csv"))

Step-by-step

1. Every command supports --json

Add --json to any netstead (or corral) CLI command and the output becomes a single machine-readable JSON document on stdout — pipe into jq, save to a file, feed to a script or AI agent. Default output is human-readable rich panels:

netstead info     --json <source>
netstead validate --json <source>
netstead quality  --json <source>
netstead scope from-nodes      --json <source> 1 25 50
netstead clean simplify-geometry --json <source>
netstead bench    --json <source>

The --json contract: exactly one parseable JSON document on stdout, no log prefix, no progress bars, no trailing whitespace. json.loads(proc.stdout) always works on a successful run.

2. Stderr stays separate

Rich output (panels, tables, progress, approval prompts) goes to stderr. With --json on stdout the parser stays clean even when an approval prompt fires on stderr — the agent loop can either suppress stderr or surface it as side-channel feedback:

result = subprocess.run(
    ["netstead", "validate", "--json", source],
    capture_output=True, text=True,
)
data = json.loads(result.stdout)        # always parseable
diagnostics = result.stderr             # human-readable, optional to display

3. Pre-approve gated operations

Mutating commands (clean, edit sessions) prompt for confirmation by default. In an agent loop, set the env var once instead of repeating --yes per call. The env var is preferable for agents because it scopes to the process tree and survives subprocess calls from inside the loop:

import os, subprocess
env = {**os.environ, "CORRAL_AUTO_APPROVE": "1"}
subprocess.run(["netstead", "clean", "--json", source], env=env, check=False)

--yes on the command line works too.

4. Schema stability

The JSON shapes are stable inside a major version. Three types your loop is most likely to parse:

Type Shape (abridged) Returned by
ValidationReport {issues: [{severity, category, code, message, table, column, row, fix_hint}], spec_version} validate, quality
EditResult {success, table, rows_changed, history_entry_id, dry_run, issues} clean, edit ops
Command summary per-command {source, ...} dict, documented in API ref info, bench, scope

Full details: API reference.

5. Exit codes

Check returncode and parse the JSON in agent loops — returncode != 0 plus an issues array tells the model what went wrong:

Command Exit 0 Exit 1 Exit 2
validate no ERROR-severity issues ≥1 ERROR issue CLI usage error
quality always (issues are WARNING/INFO) — CLI usage error
clean / edit success or pure dry-run failed precondition CLI usage error
others success unhandled error CLI usage error

Common variations

Default — subprocess.run + json.loads

The simplest pattern: one shell out, one JSON parse, hand the dict back to the model.

```python
import json, subprocess
out = subprocess.check_output(["netstead", "info", "--json", src])
report = json.loads(out)
```
Quick shell filter via jq

Useful inside ad-hoc agent shells where you want only ERROR issues.

netstead validate --json src | jq '.issues[] | select(.severity=="error")'
Stateful flows (sessions, history)

The CLI is stateless. For multi-step agent flows that need to inspect intermediate state, use MCP instead.

See Wire the MCP server.

Streaming output

Not supported — commands emit one document on completion. For long-running operations, poll a sidecar log or use the HTTP server.

Pitfalls

  • Don’t parse stderr. It’s intentionally human-formatted (rich panels, colour, prompts). Mixing it into your parser breaks on the next release.
  • Don’t mix --json and non---json calls in the same loop. Pick one and stick to it — the model gets confused when half the tool outputs are JSON and half are panels.
  • Auto-approve makes destructive ops silent. CORRAL_AUTO_APPROVE=1 skips every confirmation including ones the user might want to be asked about. Scope the env var to the agent subprocess; don’t export it shell-wide.

See also

  • MCP tools reference — richer, stateful surface for agents that can speak MCP.
  • AI surface — how --json fits with llms.txt, api-index.json, and the Claude Code Skills.