Skip to main content

Claude Cowork Deployment

Deploy Claude Cowork against a self-hosted /v1/messages gateway. Covers the architecture, three auth tiers (PAT, session, mTLS), signed manifest, audit schema.

TL;DR - Claude Cowork reads five managed-preference keys on launch that let an organisation replace Anthropic's hosted inference, auth, and plugin catalogue with its own. This page is the platform-agnostic reference: architecture, auth tiers, manifest shape, audit schema. For the platform mechanics see macOS and Windows. For the strategic case a CTO/CISO makes for this deployment, see the Cowork on your own infrastructure guide.

Anthropic's three extension points

Anthropic documents Cowork running on customer infrastructure in three support articles:

Three extension points come out of those articles:

  • /v1/messages gateway. When inferenceProvider = "gateway" is set in managed preferences, Cowork routes every chat turn, tool call, and extended-thinking block through inferenceGatewayBaseUrl. The URL must speak the Anthropic Messages wire format.
  • Managed MCP allowlist. Cowork reads a JSON file under the org-plugins mount to decide which MCP servers a user may invoke. Servers not in the list are hidden from the UI and refused at runtime.
  • org-plugins mount. A directory Cowork scans on launch. Plugins, skills, and agents inside it are visible in the slash menu and the agent runtime. End users have no write access.

The five-move deployment

Five moves take a fleet from zero to a Cowork deployment that runs entirely on infrastructure you operate. The first three run once per organisation; the last two run per device.

  1. Publish the gateway. Stand up the /v1/messages endpoint on a host the fleet can reach. See the Gateway Service reference for endpoint and routing details.
  2. Mint the credential. Pick the auth tier that matches your compliance posture - PAT, session cookie, or mTLS. On each device, systemprompt-bridge doctor reports which tiers are configured and working.
  3. Ship the signed manifest. Publish the plugins, skills, and managed MCP servers the fleet should receive, signed with an Ed25519 private key held by the gateway.
  4. Distribute the MDM profile. Push the five managed-preference keys to every device. Platform specifics: macOS, Windows.
  5. Verify in the audit trail. One SQL query against ai_requests filtered by trace_id, or systemprompt infra logs request list from the CLI. Empty result set means the deployment is not finished.

The five managed-preference keys

Cowork reads the same five keys on every platform. The meaning is identical; the transport differs.

Key Values Purpose
inferenceProvider "gateway" Switches inference from api.anthropic.com to the configured gateway
inferenceGatewayBaseUrl "http://127.0.0.1:<port>" or "https://cowork-gateway.example.com" Where /v1/messages calls go. In the standard deployment this points at the local systemprompt-bridge proxy on loopback, which attaches the identity headers and forwards to the gateway. HTTPS required unless the host is 127.0.0.1
inferenceGatewayApiKey string The bearer credential Cowork presents on outbound calls
inferenceGatewayAuthScheme "bearer" Auth scheme on outbound calls. bearer is the only value today
inferenceManifestPubkey base64 Ed25519 pubkey Pins the manifest signing key so the sync agent can verify without trust-on-first-use

Platform-specific syntax lives in the platform docs. systemprompt-bridge install --print-mdm macos|windows|linux prints the snippet for each target OS, and the helper's own overrides (SP_BRIDGE_CONFIG, SP_BRIDGE_PAT, SP_BRIDGE_GATEWAY_URL) cover Linux developer workstations.

The three auth tiers

systemprompt-bridge exposes three authentication providers. On every call they run in a fixed order - mTLS first, session second, PAT third. The first provider that produces a valid token wins.

+------------------------------------------------+
|  systemprompt-bridge on each inference call    |
+------------------------------------------------+
                     |
         cache hit?  | yes -> return cached line
                     |
                     v  no
+------------------------------------------------+
|  Tier 1: mTLS device certificate               |
|  POST /v1/auth/bridge/mtls                     |
+------------------------------------------------+
                     | no cert / disabled
                     v
+------------------------------------------------+
|  Tier 2: Session exchange (browser OAuth)      |
|  POST /v1/auth/bridge/session                  |
+------------------------------------------------+
                     | no session / 401
                     v
+------------------------------------------------+
|  Tier 3: Personal Access Token                 |
|  POST /v1/auth/bridge/pat                      |
+------------------------------------------------+
                     |
                     v
    Write {token, ttl, headers} JSON to stdout

PAT is the simplest tier. An administrator provisions the token at the gateway, the user runs systemprompt-bridge login once, and the token lives in the OS keystore (macOS Keychain, Windows Credential Manager, Linux Secret Service). Revoke the PAT at the gateway and the next helper call returns 401.

Session is right when you already have a browser-based SSO flow and want Cowork enrolment to ride on it. The helper opens {gateway}/bridge/device-link?redirect=http://127.0.0.1:<ephemeral>, the user authenticates in the browser, the browser returns a one-shot exchange code to the loopback server, and the helper swaps it for a token at /v1/auth/bridge/session. Administrators can also mint an exchange code directly with systemprompt admin bridge issue-code --user-id <id>.

mTLS is the right tier when the device itself is the identity. The helper loads a client certificate from an OS keystore reference, presents its attestation to /v1/auth/bridge/mtls, and receives a JWT bound to the certificate fingerprint enrolled via systemprompt admin bridge enroll-cert. The cert is typically provisioned by MDM and backed by TPM (Windows), Secure Enclave (macOS), or a PKCS#11 token (Linux).

The six canonical headers

Every /v1/messages call that leaves the bridge carries identity headers the gateway validates against the JWT and stamps into ai_requests:

Header Typed ID Meaning
x-user-id UserId The human on the other side of the keyboard
x-session-id SessionId Cowork chat session - stable across tool calls within one conversation
x-trace-id TraceId This one inference call. Every tool call and MCP exec inherits it
x-client-id ClientId Stable identifier for the calling client application
x-policy-version (plain) Version of the policy bundle in force at token mint time
x-call-source SessionSource Module that issued the call

The header constants live in systemprompt_identifiers::headers. The provider never sees them; the gateway strips them at the outbound boundary.

The stdout contract

The helper's credential-helper mode (systemprompt-bridge credential-helper --host claude-desktop) is strict: on every run the binary prints exactly one JSON line to stdout. Anything else - a banner, a log preamble, two lines - breaks the host app's parser and surfaces as "credential helper failed" in the UI. Diagnostics go to stderr.

{
  "token": "eyJhbGciOi...",
  "ttl": 3600,
  "headers": {
    "x-user-id": "u_29f8a3",
    "x-session-id": "s_01hy...",
    "x-trace-id": "t_a8bf...",
    "x-client-id": "sp_cowork",
    "x-policy-version": "2026-04-22",
    "x-call-source": "cowork"
  }
}

Cached at $XDG_CACHE_HOME/systemprompt-bridge/cache.json (mode 0600). Cache invalidation is a matter of deleting the file.

The signed manifest

The manifest is a JSON document served by the gateway at /v1/bridge/manifest and signed with an Ed25519 private key per RFC 8032. Every client verifies the signature against a pubkey pinned at install time - via the inferenceManifestPubkey managed preference, or fetched once from /v1/bridge/pubkey when trust-on-first-use is explicitly allowed.

{
  "manifest_version": 42,
  "issued_at": "2026-04-22T09:30:00Z",
  "not_before": "2026-04-22T09:30:00Z",
  "user_id": "u_29f8a3",
  "tenant_id": "org_acme",
  "user": {
    "id": "u_29f8a3",
    "name": "jane",
    "email": "jane@example.com",
    "roles": ["engineering", "senior"]
  },
  "plugins": [
    {
      "id": "devops-plugin",
      "version": "1.4.2",
      "sha256": "...",
      "files": [
        {"path": "plugin.toml", "sha256": "...", "size": 512},
        {"path": "handlers/deploy.sh", "sha256": "...", "size": 2048}
      ]
    }
  ],
  "skills": [
    {
      "id": "review-terraform-plan",
      "name": "review-terraform-plan",
      "description": "Audit a Terraform plan for destructive changes",
      "file_path": "skills/review-terraform-plan.md",
      "sha256": "...",
      "instructions": "..."
    }
  ],
  "agents": [
    {
      "id": "pr-reviewer",
      "name": "pr-reviewer",
      "display_name": "PR Reviewer",
      "description": "Reviews pull requests",
      "version": "1.0.0",
      "endpoint": "/v1/messages",
      "enabled": true,
      "is_default": false,
      "is_primary": false,
      "model": "claude-sonnet-4-6",
      "skills": ["review-terraform-plan"],
      "mcp_servers": ["gh-readonly"]
    }
  ],
  "hooks": [],
  "managed_mcp_servers": [
    {
      "name": "gh-readonly",
      "url": "https://mcp-internal.example.com/gh-readonly",
      "tool_policy": {"search_code": "allow", "read_file": "allow"}
    }
  ],
  "revocations": ["leaked-api-probe"],
  "signature": "base64(ed25519(canonical_json(manifest_body)))"
}

Five details carry the governance story:

  • Per-user. The user block is resolved server-side from the JWT; two engineers in the same tenant can receive different manifests.
  • managed_mcp_servers is the allowlist. Servers not in the list are refused at runtime.
  • revocations is the kill switch. The binary removes revoked files atomically on the next sync.
  • Skills and plugins are separate. Plugins execute; skills are text context. Agents tie them together.
  • Signature covers canonical JSON. Pubkey is pinned at install time; a mismatched signature aborts the sync before touching the filesystem.

The sync flow

systemprompt-bridge sync (optionally --watch --interval <secs>) runs five steps:

  1. Fetch /v1/bridge/manifest with the cached JWT. A 401/403/404 short-circuits the sync; the existing mount is untouched.
  2. Verify the Ed25519 signature against the pinned pubkey. Verification failure aborts before any filesystem write.
  3. Stage every file referenced in plugins[].files[] under a staging directory, fetched from /v1/bridge/plugins/{plugin_id}/{path}. Each file is SHA-256-hashed and compared to the manifest. Mismatch aborts.
  4. Rename atomically into place (MoveFileEx(MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) on Windows; rename(2) on Unix).
  5. Write the managed MCP allowlist and last-sync metadata into the mount.

Sign the manifest from a build pipeline that retains every version. Rotate the signing key with systemprompt admin bridge rotate-signing-key, and keep the seed in the gateway's secrets store - never on an admin laptop.

The audit schema

Every inference call through the gateway writes one row to ai_requests before the response returns to the caller, so there is no path where a successful call is not audited. The key columns:

CREATE TABLE IF NOT EXISTS ai_requests (
    id TEXT PRIMARY KEY,
    request_id VARCHAR(255) NOT NULL UNIQUE,
    user_id VARCHAR(255) NOT NULL,
    session_id VARCHAR(255),
    trace_id VARCHAR(255),
    mcp_execution_id VARCHAR(255),
    provider TEXT NOT NULL,            -- 'anthropic', 'openai', 'gemini', ...
    model TEXT NOT NULL,
    requested_model TEXT,
    route_match TEXT,
    input_tokens INTEGER,
    output_tokens INTEGER,
    cost_microdollars BIGINT NOT NULL DEFAULT 0,
    latency_ms INTEGER,
    cache_hit BOOLEAN NOT NULL DEFAULT FALSE,
    status VARCHAR(255) NOT NULL DEFAULT 'pending',
    error_message TEXT,
    actor_kind TEXT NOT NULL CHECK (actor_kind IN ('user', 'job', 'mcp')),
    actor_id TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    completed_at TIMESTAMPTZ
);

The table is indexed on trace_id, user_id, session_id, provider, status, and cost_microdollars. Tool calls land in the companion ai_request_tool_calls table, and MCP executions link back through mcp_execution_id - all sharing the same trace_id.

The one-shot lineage query

Every event triggered by one user prompt shares a trace_id. A single filter on trace_id gives the full lineage:

SELECT
  r.created_at, r.status, r.model, r.provider,
  r.input_tokens, r.output_tokens, r.cost_microdollars,
  r.mcp_execution_id
FROM ai_requests r
WHERE r.user_id  = 'u_29f8a3'
  AND r.trace_id = 't_a8bf...'
ORDER BY r.created_at ASC;

The same lineage is available from the CLI: systemprompt infra logs audit <request-id> --full reconstructs identity, policy evaluations, prompt, response, and cost for one request, and systemprompt infra logs trace show <trace-id> walks the tool-call chain.

SIEM export

systemprompt analytics requests list and analytics requests stats export the same rows as CSV over a time range for Splunk / ELK / Datadog / Sumo Logic ingestion.

Cost attribution

cost_microdollars is microdollars. SUM(cost_microdollars) GROUP BY user_id is per-user cost. GROUP BY provider compares spend across upstreams - "what did we spend on Bedrock vs direct Anthropic in April 2026" is a one-line query. systemprompt analytics costs is the pre-built rollup.

Provider routing

The gateway's /v1/messages router forwards to a configured provider entry - anthropic, openai, gemini, moonshot, qwen, minimax, and any custom entry you define in the profile's provider config - each speaking one of the supported wire protocols (anthropic, openai-chat, openai-responses, gemini). Routes match on model pattern, agent, user, or failover order. See the AI Services reference for the full rule schema and the Gateway Service reference for the route config block.

Air-gapped deployment

Nothing in the runtime flow requires outbound traffic to anthropic.com, a telemetry endpoint, or a licence server. The only network calls the binary makes are the ones explicitly pointed at your gateway.

An air-gapped deployment shifts three responsibilities inward:

  • The gateway runs inside the egress boundary.
  • The upstream provider sits on the same side - self-hosted vLLM / Ollama / sglang, a private Bedrock VPC endpoint, or Azure OpenAI over private link.
  • The Ed25519 signing key never leaves the gateway's secrets store.

Signature verification always runs; when no pubkey is pinned via MDM, the sync refuses to proceed unless --allow-tofu explicitly opts back into trust-on-first-use.

Migration from direct API usage

Teams running Cowork against api.anthropic.com with individual OAuth need a soft cutover in four evidence-gated phases:

  1. Shadow mode. The gateway accepts requests and writes to ai_requests; upstream of record stays Anthropic. Advance when shadow audit matches production call shape.
  2. Pilot cohort. MDM profile to a small volunteer group. Advance when the cohort reports no regressions and the status distribution looks right.
  3. Ring rollout. Push department by department. A spike in failed-status rows in any ring blocks the next ring until triaged.
  4. Retire direct access. Revoke individual OAuth tokens at the Anthropic side. Gateway is the only path.

The gate between phases is evidence, not calendar.

CLI

Server-side administration runs through the systemprompt CLI; device-side operations run through the systemprompt-bridge helper binary.

# Server side: enroll a device certificate fingerprint for a user (mTLS tier)
systemprompt admin bridge enroll-cert

# Server side: mint a one-shot session exchange code for the helper
systemprompt admin bridge issue-code --user-id <id>

# Server side: list active bridge sessions (recent heartbeats)
systemprompt admin bridge list

# Server side: rotate the Ed25519 manifest signing key
systemprompt admin bridge rotate-signing-key

# Device side: install the helper and managed preferences
systemprompt-bridge install --gateway https://cowork-gateway.example.com --apply

# Device side: authenticate, sync the manifest, and self-check
systemprompt-bridge login
systemprompt-bridge sync
systemprompt-bridge doctor

Where to go next