Run an Anthropic client inside a VM while keeping its real API key in your
trusted controller. An HTTP egress rule injects the key and
overwrites the top-level model on POST /v1/messages. The rest of the
Messages request and the response keep Anthropic’s format.
This recipe uses direct api.anthropic.com API-key authentication. Bedrock,
Vertex, interactive Claude login, and their credential flows require their
own integrations.
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: an Ubuntu-based snapshot containing your application, Node.js,ca-certificates, and@anthropic-ai/sdk.ANTHROPIC_API_KEY: the provider key to inject.ANTHROPIC_MODEL: the Claude model ID to select.
Install @anthropic-ai/sdk in your application’s directory with
npm install @anthropic-ai/sdk before taking the
snapshot. Install dependencies while the
builder has network access; the application VM below starts with an empty
firewall. Keep provider and Freestyle credentials out of the snapshot.
Choose a model from Anthropic’s model catalog
that your key can access and that supports your tools, input types, and thinking
settings. Use a key scoped to the intended workspace. A multi-workspace key
also needs a controller-injected anthropic-workspace-id, as described in
Anthropic authentication.
Inject The Key And Select A Model
Save this as anthropic-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"),
firewall: { rules: [] },
});
function anthropicRule(apiKey: string, model: string): CreateTlsRuleOptions {
return {
action: "allow",
domain: "api.anthropic.com",
source: { vmId },
destination: { public: true },
match: { method: ["POST"], path: { exact: "/v1/messages" } },
transform: [
{ headers: { "x-api-key": apiKey } },
{ jsonPatch: [{ op: "add", path: "/model", value: model }] },
],
};
}
const rule = await freestyle.tls.rules.create(
anthropicRule(requiredEnv("ANTHROPIC_API_KEY"), requiredEnv("ANTHROPIC_MODEL")),
);
const ruleId = rule.id;
console.log({ vmId, ruleId }); // Retain these IDs in your controller.
The edge replaces the guest’s x-api-key and sends the request to Anthropic
over a separate HTTPS connection with certificate verification. JSON Patch
add creates /model if missing and overwrites it if present. Header secrets
and patch values are sealed and read back as "***".
Anthropic supports x-api-key and Bearer authentication.
This example pairs an x-api-key transform with an SDK client using that same
header. For a Bearer client, inject an authorization header containing the
real Bearer credential instead, and configure the client to send only a
placeholder Bearer credential. Freestyle overwrites named
headers; it does not remove other authentication headers automatically.
Call Messages Inside The VM
Place this file in the directory containing the installed provider SDK:
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({
apiKey: "unused",
authToken: null,
baseURL: "https://api.anthropic.com",
});
const message = await anthropic.messages.create({
model: "selected-by-freestyle",
max_tokens: 256,
messages: [{ role: "user", content: "Reply with one sentence about Linux sandboxes." }],
});
console.log(message.model);
for (const block of message.content) {
if (block.type === "text") console.log(block.text);
}
Run inside the VM, after the rule has configured its hostname and trust store:
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt node call-anthropic.mjs
The system bundle includes Freestyle’s installed CA. Node reads
NODE_EXTRA_CA_CERTS
at startup; restart an existing process after trust-store changes. Other
distributions may use a different bundle path. Keep TLS verification enabled.
For Docker, use the container TLS recipe
with api.anthropic.com (or your alias). The VM’s CA, environment variables,
and hostname mappings are not inherited; NODE_EXTRA_CA_CERTS must point to
the mounted bundle inside the container. Python clients have separate
Requests and HTTPX settings.
The official SDK
adds /v1/messages to the base URL and supplies anthropic-version and
content-type: application/json. Do not append /v1 to this SDK’s base URL.
authToken: null prevents a Bearer token from being inherited from the
environment. The placeholder model and API key are replaced at the edge.
For SSE, set stream: true on messages.create and iterate the returned stream
with for await (const event of stream). The request remains JSON and the
response streams through. Beta query parameters do not change the exact-path
match; client-supplied anthropic-beta headers are preserved.
Choose The Scope Of Model Access
The rule transforms only POST /v1/messages. A request such as
POST /v1/messages/count_tokens or GET /v1/models is forwarded with its
original headers and body, so it receives no real key from this rule.
match selects transforms; it does not deny unmatched endpoints.
Keep the VM firewall restricted and review other TLS grants. The grant is available to every process in the VM. It does not identify a particular agent or executable.
For token counting, configure a separate domain alias with an exact
/v1/messages/count_tokens match and point a separate SDK client at it.
See alias routing.
Two rules for the same domain/source with different match values do not
dispatch by path: TLS rule selection happens first.
To let the guest select its own model, remove only the jsonPatch entry from
anthropicRule and pass a real model ID to the guest SDK. Keep the method/path
match to retain the Messages-only credential scope. Dropping match too
injects the credential into every HTTP request selected by that TLS rule.
The Messages API schema
still applies. Changing /model does not translate thinking parameters or
tools, constrain optional fallback models, or rewrite the models in nested
batch entries. A Messages-only rule also does not cover multipart files or
all requests made by Claude Code. Inventory those requests before adapting the
Claude Code sandbox guide.
Rotate The Key Or Change The Model
After changing the controller’s secret or selected model, replace the full rule
using the same function and saved ruleId:
await freestyle.tls.rules.update(
ruleId,
anthropicRule(requiredEnv("ANTHROPIC_API_KEY"), requiredEnv("ANTHROPIC_MODEL")),
);
Include the real secret, model, and match conditions on every update. Redacted API reads cannot reconstruct them. Cached decisions can take about five seconds to refresh; requests already in flight continue with the previous values. Verify a fresh request before revoking an old credential. Freestyle does not renew expiring provider credentials automatically.
When the VM no longer needs this grant:
await freestyle.tls.rules.delete(ruleId);
This removes the grant after cache refresh. Revoke the key at Anthropic if the credential itself should stop working, and delete the VM separately when done. Other TLS and firewall grants continue to apply.
Verify A Request
Make a small real request, inspect message.model, and check provider usage.
A model alias can resolve to a versioned ID. Change the selected model in the
controller and confirm a subsequent request after refresh. Requests are billed
normally by your provider.
For 401, check that the request uses the matched method/path and that the
guest and transform use the same authentication header. Check workspace scope
and model access at Anthropic. For 400, check max_tokens, tools, thinking
settings, and required headers against the selected model. For certificate
failures, check the guest CA bundle.
The edge accepts bounded, uncompressed JSON for patched requests; malformed JSON and failed patches are rejected before forwarding. See JSON transform limits for status codes and body limits. Transforms do not provide provider token refresh, spending limits, or a complete model gateway policy.