Quickstart
PyxGrant is one Go binary. Build it with Go 1.26, then run the hostile-server walkthrough to see every control fire on your machine.
go build -o pyxgrant ./cmd/pyxgrant pyxgrant policy init # writes a starting policy to the state directory pyxgrant selftest # checks the policy, the state directory, and core controls pyxgrant demo # 26 sections against a hostile MCP server
State, pins, approvals, and the audit log live in %LOCALAPPDATA%\PyxGrant on Windows and ~/.pyxgrant elsewhere. Set PYXGRANT_HOME or -state to move them. policy init -hardened writes the stricter profile instead of the permissive default.
MCP servers
Find the servers on a machine, then rewrite the client config so each one runs behind PyxGrant. discover also lists ungoverned local model ports (Ollama, LM Studio, vLLM-style) with no gateway in front. -vault moves plaintext tokens from the config into the OS vault and injects them at start. -undo restores the original.
pyxgrant agents scan pyxgrant discover pyxgrant wrap -config .cursor/mcp.json -dry-run pyxgrant wrap -config .cursor/mcp.json -vault
To run one server by hand, put it after --. For a Streamable HTTP server, run the reverse proxy in front of it.
pyxgrant stdio -server github -- npx -y @modelcontextprotocol/server-github pyxgrant http -upstream https://mcp.example.com -listen 127.0.0.1:8710
On Windows, stdio wrap starts the child. The scope token is passed in the environment there, not through an inherited pipe, so a process that can read that child's environment can read the token.
Coding-agent hook
Claude Code and Cursor call a command before each built-in tool runs. pyxgrant hook reads the call on stdin and answers allow, ask, or deny, confined to the workspace and checked for blast radius. For Claude Code, add it as a PreToolUse hook in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{ "matcher": "*", "hooks": [{ "type": "command", "command": "pyxgrant hook claude-code" }] }
]
}
}
By default a path outside the workspace asks, and a high-blast call asks. -outside-workspace deny and -blast-action block refuse them instead. -egress-allow limits which hosts shell commands can reach.
Add a PostToolUse hook so a document the agent just read is scanned too. A PDF or Office file that never crossed MCP is extracted; active content such as a macro or PDF JavaScript is blocked, and the text is checked for injection and secrets.
{
"hooks": {
"PostToolUse": [
{ "matcher": "*", "hooks": [{ "type": "command", "command": "pyxgrant hook claude-code" }] }
]
}
}
Observe, then enforce
Set the enforcement field in the policy. In observe mode PyxGrant decides every call as it would, records what it would have refused or held as an observe: finding, and lets the call through. Redaction still applies.
{ "enforcement": "observe" }
pyxgrant stats # what fired, from the audit log
pyxgrant policy learn # propose least privilege from observed use
pyxgrant replay -candidate new.json # what the new policy would deny, hold, or allow
pyxgrant simulate -tool run_command -args '{"cmd":"rm -rf /"}'
Switch to "enforce" when the record looks right. pyxgrant policy pin -set records the policy's hash, so a later edit is not adopted until someone pins it again.
Approvals
Create an approver key and add its public key to hitl.approver_keys in the policy. Until a key or approval secret is configured, approvals are refused. A held call that nobody answers is refused after ten minutes.
pyxgrant approver init pyxgrant approvals pyxgrant approve <id> pyxgrant deny <id> pyxgrant freeze -class pay # hold every payment tool until resume pyxgrant resume -class pay pyxgrant contain act -verb pause -actor ops pyxgrant contain undo -id <id>
Your own agents
For LangGraph, CrewAI, or plain Python, run the decision service on loopback. Your code asks before each tool call, runs it only on allow, then sends the result back to be cleaned before the model sees it.
pyxgrant decide serve -server claims
# decision service on 127.0.0.1:8760
POST /v1/tools {"tools": [...]} register and pin the tool list
POST /v1/decide {"tool": "read_hr", "arguments": {...}}
→ {"decision": "allow", "call_id": "...", "arguments": {...}}
POST /v1/result {"call_id": "...", "text": "..."}
→ {"decision": "allow", "result": {...}} secrets redacted, injection stripped
It listens on loopback only, unless PYXGRANT_DECIDE_TOKEN is set. Enforcement holds only if your code honors the answer.
Model proxy
Point your model client at the proxy. It meters every call by identity, refuses with a 429 once the budget is spent, and can hold named models for approval. Each forwarded completion also spends one step on the same agency counter as a tool call; a spent counter returns 429 agency-exhausted. Under the hardened profile, a model with no price is refused with 403 model-unpriced. Model spend covers the flags.
pyxgrant llm -budget-cost 5 -budget-window 24h -max-calls-per-min 60 -injection-action hold
Refusal codes
A refused MCP call returns a JSON-RPC error and is not forwarded. These are the codes the demo produces.
| Code | Meaning |
|---|---|
-32001 | The tool is quarantined: it shadows another tool, or changed after approval |
-32003 | Denied by policy |
-32006 | Data from a sensitive source can't cross to a public tool |
-32007 | An exfiltration vector in outbound arguments, such as a markdown image with data in the URL |
-32010 | Injected instructions, including a poisoned memory write |
-32017 | A write to the agent's own MCP config or to PyxGrant's policy |
-32018 | Blast radius over the limit |
-32019 | A reworded read-back of quarantined memory |
-32023 | The grant behind this session was revoked |
Verify the record
Each session's log is hash-chained and its head sealed with Ed25519. Verify one session, a file, or everything. Keep the signing key away from the log with audit.signing_key_path; if it sits beside the log, a local admin could shorten and re-sign it, and PyxGrant warns. Under the hardened profile the seal must use audit.signing_key_kms. prove -anchor must live in a tree that does not overlap the pack.
pyxgrant audit list pyxgrant audit verify -session <id> pyxgrant audit verify-all pyxgrant receipt issue -session <id> pyxgrant receipt verify -file receipt.json -bundle bundle.json
Commands
| Job | Commands |
|---|---|
| Find | agents scan discover processes shadow saas idp access |
| Route | wrap stdio http hook decide llm a2a agui browser capture endpoint |
| Decide in a domain | pay ot voice sandbox |
| Hold and contain | approvals approve deny freeze resume contain grant pins rollback |
| Tune | policy simulate replay eval stats |
| Prove | audit receipt ediscovery disclosures reconcile report |
| Test | demo benchmark redteam perf bypass boundaries |
| Operate | console agent enroll service status |
Coverage map
pyxgrant compliance prints the product's own map against published frameworks, with what is covered, partial, or outside a runtime gateway:
| Framework | Covered | Partial | Not covered |
|---|---|---|---|
| OWASP Top 10 for LLM Applications (2025) | 5 | 2 | 3 |
| OWASP Top 10 for Agentic Applications (2025) | 8 | 2 | 0 |
| NIST and CSA agentic guidance (2026) | 4 | 1 | 0 |
Not covered in the LLM list: data and model poisoning, vector and embedding weaknesses, and misinformation. Those sit in the model and retrieval layers, outside a gateway. pyxgrant compliance also maps the EU AI Act, ISO 42001, NIST AI RMF, and the HIPAA Security Rule's technical safeguards. Those last maps are an engineering view, not a certification.