DEV Community

Sahajmeet Kaur
Sahajmeet Kaur

Posted on

What It Took to Actually Govern Claude Code Across Our Engineering Team

TL;DR

  • Claude Code's attack surface is bigger than most teams realize - two CVEs in early 2026 showed that cloning a repo is enough to get your API keys stolen or run arbitrary code on a developer's machine
  • The four gaps we found: unmanaged API keys, no centralized traffic visibility, no filesystem controls, and MCP servers running completely ungoverned
  • Fixing all four required more than just patching - it needed a different mental model for how a terminal-based AI tool should be treated

A few months ago our security team flagged something in an audit: we had 60+ engineers using Claude Code, and our "governance" for it was essentially nothing. API keys were in .bash_profile files. There was no way to see what models people were hitting, what it was costing, or who had access to what. When someone left the company, we had no clean way to revoke their Claude Code access without hunting down which machine they'd set their key on.

We'd done all the right things for Claude.ai — SSO, domain capture, admin console, the works. But Claude Code is a different beast. It's not a web app. It runs in a terminal with the developer's full filesystem permissions, and it authenticates with an API key, not a browser session. None of our web-layer controls touched it.

The audit was uncomfortable. Then the CVEs made it urgent.


The wake-up call: two CVEs that changed how we thought about Claude Code

In early 2026, Check Point Research published findings on two vulnerabilities in Claude Code — CVE-2025-59536 and CVE-2026-21852 — that made me realize we'd been thinking about this wrong.

CVE-2025-59536 (CVSS 8.7): A malicious .claude/settings.json in a repository could execute arbitrary shell commands before Claude Code even showed a trust dialog. In earlier versions, hooks defined in that file ran at startup — before the user was asked to confirm anything. Cloning an attacker's repo and running claude in it was enough to get RCE on a developer's machine.

CVE-2026-21852 (CVSS 5.3): This one hit differently. Claude Code uses an environment variable called ANTHROPIC_BASE_URL to decide where to send API requests. A malicious repo could override that via its settings file, redirecting all traffic — including the authentication header carrying the developer's API key — to an attacker-controlled server. The attacker proxies requests to the real Anthropic API so nothing looks broken. The developer notices nothing. The attacker has your key.

Both are patched now (CVE-2025-59536 in v1.0.111, CVE-2026-21852 in v2.0.65). But the thing that stuck with me wasn't the specific vulnerabilities — it was the underlying assumption they exposed. We'd all been treating .claude/settings.json as passive config. It's not. In an agentic tool that can run shell commands and call external APIs, repo-level config is part of the execution layer. Same threat model as a malicious package.json postinstall script. We just weren't thinking about it that way yet.

After the CVE disclosure, my team did a sweep of our repos and found three that had .claude/settings.json files with non-standard ANTHROPIC_BASE_URL overrides. None of them were malicious — developers had put them there for legitimate local testing. But they also would have redirected traffic for anyone else who cloned those repos. We removed them and added a CI check. Then we started working on the actual governance problem.


Gap 1: API keys were completely unmanaged

This was the most embarrassing one to admit. Every developer using Claude Code had either:

a) Their own personal Anthropic key (which meant their personal billing, no audit trail, and no way to revoke on offboarding)
b) A shared team key that lived in a shared .env somewhere (which is worse)

The fix seems obvious in retrospect — issue keys through the Anthropic Admin Console with explicit expiry, store them in AWS Secrets Manager or HashiCorp Vault, and never let them touch .bash_profile or shell history. Rotate quarterly, revoke immediately on offboarding.

But the deeper fix was routing Claude Code through a gateway so the Anthropic key never lived on developer machines at all. With AI Gateway, developers authenticate to the gateway with a scoped virtual key. The underlying Anthropic credential stays in the gateway's secrets manager. If a developer's machine is compromised, the attacker gets a gateway key that we can revoke from a dashboard — not a raw Anthropic API key with workspace-level access.

That distinction matters more than it sounds. A stolen Anthropic key can access all workspace files, modify shared data, and run up API costs before you notice. A stolen gateway key gets you a revocable token with model-level and budget-level restrictions baked in.


Gap 2: No visibility into what was actually happening

Before we set up the gateway, our "observability" for Claude Code was checking the Anthropic billing dashboard once a month and wincing.

We had no idea:

  • Which models developers were hitting
  • How much each team was spending vs others
  • Whether anyone was sending production data through Claude Code
  • What happened when a model was unavailable (usually: the developer just didn't know and filed a vague bug report)

Setting ANTHROPIC_BASE_URL to point at a gateway is the single highest-leverage change you can make to a Claude Code deployment. One line of config gives you a centralized enforcement point for everything — not just observability, but model allowlisting, per-developer rate limits, fallback routing, and budget caps.

export ANTHROPIC_BASE_URL=https://<your-gateway-url>/api/inference/
Enter fullscreen mode Exit fullscreen mode

After we did this, we could see request-level traces with developer attribution, per-model token spend broken down by team, and cost anomalies surfaced automatically. We found one engineer running a batch job through Claude Code that was generating about 3x average daily spend in an afternoon. Not malicious — they just didn't know. We set a per-developer daily limit and the problem went away without any policy conversations.

One thing we learned the hard way: if you're using Claude Admin Console's server-managed settings to control Claude Code, those settings are bypassed when ANTHROPIC_BASE_URL is set. So if you route through a gateway, you need MDM (Jamf on macOS, Puppet/Ansible on Linux) to push the ANTHROPIC_BASE_URL setting into system-level managed config files that developers can't override:

  • macOS: /Library/Application Support/ClaudeCode/managed-settings.json
  • Linux: /etc/claude-code/managed-settings.json

This is also the direct mitigation for CVE-2026-21852 — if ANTHROPIC_BASE_URL is set at the OS level by MDM and locked, a malicious repo's .claude/settings.json can't override it.


Gap 3: The local machine was completely open

Even with the gateway in place, the gateway only governs network-level traffic. Claude Code running on a developer's machine can still read .env files, .ssh keys, ~/.aws/credentials, and anything else the local user has access to — and that content can end up in a prompt before it ever hits the network.

We spent an afternoon putting together a baseline managed-settings.json. Here's the version we landed on:

{
  "permissions": {
    "disableBypassPermissionsMode": "disable",
    "deny": [
      "Bash(curl:*)",
      "Bash(wget:*)",
      "Read(**/.env)",
      "Read(**/.env.*)",
      "Read(**/secrets/**)",
      "Read(**/.ssh/**)",
      "Read(**/credentials/**)"
    ],
    "ask": [
      "Bash(git push:*)",
      "Write(**)"
    ]
  },
  "allowManagedPermissionRulesOnly": true,
  "allowManagedHooksOnly": true,
  "transcriptRetentionDays": 14,
  "sandbox": {
    "enabled": true
  }
}
Enter fullscreen mode Exit fullscreen mode

A few settings here are doing the most work:

allowManagedPermissionRulesOnly: true — this is the CVE-2025-59536 mitigation. It means project-level .claude/settings.json files cannot add new permissions, only the system-level managed config applies. A malicious repo can't expand what Claude Code is allowed to do on that machine.

allowManagedHooksOnly: true — blocks hook injection. Hooks can run arbitrary code between sessions; this prevents a cloned repo from registering new hooks.

disableBypassPermissionsMode: "disable" — prevents --dangerously-skip-permissions from being used in scripts or CI. We found two CI workflows that had been using this flag. Both got refactored.

deny list — blocking reads on .env, .ssh, and credentials directories. We debated this — some developers complained it broke legitimate workflows. We made exceptions on a case-by-case basis via an explicit allow rather than leaving the door open by default.

Sandboxing adds OS-level isolation on top. On macOS it uses Seatbelt, on Linux bubblewrap. It enforces filesystem and network boundaries at a layer below Claude Code's own permission system.


Gap 4: MCP was running completely ungoverned

This was the gap that took us longest to appreciate, because MCP looks like a developer experience feature until you realize what it actually is: direct programmatic access from Claude Code to internal systems.

Our developers had connected Claude Code to GitHub (for code search), Jira (for ticket context), and a couple of internal APIs. All of those connections were configured locally on developer machines, each with their own credentials stored wherever. There was no approval process, no audit trail, and no way to see which tools Claude had been invoking during a session.

The prompt injection risk here is underappreciated. When Claude retrieves content from an external system via an MCP tool — a GitHub issue, a Jira ticket, a web page — that content arrives in Claude's context. If it contains injected instructions, Claude may execute them silently. We had a case where a Jira ticket from an external vendor contained what looked like a formatting instruction that Claude Code interpreted as a command. Nothing bad happened, but it was a near miss that made the problem very concrete.

The fix was centralizing MCP access through a gateway with an allowlist. We deployed TrueFoundry's MCP Gateway as the single endpoint for all MCP server access. In managed-settings.json:

{
  "allowedMcpServers": [
    { "serverUrl": "https://<your-mcp-gateway-url>/*" }
  ],
  "strictKnownMarketplaces": []
}
Enter fullscreen mode Exit fullscreen mode

Setting strictKnownMarketplaces to an empty array blocks marketplace-sourced MCP server installations. Developers can no longer add random MCP servers from the Claude marketplace — any new server has to go through our review process and get registered in the gateway.

What we got from the gateway itself: each developer authenticates once, and the gateway handles downstream auth to GitHub, Jira, and everything else. RBAC controls which teams can access which tools. Every tool invocation generates an audit trace with the developer's identity, the tool name, the request and response, and the latency. We can see exactly what Claude touched during a session, not just which model it called.

The Virtual MCP Servers feature turned out to be genuinely useful for our security team's access: we set up a "security tools" endpoint that exposes only the Sentry and Datadog tools relevant to security workflows, separate from the broader set of tools available to product engineers. Agents only see what they're supposed to see.


What the end state looks like

Six months after the audit, our setup is:

Identity: SSO + domain capture for Claude.ai. All Claude Code keys are gateway virtual keys issued through TrueFoundry, rotated automatically, revoked on offboarding via a single dashboard action.

Routing: ANTHROPIC_BASE_URL pushed via MDM to all developer machines, pointing at TrueFoundry AI Gateway. Per-developer daily token limits. Per-team budget caps. Model allowlist (we've restricted certain high-cost models to specific teams that have a justified need).

Sandboxing: managed-settings.json deployed via MDM with the permission deny-list and sandbox enabled. allowManagedPermissionRulesOnly and allowManagedHooksOnly both true.

MCP: TrueFoundry MCP Gateway as the single MCP endpoint. All downstream servers registered and approved. Tool-level RBAC. Full audit trail exported to Datadog.

Audit logging: Everything flows through the gateway to Datadog via OpenTelemetry. 90-day retention. We get a weekly summary of spend by team, model, and application, and an alert if any developer's usage spikes more than 3x their 7-day average.

Is it perfect? No. BYOD is still a gap — we don't have MDM coverage on contractor machines, which means ANTHROPIC_BASE_URL enforcement is honor-system for that population. If anyone has solved BYOD Claude Code governance cleanly, I'd genuinely like to hear how. Drop it in the comments.

Top comments (0)