Skip to main content

Claude Desktop + AxonFlow Integration

AxonFlow adds governance to Claude Desktop through a local MCP governance proxy — a one-click Desktop Extension (.mcpb) that fronts your internal MCP servers and checks every tool call against AxonFlow policy before it runs.

Where the Claude Code integration governs through a hook plugin, Claude Desktop governs through an MCP proxy. That difference is not a style choice — it is the load-bearing fact below.


Why a proxy, not a hook

PreToolUse / PostToolUse hooks are a Claude Code feature only. Claude Desktop cannot block a tool call with a hook. On Desktop the only pre-execution interception point is the MCP layer. So AxonFlow governs Desktop as an MCP proxy that Claude Desktop points at, which fronts your backend MCP servers:

Claude Desktop ──stdio MCP──▶ AxonFlow proxy ──POST /api/v1/decide──▶ AxonFlow (PDP)
(Chat / Cowork / Claude Code modes) │ allow → forward to backend (+ redact response)
│ deny → block (JSON-RPC -32001)

Your MCP servers (CRM / database / back-office)

The proxy is a stdio MCP server that Claude Desktop launches and owns — not an HTTP service you curl. It speaks MCP to Claude Desktop on one side and to your backend MCP servers on the other, and on every tools/call it asks AxonFlow's Decision Mode endpoint (POST /api/v1/decide) whether to allow, deny, or hold the call.

Because it sits at the MCP boundary, the proxy governs across all three Desktop modes — Chat, Cowork, and Claude Code. "Cowork" is not a separate plugin target; it is one of Desktop's modes, and any tool call from any mode passes through the same governed boundary.


What this integration enables

Claude Desktop is excellent for individual productivity, but on its own it provides no policy enforcement before a tool runs, no PII filtering on tool responses, and no compliance-grade audit trail. Those are governance concerns, and they become blockers the moment Desktop is connected to internal systems through MCP.

The AxonFlow proxy closes that gap without changing how people use Desktop:

  • Policy enforcement before execution — every tools/call is evaluated against AxonFlow policy and blocked if it violates one (SQL injection, dangerous operations, blocked-PII patterns), before the backend is ever called.
  • PII redaction on responses — the proxy scans every tool response and masks PII before it reaches the conversation context, by default (it does not wait for a redact_pii obligation — the model never sees the response, so the proxy can't gate the scan on the request). Scope stated honestly below.
  • Fail-closed by default — if the policy service is unreachable, tool calls are blocked, not silently forwarded.
  • Compliance-grade audit — every call writes a Layer-1 audit record carrying a decision_id / trace_id and a gateway_id of claude_desktop.<host>, so a reviewer can trace any Desktop tool call to its policy verdict and into a SIEM.

What gets governed

On every tools/callBehaviour
Benign callForwarded to the backend MCP server.
Policy-violating call (SQLi, dangerous op, blocked PII)Blocked — JSON-RPC error -32001; the deny reason plus decision_id / trace_id surface to the user; the backend is never called.
needs_approval verdictHeld — JSON-RPC error -32002, pending a human approval (HITL).
Any PII-bearing responsePII is masked ([REDACTED:<type>]) in the response before it reaches Claude's context. Every response is scanned by default — a redact_pii obligation is not required (and a clean request whose response happens to carry PII is still redacted).
Policy service unreachableFail-closed by default — JSON-RPC error -32003, the call is blocked. (Opt-in fail-open forwards the call but still applies response redaction as a safety net, unless AXONFLOW_REDACT_RESPONSES=off.)

Every call writes a Layer-1 audit recordsession_id, leader_email, tool_name, a parameter hash, response record count, duration — plus decision_id / trace_id / gateway_id=claude_desktop.<host> for SIEM correlation.


Quick start

The proxy ships as a one-click Desktop Extension. The whole install is five steps:

1. Stand up (or point at) an AxonFlow agent — it serves POST /api/v1/decide.
2. Build the extension: ./build.sh → build/axonflow-governance-<v>.mcpb
3. Claude Desktop → Settings → Extensions → Install from file → pick the .mcpb
4. Fill the config fields (endpoint, client id/secret, tenant, fail-mode=closed,
and a backend-servers file — see config.example.json).
5. Restart Claude Desktop. Backend tools appear, now governed.

No LLM provider keys are needed — Claude Desktop handles all LLM calls; AxonFlow only enforces policy, redacts responses, and records the audit trail.

Extension repo: getaxonflow/axonflow-claude-desktop-plugin (MIT). Multi-arch .mcpb (macOS universal, Linux amd64/arm64, Windows amd64).


Configuration reference

When you install the .mcpb, Claude Desktop's extension UI collects these fields and injects them into the proxy process as AXONFLOW_* environment variables (the contract is declared in the extension's manifest.json):

Field (extension UI)Env varNotes
AxonFlow endpointAXONFLOW_ENDPOINTBase URL of the AxonFlow Decision API, e.g. https://app.getaxonflow.com:8090. Required. Default http://localhost:8080.
Client IDAXONFLOW_CLIENT_IDHTTP Basic-auth username on the /api/v1/decide call. Against an Enterprise PDP this must be the license's org id. Default claude-desktop-proxy.
Client secretAXONFLOW_CLIENT_SECRETHTTP Basic-auth password on the /api/v1/decide call — the Enterprise license key. Sensitive. Leave blank in community mode (no auth header is sent).
User token (enterprise JWT)AXONFLOW_USER_TOKENOptional enterprise JWT so the audit row carries the validated user. Sensitive.
Tenant IDAXONFLOW_TENANT_IDOptional. The AxonFlow tenant this Desktop install belongs to.
Org IDAXONFLOW_ORG_IDOptional; the authenticated identity is authoritative.
Fail modeAXONFLOW_FAIL_MODEclosed (default, recommended) blocks on policy-service outage; open forwards but still redacts the response (unless AXONFLOW_REDACT_RESPONSES=off). Keep closed for regulated use.
Leader emailAXONFLOW_LEADER_EMAILThe Desktop user's email, stamped on every audit row.
Backend MCP servers (file)AXONFLOW_BACKENDS_FILEPath to a JSON file describing the backend MCP servers to front. Required.
Audit log pathAXONFLOW_AUDIT_LOGOptional path to an append-only JSONL Layer-1 audit sink. Blank → stderr only.
Enterprise auth

The proxy authenticates to the Decision API with HTTP Basic authAuthorization: Basic base64(client_id:client_secret) — exactly as the AxonFlow SDK does. An Enterprise PDP reads credentials only from that header, so set AXONFLOW_CLIENT_ID to the license's org id and AXONFLOW_CLIENT_SECRET to the license key; a mismatched or missing pair returns 401. A community-mode PDP needs no credentials — leave the secret blank and no header is sent.

A few values are read straight from the environment rather than surfaced as extension fields: AXONFLOW_GATEWAY_ID (default claude_desktop.<hostname>), AXONFLOW_BACKENDS (an inline-JSON alternative to AXONFLOW_BACKENDS_FILE), AXONFLOW_DECIDE_TIMEOUT (default 10s), AXONFLOW_BACKEND_TIMEOUT (default 30s), and AXONFLOW_AI_AGENT (default claude-desktop).

Backend map

The backend file lists the MCP servers the proxy fronts. Each entry has an id plus exactly one transport — command (+ optional args / env) for a stdio backend the proxy launches, or url for an http backend the proxy POSTs JSON-RPC to:

{
"backends": [
{ "id": "crm", "command": "node", "args": ["/opt/acme/crm-mcp/server.js"] },
{ "id": "ledger", "command": "python3", "args": ["-m", "ledger_mcp"] },
{ "id": "warehouse", "url": "http://127.0.0.1:9100/mcp" }
]
}

With more than one backend, re-exposed tool names are namespaced <id>__<tool> (e.g. crm__lookup, warehouse__query) to avoid collisions. With a single backend, tool names pass through unchanged.

Aggregation vs. one proxy per server

The proxy ships as an aggregator: one extension fronts all backends, which is the least-friction option. To isolate a high-risk backend, run the same binary with a single-backend config — that is configuration, not a rebuild.


Honest redaction scope

State this precisely — do not overclaim. The proxy's response redactor is a regex filter, not a complete DLP engine:

  • When it runs: on every forwarded tool response, by default (AXONFLOW_REDACT_RESPONSES=always). It does not wait for the PDP to flag the request — the model never sees the response, so a clean request whose response carries PII is still redacted. (on-obligation restores the legacy obligation-gated behaviour; off disables it — an explicit opt-out.)
  • It covers: PII in string and numeric leaf values, and in object keys, at any nesting depth (the redactor walks keys and values recursively). Masked spans are replaced with a stable [REDACTED:<type>] token. Built-in patterns target Indonesian and generic identifiers (NIK, NPWP, Indonesian phone, bank accounts, email, payment-card PAN).
  • It does not catch: PII split across separate JSON array elements (e.g. ["3174","0125","0990","0001"] reassembling to a NIK — each element is scanned independently and none matches alone), nor base64- / hex- / URL-encoded values, nor PII types without a built-in pattern.
  • It may over-mask: because every response is now scanned, the deliberately-loose payment-card rule masks any 13–19 digit run as [REDACTED:card] — so a long benign numeric (a 13-digit epoch-millisecond timestamp, a 16-digit order id) can be masked too. Over-masking is the safe failure direction; if a benign long-digit field matters to a workflow, keep it under 13 digits or scope it out.

The robust control for those gaps is block-at-the-gate — a deny policy so the call never runs — or a backend designed not to emit the raw field, not the last-line response filter. The accurate framing is "PII in tool responses is materially reduced, and high-risk fields are blocked at the gate," never "no PII can reach the context."

The redactor is a defense-in-depth mirror of AxonFlow's platform PII detectors; the platform PDP remains the authoritative gate.


Team vs. Enterprise — the monitoring boundary

If you are governing a Claude Team deployment, be precise about what this does and does not cover:

  • AxonFlow on Team governs the MCP / tool surface — which is where data-exfiltration and unauthorized-tool-action risk actually lives. Every tool call from Chat, Cowork, or Claude Code mode passes through the governed boundary.
  • It does not intercept plain chat content that involves no MCP tool call. Org-wide chat / file monitoring requires Anthropic's Enterprise Compliance API.
  • Fleet-wide server-managed settings pushed from the Claude.ai admin console are also Enterprise-only. On Team, fleet configuration is delivered via MDM (the com.anthropic.claudefordesktop preference domain) plus the in-app extension allowlist.

For the full fleet-rollout procedure (MDM recipes, lockdown keys, credential distribution, and a per-machine governance probe), design partners can reach the gated Fleet Deployment Runbook in their starter pack.


What gets logged

Every governed Desktop tool call is inspectable through AxonFlow's audit trail. Each Layer-1 record carries:

  • tool name and the backend it targeted
  • whether the call was allowed, blocked, or held for approval
  • a parameter hash and the response record count (not raw arguments)
  • the matched policy and block reason when present
  • decision_id, trace_id, and gateway_id=claude_desktop.<host> for correlation

The evidence chain for a reviewer: the decision_id on a record resolves via GET /api/v1/decisions/{id}/explain to the full policy decision, and the trace_id joins the OpenTelemetry decision span (decision.gateway_id = claude_desktop.<host>) — so any Desktop tool call traces from its audit row to the policy verdict to the SIEM.


Verifying it works

The extension repo ships a live end-to-end test (runtime-e2e/) that drives a real proxy over stdio in front of a real backend against a from-source AxonFlow agent:

✅ allow:       benign tools/call forwarded to the backend
✅ deny: SQLi tools/call blocked with -32001 (policy sys_sqli_stacked_drop)
✅ redact: PII response stripped — [REDACTED:nik] [REDACTED:email] [REDACTED:phone_id]
✅ fail-closed: PDP unreachable → call blocked with -32003
✅ audit: one Layer-1 record per call (decision_id + trace_id + gateway_id=claude_desktop.*)
note

POST /api/v1/decide returns a deny for block-action policies (SQL injection, dangerous operations). A request that carries a redact-action PII pattern returns allow plus a redact_pii obligation. The proxy's response redaction does not depend on that obligation, though — it scans every response by default, so a clean request whose response carries PII (e.g. a plain customer_id lookup that returns a NIK) is still redacted. Use a block-action pattern when you want a hard-deny demo.


Next steps

Need help?