Quick start
Wire praxec into your coding agent and watch it try the wrong move — and find it isn't there. The guardrail in about a minute.
You’re going to give your agent a rule it cannot break, then watch it try. In about a minute you’ll see the agent reach for an action, get refused, and watch the right move appear only once it’s earned. No prompt-wrangling — the rule is in the structure.
1. Create a config file
Save this as ship-guard.yaml:
version: "1.0.0"
workflows:
ship_guard:
initialState: unchecked
states:
unchecked:
transitions:
run_check: { target: checked } # the only move offered here
checked:
transitions:
ship: { target: shipped } # `ship` exists ONLY in this state
shipped: {} # terminal — done
A three-state machine. The rule lives in its shape: ship is declared only inside checked, and the only way into checked is run_check. So from unchecked, ship is not a move that exists. Right now run_check just advances the state — we make it run your real test suite below.
2. Start the gateway
praxec serve --config ship-guard.yaml
praxec is now running as an MCP server over stdio, waiting for a host to connect.
3. Wire it into your coding agent
Declare praxec serve as an MCP server in your host’s config. The editor guide covers each host (Claude Code, Cursor, Zed, VS Code, Claude Desktop) in depth.
For Claude Desktop on macOS, the config lives at ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"praxec": {
"command": "/absolute/path/to/praxec",
"args": ["serve", "--config", "/absolute/path/to/ship-guard.yaml"]
}
}
}
Absolute paths — relative paths won’t resolve when the host launches the process.
4. Restart your host, then ask it to ship
Restart your agent (Claude Code, Cursor, etc.). It now has exactly two tools — praxec.query (reads) and praxec.command (writes) — regardless of how much you wire in later.
Now ask it: “Start the ship_guard workflow and ship the change.” Here’s what crosses the wire:
# It starts the workflow:
→ praxec.command { "definitionId": "ship_guard" }
← { "workflow": { "id": "wf_01H…", "state": "unchecked", "version": 1 },
"result": { "status": "started" },
"links": [ { "rel": "run_check", "method": "praxec.command",
"args": { "workflowId": "wf_01H…", "expectedVersion": 1, "transition": "run_check" } } ] }
ship is not in links. It isn’t blocked — it isn’t on the menu. If the agent reaches for it anyway:
→ praxec.command { "workflowId": "wf_01H…", "expectedVersion": 1, "transition": "ship" }
← { "result": { "status": "rejected" },
"error": { "code": "INVALID_TRANSITION",
"message": "Transition 'ship' is not valid from state 'unchecked'.",
"attemptedTransition": "ship" },
"links": [ { "rel": "run_check", … } ] } # the refusal hands back the legal move
The agent follows the only legal move, and ship appears:
→ praxec.command { "workflowId": "wf_01H…", "expectedVersion": 1, "transition": "run_check" }
← { "workflow": { "state": "checked", "version": 2 },
"result": { "status": "executed" },
"links": [ { "rel": "ship", … } ] } # now it's a move
That’s the whole idea: the wrong action wasn’t discouraged, it was unreachable — and the right one became available only once it was earned. You didn’t write a prompt. You wrote nine lines of YAML.
That’s TDD —
redhas no write-implementation move until a test fails. That’s deploy-gating —deployisn’t a move until tests pass. Same machine, your labels.
5. Make the check real
Right now run_check is a placeholder that just advances. Point it at your actual test command so the gate has teeth — add a cli connection and give the transition an executor:
version: "1.0.0"
connections:
shell:
kind: cli
command: npm # or: cargo, pytest, /bin/bash …
workflows:
ship_guard:
initialState: unchecked
states:
unchecked:
transitions:
run_check:
target: checked
executor: { kind: cli, connection: shell, args: ["test"] }
checked:
transitions:
ship: { target: shipped }
shipped: {}
Now run_check runs npm test. If it fails, the transition fails and you stay in unchecked — so ship never becomes reachable. The agent cannot ship past a red suite, because the move to do so only exists on the other side of a green one.
What just happened
The headline: the agent could only ever do what the workflow’s current state allowed. The illegal move wasn’t a thing it could call, and every response — including the refusal — handed back the moves that were legal, so it could recover on its own. That’s the guardrail, and it holds without a single line in the system prompt.
You also got, for free:
- Audit — every step (start, the rejected
ship, therun_check) emitted a structured JSON event. You have a complete, replayable trace of what the agent did. - Schema validation — malformed input is rejected before it reaches your executor.
- A fixed tool surface — the model always sees two tools, whether you have one workflow or five hundred. (That’s also where the token savings come from at scale — see the cost analysis.)
Next: add an approval gate so some moves wait for a human (actor: human — the agent literally can’t submit them), see how discovery and search keep the surface flat, or compose a small cognitive architecture with skills and multi-step workflows.