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.
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
--jsonand non---jsoncalls 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=1skips 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
--jsonfits withllms.txt,api-index.json, and the Claude Code Skills.