AxonFlow Governance for Claude Desktop
July 17, 2026 · View on GitHub
Runtime governance for Claude Desktop at the MCP layer: an AxonFlow MCP
proxy, packaged as a one-click .mcpb Desktop Extension, that fronts your
internal MCP servers and enforces policy on every tool call — block
policy-violating calls, redact PII from responses before it reaches the
conversation, and audit everything.
Source-available (MIT). Self-hosted: tool calls are evaluated by your AxonFlow deployment — no data leaves your infrastructure.
Quickstart
1. Build the extension: ./build.sh → build/axonflow-governance-<v>.mcpb
2. Claude Desktop → Settings → Extensions → Install from file → pick the .mcpb
3. Fill the config fields: AxonFlow endpoint, Client ID/secret, Tenant,
Fail mode (keep "closed"), Backend servers file
4. Point "Backend MCP servers" at a JSON file (see config.example.json)
5. Restart Claude Desktop. Your backend tools now appear — governed.
That's it. Every tools/call is now checked against AxonFlow Decision Mode
before it runs.
Desktop proxy setup & usage walkthrough — install the governance proxy and see PII redaction + tool-call governance on a real MCP backend: Watch on YouTube
Why a proxy, not a hook
Claude Desktop has no PreToolUse/PostToolUse hooks — those are a Claude Code feature. On Desktop the only pre-execution interception point is the MCP layer. So enforcement cannot be a hook; it is this proxy, which sits between Claude Desktop and your backend MCP servers:
Claude Desktop ──stdio MCP──▶ AxonFlow proxy ──POST /api/v1/decide──▶ AxonFlow (PDP)
│ allow → forward, redact response
│ deny → block (-32001)
▼
backend MCP servers (CRM / BigQuery / back-office)
The proxy is both an MCP server (to Claude Desktop) and an MCP client (to each backend). It aggregates the backends' tools and re-exposes them as one extension.
What it enforces
On every tools/call | Behaviour |
|---|---|
| allow | forwarded to the backend unchanged |
| deny | blocked with JSON-RPC -32001; the deny reason + decision_id/trace_id surface to the user; the backend is never called |
| needs_approval | held with -32002 (HITL); not forwarded |
| response redaction | every allowed backend response is sent to AxonFlow's authoritative engine (POST /api/v1/mcp/check-output) and PII is masked before it reaches Claude's context — the cross-border-data control. Coverage tracks the platform's detectors (NIK + SSN + email + phone …); the proxy never re-implements redaction locally. |
| response blocked | if the engine hard-blocks a response (critical-PII deny, response SQLi, exfiltration), the call is denied (-32001) and the response is never forwarded |
| PDP / engine unreachable | fail-closed by default (-32003): the call is blocked. The response plane is unconditionally fail-closed — if the redaction engine is unreachable the (already-executed) response is not forwarded, even under fail-open. Opt into request-plane fail-open only with eyes open. |
Every call writes one Layer-1 audit row (session_id, leader_email,
tool_name, parameters_hash, response_record_count, duration_ms, plus
decision_id / trace_id / gateway_id for SIEM correlation). That row is
the proxy's local JSONL — it always carries leader_email.
Per-leader attribution in the platform audit trail (trust gate)
The proxy also asserts the leader identity to AxonFlow on every governed
call: X-User-Email (= AXONFLOW_LEADER_EMAIL) and X-Session-Id on both
/api/v1/decide and /api/v1/mcp/check-output. Whether that identity lands
in the platform's audit trail (audit_logs.user_email / session_id —
what the customer-portal User column shows) is decided by the platform,
not the proxy:
- Requires platform ≥ 9.9.0 (the release carrying axonflow-enterprise#2896)
with
AXONFLOW_TRUST_IDENTITY_HEADERS=trueset on the AxonFlow agent. The operator is declaring the proxy a trusted identity source. Scope that honestly: the running conversation/model can never alterleader_emailmid-session, but in a self-service install the Desktop user sets it themselves in Settings → Extensions — so turn the gate on only whereleader_emailprovisioning is controlled (e.g. MDM-managed extension settings), or where self-asserted attribution is acceptable. - Without the flag (the default), the platform ignores the headers: its
audit rows are attributed to the validated identity — the license org, or
the
AXONFLOW_USER_TOKENuser when that JWT identifies a person — not the asserted leader. Per-leader attribution then exists only in the proxy's local Layer-1 JSONL and, on decide rows, in thex_leader_identitykey insidepolicy_details(recorded regardless of the flag; gate-off check-output rows carry no leader marker platform-side). - Either way the headers are attribution-only — they never influence a policy verdict.
Configuration
Configured entirely through the Desktop Extension UI, which maps to these
environment variables (see manifest.json):
| Field | Env var | Notes |
|---|---|---|
| AxonFlow endpoint | AXONFLOW_ENDPOINT | e.g. https://app.getaxonflow.com:8090 |
| Client ID / secret | AXONFLOW_CLIENT_ID / AXONFLOW_CLIENT_SECRET | secret is masked + stored securely |
| User token (JWT) | AXONFLOW_USER_TOKEN | optional; enterprise validated-user audit — an admin-minted per-user token (see below) |
| Tenant / Org | AXONFLOW_TENANT_ID / AXONFLOW_ORG_ID | |
| Fail mode | AXONFLOW_FAIL_MODE | closed (default) or open — request plane only; response redaction is always fail-closed |
| Response redaction | AXONFLOW_REDACT_RESPONSES | always (default) · on-obligation (legacy: only on a redact_pii obligation / fail-open forward) · off (explicit opt-out footgun — disables the whole response-governance call, i.e. PII redaction and response-side SQLi/exfil hard-blocks) |
| Leader email | AXONFLOW_LEADER_EMAIL | stamped on the proxy's local Layer-1 audit rows; asserted to the platform as X-User-Email — attributed into the platform audit trail only under the trust gate (see above) |
| Backend servers file | AXONFLOW_BACKENDS_FILE | JSON map — see config.example.json |
| Audit log path | AXONFLOW_AUDIT_LOG | optional JSONL sink |
User token (AXONFLOW_USER_TOKEN): per-user validated attribution
AXONFLOW_USER_TOKEN is a per-user token your AxonFlow admin mints from
the customer portal's admin API
(POST /api/v1/admin/organizations/{org_id}/user-tokens) — see the platform's
per-user token provisioning guide
(access-gated: the enterprise repo is private; ask your AxonFlow contact if
the link 404s for you).
The proxy forwards it as the body user_token on both governed planes
(/api/v1/decide and /api/v1/mcp/check-output), and the platform — not the
proxy — validates it: HS256 signature, expiry, and the revocation deny-list.
Only admin-minted (HS256) tokens work here: the planes this proxy calls
pin the accepted algorithm to HS256, so a tenant-OIDC access token (RS256) is
rejected — OIDC per-user tokens apply to the platform's X-User-Token header
planes (MCP-server and agent-proxied REST), not to this proxy.
Unlike AXONFLOW_LEADER_EMAIL (asserted, honored only under the trust gate
above), the user token is cryptographically validated: no platform flag is
needed for its attribution to land. (If the trust gate is on and
AXONFLOW_LEADER_EMAIL is also set, the asserted leader email takes
precedence in the audit row's user_email — the role stays the token's.)
| Token state | Request plane (decide) | Platform audit row |
|---|---|---|
| valid minted token | verdict per policy | attributed to the token's user (email + role) on decide and check-output rows |
| absent (left blank) | verdict per policy | attributed to the org's service identity (<org>@axonflow.local) |
| expired / revoked / malformed | the platform 401s with verdict: deny; the proxy surfaces a structured JSON-RPC -32003 deny (policy service rejected the request (check proxy credentials/config)) and the backend is never called. This is never fail-open — a rejected token is a governance verdict, not an outage, so AXONFLOW_FAIL_MODE=open does not forward it. | blocked with security_event: user_token_rejected |
So: leave it blank for service-identity attribution, set it to a minted token
for per-user attribution — and when calls suddenly start failing with the
-32003 policy service rejected the request (check proxy credentials/config)
message, an expired or revoked user token is the first thing to check
(rotate it via the same admin API).
PII posture: redact (chat default) vs. block
What the engine does when it finds critical PII (NIK, NPWP, SSN, …) — mask
it, block it, or forward it untouched — is decided by the connected AxonFlow
deployment's PII_ACTION, not by a proxy env var. PII_ACTION is read at
boot and applies on both planes: the request (the /api/v1/decide verdict)
and the response (the check-output redaction the proxy runs on every allowed
backend response).
PII_ACTION | Request plane (decide verdict) | Response plane (check-output) | Net for chat |
|---|---|---|---|
redact (chat default) | allow — the call is forwarded, not denied | critical PII is masked (e.g. NIK → [REDACTED]) and the masked response is forwarded | call proceeds; PII is stripped out of Claude's context |
block | deny (-32001) — backend never called | a critical-PII response is blocked (-32001) — the engine returns 403, the proxy drops it; nothing reaches Claude | every critical-PII match hard-stops |
warn / log | allow | response is forwarded unredacted (detect-don't-modify) | PII reaches Claude — detection signal only |
This change exists because of the response plane. The block a partner saw —
[MCP] Response blocked by Indonesia PII detection — fired on check-output
under PII_ACTION=block: a backend tool returned a NIK, and the engine blocked
the whole response instead of masking it. Flipping the deployment to
PII_ACTION=redact is exactly what turns that response-plane block → mask,
which is the right behaviour for a chat assistant. For self-hosted deployments
set this in the install bundle's .env (PII_ACTION=redact) — see
axonflow-install.
Two separate knobs — don't conflate them:
AXONFLOW_REDACT_RESPONSES(proxy, table above): controls whether the proxy sends each allowed response tocheck-outputat all (alwaysdefault ·on-obligation·off). It does not decide block-vs-mask.PII_ACTION(engine): controls whatcheck-outputthen does with critical PII —block→ response blocked,redact→ response masked,warn/log→ response forwarded unredacted.
So AXONFLOW_REDACT_RESPONSES=always only guarantees the response is checked;
whether a NIK in it is masked or the response is blocked is the engine's
PII_ACTION. On the request plane the proxy forwards the original arguments to
the backend unchanged (it does not mask outbound arguments) — under redact
the request is simply allowed through.
Known limitation / roadmap:
PII_ACTIONis deployment-global — there is no per-tenant or per-team override today, so a team that needsredactwhile another needsblockcurrently requires separate deployments. Per-tenant policy posture is tracked on the Decision Mode policy-hierarchy roadmap (axonflow-enterprise #2426, WS5).
Backend map
Each backend is fronted over stdio (the proxy launches it: command +
args + env) or http (url). With more than one backend, tool names are
namespaced <id>__<tool> to avoid collisions; with a single backend, names pass
through unchanged. See config.example.json.
Aggregation vs. per-server
The proxy ships as an aggregator (one extension fronts N backends — the
recommended mode). The aggregation contract (initialize → tools/list →
tools/call routing across backends) is validated end-to-end in
runtime-e2e/. If you prefer one proxy per backend (e.g.
to isolate a high-risk server), run the same binary with a single-backend config
— no code change; tool names are then unprefixed. The choice is config, not a
rebuild.
Scope & boundaries (be honest with your Risk Committee)
- AxonFlow on a Team plan governs the MCP/tool surface — which is where the data-exfiltration and tool-action risk lives.
- Plain chat content (no MCP) is not interceptable without Anthropic's Enterprise Compliance API. This proxy does not claim otherwise.
- Response redaction is performed by AxonFlow's authoritative engine
(
POST /api/v1/mcp/check-output), not a local regex — the proxy submits each backend response and forwards the engine's redacted text. Coverage therefore tracks the platform's detectors (NIK + SSN + email + phone …) and improves with the platform, with no proxy change. Because it is a network call, the response plane is fail-closed: if the engine is unreachable or errors, the response is not forwarded (a network hiccup must never leak un-redacted PII into the context). The platform (PDP) at the gate remains the authoritative detector; block-at-the-gate (or a backend that never emits the data) is the fix for data the engine can't see (e.g. PII split across separate JSON array elements, or base64/hex-encoded), not a last-line proxy filter. - HTTP backends speak MCP Streamable HTTP: the proxy accepts both
application/jsonandtext/event-streamresponses (sending anAcceptheader that covers both, so spec-compliant servers don't reject with 406). - If a backend MCP server is restarted mid-session (redeploy, crash), the
proxy transparently re-establishes the dropped stdio session — re-spawning and
re-handshaking it on the next call — so a backend redeploy no longer forces a
Claude Desktop restart. A genuinely-down backend is retried with bounded
exponential backoff and surfaces a clean, retryable error meanwhile. Reconnect
runs only after the policy verdict, so it never forwards an ungoverned call.
Caveat (at-least-once): if a
tools/callexecuted on the backend but the process died before its response returned, the transparent retry re-runs it — a tool with side effects (writes/mutations) can therefore execute twice. The reconnect is intended for the read/lookup tools this proxy fronts today; an at-most-once gate for write tools is tracked in #17. (Backends down at startup are skipped cleanly and never exposed.)
Build & test
go test ./cmd/... -cover # unit tests
./build.sh # multi-arch .mcpb (darwin universal, linux amd64/arm64, win amd64)
cd runtime-e2e && docker compose up -d && ./run.sh # live end-to-end
Repository layout
cmd/axonflow-mcp-proxy/ the proxy (stdio MCP server + aggregation + decide enforcement + redaction + audit)
manifest.json .mcpb Desktop Extension manifest
build.sh multi-arch build + .mcpb packaging
config.example.json backend-map example
runtime-e2e/ live end-to-end test (proxy + stub backend + real AxonFlow agent)
License
MIT — see LICENSE.