Freestyle Docs

Freestyle / Guides

Inject a Claude Code OAuth Token at the Edge

Run Claude Code in a sandbox on a Claude subscription token held at the Freestyle edge, so the VM and its snapshot never store the credential, and rotate it without a rebuild.

Run Claude Code inside a VM on a Claude subscription, while the long-lived token from claude setup-token stays in your trusted controller. An HTTP egress rule injects it as an Authorization: Bearer header on the VM’s requests to api.anthropic.com. The VM carries only a placeholder, so the credential never lands on disk or in a snapshot, and you rotate it by replacing the rule instead of rebuilding.

This is the third way to authenticate the CLI. The API-key and interactive-login options put a live credential in the invoking process or bake it into the snapshot; this one keeps the subscription token at the edge. For direct Messages API calls with a Console key — a different credential and the x-api-key header — see Select Anthropic Models and Inject Authentication instead.

Mint A Long-Lived Token

On your own machine, with Claude Code installed and signed in to the Claude account whose subscription should pay for the runs:

claude setup-token

This prints a long-lived OAuth token (the value behind CLAUDE_CODE_OAUTH_TOKEN) tied to your Claude plan — distinct from a Console API key. Treat it as a secret: it authenticates as you and draws on your plan’s usage limits. Hold it in the controller, not the VM.

Prepare The Controller And Snapshot

Run the controller outside the sandbox. Install the SDK with npm install freestyle@latest and supply these environment variables there:

  • FREESTYLE_API_KEY: your Freestyle API key.
  • FREESTYLE_SNAPSHOT_ID: a snapshot with Claude Code installed. Build one with the Claude Code sandbox guide — install the CLI, but do not sign in with /login, so the snapshot stays free of any credential.
  • CLAUDE_CODE_OAUTH_TOKEN: the token from the previous step, to inject.

Keep the credential out of the snapshot and the VM environment. Do not bake a /login session, and leave ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, and any apiKeyHelper unset in the VM — each outranks CLAUDE_CODE_OAUTH_TOKEN, and a set ANTHROPIC_API_KEY makes the CLI send x-api-key instead of the Bearer header this rule injects.

Inject The Token

Save this as claude-code-controller.ts outside the VM:

import { Freestyle, type CreateTlsRuleOptions } from "freestyle";

function requiredEnv(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const freestyle = new Freestyle();
const { vmId } = await freestyle.vms.create({
  snapshotId: requiredEnv("FREESTYLE_SNAPSHOT_ID"),
  // Claude Code reaches several Anthropic hosts; allow egress, or narrow it to
  // the hosts the CLI uses. The rule below is what injects the credential.
  firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
});

function claudeCodeRule(token: string): CreateTlsRuleOptions {
  return {
    action: "allow",
    domain: "api.anthropic.com",
    source: { vmId },
    destination: { public: true },
    // No `match`: Claude Code calls more than POST /v1/messages on this host
    // (model listing, feature flags, telemetry). A path match would leave the
    // other requests without the injected credential.
    transform: [{ headers: { authorization: `Bearer ${token}` } }],
  };
}

const rule = await freestyle.tls.rules.create(
  claudeCodeRule(requiredEnv("CLAUDE_CODE_OAUTH_TOKEN")),
);
console.log({ vmId, ruleId: rule.id }); // Retain these IDs in your controller.

The edge overwrites the guest’s Authorization header and sends the request to Anthropic over a separate, certificate-verified HTTPS connection. Header secrets are sealed and read back as "***". Freestyle overwrites named headers; it does not strip other authentication headers, so keep the VM from sending its own.

The same rule from the Freestyle CLI:

freestyle tls create \
  --domain api.anthropic.com \
  --from vm="$VM_ID" \
  --to public \
  --header "authorization=Bearer $CLAUDE_CODE_OAUTH_TOKEN"

Run Claude Code In The VM

Give the VM a placeholder CLAUDE_CODE_OAUTH_TOKEN. It must be non-empty — the CLI sends it as the Bearer credential, and the edge replaces it with the real one — but it is never the live secret.

const { vm } = await freestyle.vms.create({
  firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
  slug: "claude-code-sandbox",
  snapshotId: requiredEnv("FREESTYLE_SNAPSHOT_ID"),
  idleTimeoutSeconds: null,
});
// create the rule for this vmId as above, then:

const run = await vm.exec({
  command:
    `CLAUDE_CODE_OAUTH_TOKEN="placeholder-replaced-at-the-edge" ` +
    `~/.local/bin/claude -p "Write a haiku about sandboxes"`,
  timeoutMs: 120_000,
});
console.log(run.stdout);

Claude Code’s standalone binary verifies TLS against the system trust store, which already includes Freestyle’s CA once the rule exists, so you do not need NODE_EXTRA_CA_CERTS for the CLI. The whole run, via the Freestyle CLI:

freestyle vm exec "$VM_ID" -- \
  env CLAUDE_CODE_OAUTH_TOKEN="placeholder-replaced-at-the-edge" \
  claude -p "Write a haiku about sandboxes"

Verify A Request

A valid token authenticates and the prompt returns. A wrong value comes back straight from Anthropic, which proves the edge delivered the header and only the value was off:

# Inside the VM — the edge injects the Authorization header.
curl -sS -o /dev/null -w "%{http_code}\n" https://api.anthropic.com/v1/models \
  -H "anthropic-version: 2023-06-01"
# 200 with a live token; 401 {"type":"authentication_error",
# "message":"OAuth access token is invalid."} with a bad one.

To confirm the edge is terminating TLS, check the certificate the VM sees — its issuer is Freestyle TLS Egress CA and its subject is api.anthropic.com:

echo | openssl s_client -connect api.anthropic.com:443 \
  -servername api.anthropic.com 2>/dev/null | openssl x509 -noout -issuer -subject

Rotate Or Revoke

Mint a fresh token with claude setup-token, then replace the rule in place with the saved ruleId. update restates the whole rule:

await freestyle.tls.rules.update(ruleId, claudeCodeRule(requiredEnv("CLAUDE_CODE_OAUTH_TOKEN")));

Or from the CLI, restating domain, source, and destination:

freestyle tls update "$RULE_ID" \
  --domain api.anthropic.com --from vm="$VM_ID" --to public \
  --header "authorization=Bearer $CLAUDE_CODE_OAUTH_TOKEN"

Cached decisions refresh in about five seconds; requests already in flight keep the old value. Verify a fresh request before retiring the previous token. Delete the rule when the VM no longer needs the grant — the VM is then left with only its placeholder:

await freestyle.tls.rules.delete(ruleId);

To invalidate the credential itself, revoke it from your Claude account’s device and session settings; logging out of all devices invalidates long-lived tokens. Freestyle does not renew an expiring token for you.

Scope And Caveats

The grant is available to every process in the VM; it does not identify a particular agent or executable. Keep the VM firewall restricted to the hosts Claude Code needs and review other TLS grants. The token authenticates as your Claude account against your plan’s limits — use your own, and follow Anthropic’s terms for subscription credentials.

The rule injects on all of the VM’s api.anthropic.com traffic. If the same VM also runs a client that should authenticate with a Console x-api-key, that is a separate credential and header; scope it with its own rule per Select Anthropic Models and Inject Authentication.

If an invocation reaches Anthropic through Node rather than the standalone binary — for example the Claude Agent SDK bridge — and raises a certificate error, point NODE_EXTRA_CA_CERTS at /etc/ssl/certs/ca-certificates.crt, which carries Freestyle’s CA. For a 401, confirm no higher-precedence credential is set in the VM, that the rule’s domain and source match the VM, and that the token is still valid.

esc