claude with Claude Code already installed. Most workflows only need:
gravixlayer runtime create --template claude ...
CLAUDE.md, or MCP tools inside the sandbox.
The open-source agent-claude.Dockerfile documents what the platform claude image is based on (Claude Code install, IS_SANDBOX=1, base toolchain).
Optional: build your own image from the Dockerfile
Only if you need a private/custom variant. From a checkout of gravixlayer-python:POST /v1/agents/template/build
from pathlib import Path
from gravixlayer import GravixLayer, TemplateBuilder
client = GravixLayer() # defaults to cloud="aws", region="us-east-1"
dockerfile = Path(
"examples/templates/dockerfiles/agent-claude.Dockerfile"
).read_text()
builder = (
TemplateBuilder("my-claude", "Custom Claude Code image")
.dockerfile(dockerfile)
.vcpu(2)
.memory(2048)
.disk(8192) # prefer ≥ 8 GB
)
status = client.templates.build_and_wait(builder, timeout_secs=1800)
print(status.template_id, status.status)
# Then: client.runtime.create(template=status.template_id, ...)
# or template="my-claude"
import { readFile } from 'node:fs/promises';
import { GravixLayer, TemplateBuilder } from 'gravixlayer';
const client = new GravixLayer(); // defaults to cloud="aws", region="us-east-1"
const dockerfile = await readFile(
'examples/templates/dockerfiles/agent-claude.Dockerfile',
'utf8',
);
const builder = new TemplateBuilder('my-claude', 'Custom Claude Code image')
.dockerfile(dockerfile)
.vcpu(2)
.memory(2048)
.disk(8192); // prefer ≥ 8 GB
const status = await client.templates.buildAndWait(builder, { timeoutMs: 1_800_000 });
console.log(status.templateId, status.status);
// Then: await client.runtime.create({ template: status.templateId })
// or { template: 'my-claude' }
gravixlayer template build \
--name my-claude \
--description "Custom Claude Code image" \
--dockerfile examples/templates/dockerfiles/agent-claude.Dockerfile \
--vcpu-count 2 \
--memory-mb 2048 \
--disk-mb 8192 \
--wait \
--build-timeout 1800 \
--output json | jq .
API="https://api.gravixlayer.ai/v1"
AUTH=(-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" -H "Content-Type: application/json")
DOCKERFILE=$(jq -Rs . < examples/templates/dockerfiles/agent-claude.Dockerfile)
curl -sS -X POST "$API/agents/template/build" "${AUTH[@]}" \
-d "{
\"name\": \"my-claude\",
\"description\": \"Custom Claude Code image\",
\"dockerfile\": $DOCKERFILE,
\"vcpu_count\": 2,
\"memory_mb\": 2048,
\"disk_mb\": 8192
}" | jq .
from_image vs dockerfile.
Optional: bake packages and a default CLAUDE.md
Advanced: publish a Claude base image to your registry, then layer packages:
from gravixlayer import GravixLayer, TemplateBuilder
client = GravixLayer() # defaults to cloud="aws", region="us-east-1"
builder = (
TemplateBuilder("my-claude", "Claude Code + pytest + project CLAUDE.md")
.from_image("your-registry.example.com/claude:latest")
.vcpu(2)
.memory(2048)
.disk(8192)
.pip_install("pytest", "ruff", "httpx")
.copy_file(
"# Always prefer small PRs and add tests for new endpoints.\n",
"/workspace/CLAUDE.md",
)
)
status = client.templates.build_and_wait(builder, timeout_secs=1800)
print(status.template_id, status.status)
import { GravixLayer, TemplateBuilder } from 'gravixlayer';
const client = new GravixLayer(); // defaults to cloud="aws", region="us-east-1"
const builder = new TemplateBuilder('my-claude', 'Claude Code + pytest + project CLAUDE.md')
.fromImage('your-registry.example.com/claude:latest')
.vcpu(2)
.memory(2048)
.disk(8192)
.pipInstall('pytest', 'ruff', 'httpx')
.addFile(
'/workspace/CLAUDE.md',
'# Always prefer small PRs and add tests for new endpoints.\n',
);
const status = await client.templates.buildAndWait(builder, { timeoutMs: 1_800_000 });
console.log(status.templateId, status.status);
CONTENT_B64=$(printf '%s' '# Always prefer small PRs and add tests for new endpoints.
' | base64)
gravixlayer template build \
--name my-claude \
--description "Claude Code + pytest + project CLAUDE.md" \
--docker-image your-registry.example.com/claude:latest \
--vcpu-count 2 \
--memory-mb 2048 \
--disk-mb 8192 \
--build-step '{"type":"pip_install","args":["pytest","ruff","httpx"]}' \
--build-step "{\"type\":\"copy_file\",\"args\":[\"/workspace/CLAUDE.md\"],\"content\":\"$CONTENT_B64\"}" \
--wait \
--build-timeout 1800 \
--output json | jq .
API="https://api.gravixlayer.ai/v1"
AUTH=(-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" -H "Content-Type: application/json")
CONTENT_B64=$(printf '%s' '# Always prefer small PRs and add tests for new endpoints.
' | base64)
curl -sS -X POST "$API/agents/template/build" "${AUTH[@]}" \
-d "{
\"name\": \"my-claude\",
\"description\": \"Claude Code + pytest + project CLAUDE.md\",
\"docker_image\": \"your-registry.example.com/claude:latest\",
\"vcpu_count\": 2,
\"memory_mb\": 2048,
\"disk_mb\": 8192,
\"build_steps\": [
{\"type\": \"pip_install\", \"args\": [\"pytest\", \"ruff\", \"httpx\"]},
{\"type\": \"copy_file\", \"args\": [\"/workspace/CLAUDE.md\"], \"content\": \"$CONTENT_B64\"}
]
}" | jq .
claude template (no rebuild) — see Anthropic’s skills docs:
import os
from gravixlayer import GravixLayer
client = GravixLayer() # defaults to cloud="aws", region="us-east-1"
sandbox = client.runtime.create(
template="claude",
providers=[os.environ["ANTHROPIC_PROVIDER_ID"]],
network_policy_ids=[os.environ["CLAUDE_NETWORK_POLICY_ID"]],
env_vars={"ANTHROPIC_API_KEY": os.environ["ANTHROPIC_API_KEY"]},
timeout=1800,
)
sandbox.file.create_directory("/workspace/.claude/skills/review")
sandbox.file.write(
"/workspace/.claude/skills/review/SKILL.md",
"""---
name: review
description: Prefer security and test coverage in PR reviews
---
When reviewing, check authz, input validation, and missing tests.
""",
)
result = sandbox.run_cmd(
command="claude",
args=[
"--dangerously-skip-permissions",
"-p",
"List skills available under .claude/skills and summarize the review skill.",
"--output-format",
"text",
],
working_dir="/workspace",
timeout=600,
)
print(result.stdout)
sandbox.kill()
import { GravixLayer } from 'gravixlayer';
const client = new GravixLayer(); // defaults to cloud="aws", region="us-east-1"
const sandbox = await client.runtime.create({
template: 'claude',
providers: [process.env['ANTHROPIC_PROVIDER_ID']!],
networkPolicyIds: [process.env['CLAUDE_NETWORK_POLICY_ID']!],
envVars: { ANTHROPIC_API_KEY: process.env['ANTHROPIC_API_KEY']! },
timeout: 1800,
});
await sandbox.file.createDirectory('/workspace/.claude/skills/review');
await sandbox.file.write(
'/workspace/.claude/skills/review/SKILL.md',
`---
name: review
description: Prefer security and test coverage in PR reviews
---
When reviewing, check authz, input validation, and missing tests.
`,
);
const result = await sandbox.runCmd('claude', {
args: [
'--dangerously-skip-permissions',
'-p',
'List skills available under .claude/skills and summarize the review skill.',
'--output-format',
'text',
],
workingDir: '/workspace',
timeout: 600,
});
console.log(result.stdout);
await sandbox.kill();
RT=$(gravixlayer runtime create \
--template claude \
--provider "$ANTHROPIC_PROVIDER_ID" \
--network-policy "$CLAUDE_NETWORK_POLICY_ID" \
--env "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" \
--timeout 1800 \
--wait \
--output json | jq -r '.runtime_id')
gravixlayer runtime files mkdir "$RT" /workspace/.claude/skills/review
cat <<'EOF' | gravixlayer runtime files write "$RT" /workspace/.claude/skills/review/SKILL.md
---
name: review
description: Prefer security and test coverage in PR reviews
---
When reviewing, check authz, input validation, and missing tests.
EOF
gravixlayer runtime exec "$RT" --timeout 600 --workdir /workspace -- \
claude --dangerously-skip-permissions \
-p 'List skills available under .claude/skills and summarize the review skill.' \
--output-format text
gravixlayer runtime kill "$RT" -y
API="https://api.gravixlayer.ai/v1"
AUTH=(-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" -H "Content-Type: application/json")
RT=$(curl -sS -X POST "$API/agents/runtime" "${AUTH[@]}" \
-d "{
\"template\": \"claude\",
\"cloud\": \"aws\",
\"region\": \"us-east-1\",
\"timeout\": 1800,
\"providers\": [\"$ANTHROPIC_PROVIDER_ID\"],
\"network_policy_ids\": [\"$CLAUDE_NETWORK_POLICY_ID\"],
\"env_vars\": {\"ANTHROPIC_API_KEY\": \"$ANTHROPIC_API_KEY\"}
}" | jq -r '.runtime_id')
# Wait until running, then:
curl -sS -X POST "$API/agents/runtime/$RT/files/create-directory" "${AUTH[@]}" \
-d '{"path": "/workspace/.claude/skills/review"}' | jq .
curl -sS -X POST "$API/agents/runtime/$RT/files/write" "${AUTH[@]}" \
-d '{
"path": "/workspace/.claude/skills/review/SKILL.md",
"content": "---\nname: review\ndescription: Prefer security and test coverage in PR reviews\n---\nWhen reviewing, check authz, input validation, and missing tests.\n"
}' | jq .
curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
-d '{
"command": "claude",
"args": [
"--dangerously-skip-permissions",
"-p", "List skills available under .claude/skills and summarize the review skill.",
"--output-format", "text"
],
"working_dir": "/workspace",
"timeout": 600000
}' | jq .
curl -sS -X DELETE "$API/agents/runtime/$RT" \
-H "Authorization: Bearer $GRAVIXLAYER_API_KEY"
Register an MCP server
Works on the platformclaude template. Your network policy must allow Anthropic and the MCP host. Keep tokens in Identity / env_vars, not in the image.
import os
import uuid
from gravixlayer import GravixLayer
ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"]
MCP_URL = os.environ["MCP_URL"] # e.g. https://mcp.example.com/mcp
MCP_TOKEN = os.environ["MCP_TOKEN"]
MCP_HOST = os.environ["MCP_HOST"] # e.g. mcp.example.com
client = GravixLayer() # defaults to cloud="aws", region="us-east-1"
provider = client.identity.providers.create(
name=f"anthropic-mcp-{uuid.uuid4().hex[:8]}",
provider_type="api_key",
secrets=[
{"key": "ANTHROPIC_API_KEY", "value": ANTHROPIC_API_KEY},
{"key": "MCP_TOKEN", "value": MCP_TOKEN},
],
)
policy = client.network_policies.create(
name=f"claude-mcp-{uuid.uuid4().hex[:8]}",
egress_mode="allowlist",
rules=[
{"destination": "api.anthropic.com", "port": 443, "protocol": "tcp"},
{"destination": MCP_HOST, "port": 443, "protocol": "tcp"},
],
)
sandbox = client.runtime.create(
template="claude",
providers=[provider.id],
network_policy_ids=[policy.id],
env_vars={
"ANTHROPIC_API_KEY": ANTHROPIC_API_KEY,
"MCP_TOKEN": MCP_TOKEN,
},
timeout=1800,
)
add = sandbox.run_cmd(
command="claude",
args=[
"mcp",
"add",
"--transport",
"http",
"team-mcp",
MCP_URL,
"--header",
f"Authorization: Bearer {MCP_TOKEN}",
],
timeout=120,
)
print(add.stdout, add.stderr)
result = sandbox.run_cmd(
command="claude",
args=[
"--dangerously-skip-permissions",
"-p",
"Use the team-mcp tools to list available tools and summarize them in three bullets.",
"--output-format",
"text",
],
timeout=900,
)
print(result.stdout)
sandbox.kill()
client.identity.providers.delete(provider.id)
client.network_policies.delete(policy.id)
import { GravixLayer } from 'gravixlayer';
const ANTHROPIC_API_KEY = process.env['ANTHROPIC_API_KEY']!;
const MCP_URL = process.env['MCP_URL']!; // e.g. https://mcp.example.com/mcp
const MCP_TOKEN = process.env['MCP_TOKEN']!;
const MCP_HOST = process.env['MCP_HOST']!; // e.g. mcp.example.com
const client = new GravixLayer(); // defaults to cloud="aws", region="us-east-1"
const provider = await client.identity.providers.create(`anthropic-mcp-${Date.now()}`, {
providerType: 'api_key',
secrets: [
{ key: 'ANTHROPIC_API_KEY', value: ANTHROPIC_API_KEY },
{ key: 'MCP_TOKEN', value: MCP_TOKEN },
],
});
const policy = await client.networkPolicies.create(`claude-mcp-${Date.now()}`, {
egressMode: 'allowlist',
rules: [
{ destination: 'api.anthropic.com', port: 443, protocol: 'tcp' },
{ destination: MCP_HOST, port: 443, protocol: 'tcp' },
],
});
const sandbox = await client.runtime.create({
template: 'claude',
providers: [provider.id],
networkPolicyIds: [policy.id],
envVars: {
ANTHROPIC_API_KEY,
MCP_TOKEN,
},
timeout: 1800,
});
const add = await sandbox.runCmd('claude', {
args: [
'mcp',
'add',
'--transport',
'http',
'team-mcp',
MCP_URL,
'--header',
`Authorization: Bearer ${MCP_TOKEN}`,
],
timeout: 120,
});
console.log(add.stdout, add.stderr);
const result = await sandbox.runCmd('claude', {
args: [
'--dangerously-skip-permissions',
'-p',
'Use the team-mcp tools to list available tools and summarize them in three bullets.',
'--output-format',
'text',
],
timeout: 900,
});
console.log(result.stdout);
await sandbox.kill();
await client.identity.providers.delete(provider.id);
await client.networkPolicies.delete(policy.id);
# CLAUDE_NETWORK_POLICY_ID must already allow your MCP host
RT=$(gravixlayer runtime create \
--template claude \
--provider "$ANTHROPIC_PROVIDER_ID" \
--network-policy "$CLAUDE_NETWORK_POLICY_ID" \
--env "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" \
--env "MCP_TOKEN=$MCP_TOKEN" \
--timeout 1800 \
--wait \
--output json | jq -r '.runtime_id')
gravixlayer runtime exec "$RT" --timeout 120 -- \
claude mcp add --transport http team-mcp "$MCP_URL" \
--header "Authorization: Bearer $MCP_TOKEN"
gravixlayer runtime exec "$RT" --timeout 900 -- \
claude --dangerously-skip-permissions \
-p 'Use the team-mcp tools to list available tools.' \
--output-format text
gravixlayer runtime kill "$RT" -y
API="https://api.gravixlayer.ai/v1"
AUTH=(-H "Authorization: Bearer $GRAVIXLAYER_API_KEY" -H "Content-Type: application/json")
RT=$(curl -sS -X POST "$API/agents/runtime" "${AUTH[@]}" \
-d "{
\"template\": \"claude\",
\"cloud\": \"aws\",
\"region\": \"us-east-1\",
\"timeout\": 1800,
\"providers\": [\"$ANTHROPIC_PROVIDER_ID\"],
\"network_policy_ids\": [\"$CLAUDE_NETWORK_POLICY_ID\"],
\"env_vars\": {
\"ANTHROPIC_API_KEY\": \"$ANTHROPIC_API_KEY\",
\"MCP_TOKEN\": \"$MCP_TOKEN\"
}
}" | jq -r '.runtime_id')
# Wait until running, then (timeout is milliseconds):
curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
-d "{
\"command\": \"claude\",
\"args\": [
\"mcp\", \"add\", \"--transport\", \"http\", \"team-mcp\", \"$MCP_URL\",
\"--header\", \"Authorization: Bearer $MCP_TOKEN\"
],
\"timeout\": 120000
}" | jq .
curl -sS -X POST "$API/agents/runtime/$RT/commands/run" "${AUTH[@]}" \
-d '{
"command": "claude",
"args": [
"--dangerously-skip-permissions",
"-p", "Use the team-mcp tools to list available tools.",
"--output-format", "text"
],
"timeout": 900000
}' | jq .
curl -sS -X DELETE "$API/agents/runtime/$RT" \
-H "Authorization: Bearer $GRAVIXLAYER_API_KEY"
Tips
| Tip | Why |
|---|---|
Prefer platform claude unless you need extras | No build step; always up to date with the platform |
| Allowlist Anthropic + MCP + git only | Smaller blast radius with permission bypass |
Bake CLAUDE.md, never bake secrets | Secrets via Identity |
| Smoke-test with get-started | Catch egress / key issues before CI |