PyxGrant / Documentation

Documentation

Build the binary, route your MCP servers and coding agents through it, watch in observe mode, then enforce. pyxgrant <command> -h lists the flags.

POST 127.0.0.1:8760/v1/decidedecision service

      

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.

CodeMeaning
-32001The tool is quarantined: it shadows another tool, or changed after approval
-32003Denied by policy
-32006Data from a sensitive source can't cross to a public tool
-32007An exfiltration vector in outbound arguments, such as a markdown image with data in the URL
-32010Injected instructions, including a poisoned memory write
-32017A write to the agent's own MCP config or to PyxGrant's policy
-32018Blast radius over the limit
-32019A reworded read-back of quarantined memory
-32023The 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

JobCommands
Findagents scan discover processes shadow saas idp access
Routewrap stdio http hook decide llm a2a agui browser capture endpoint
Decide in a domainpay ot voice sandbox
Hold and containapprovals approve deny freeze resume contain grant pins rollback
Tunepolicy simulate replay eval stats
Proveaudit receipt ediscovery disclosures reconcile report
Testdemo benchmark redteam perf bypass boundaries
Operateconsole 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:

FrameworkCoveredPartialNot covered
OWASP Top 10 for LLM Applications (2025)523
OWASP Top 10 for Agentic Applications (2025)820
NIST and CSA agentic guidance (2026)410

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.