Skip to content

docs: add Claude Code + CubeSandbox integration guide and example#694

Open
LangQi99 wants to merge 3 commits into
TencentCloud:masterfrom
LangQi99:docs/claude-code-integration
Open

docs: add Claude Code + CubeSandbox integration guide and example#694
LangQi99 wants to merge 3 commits into
TencentCloud:masterfrom
LangQi99:docs/claude-code-integration

Conversation

@LangQi99

@LangQi99 LangQi99 commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Addresses the Claude Code sub-direction of #644 with an end-to-end
integration guide plus a runnable example project.

  • Runnable example at examples/claude-code-integration/:
    • Dockerfile — stacks Node.js 20 + Anthropic's official @anthropic-ai/claude-code on top of ghcr.io/tencentcloud/cubesandbox-base:2026.16 so envd is already listening on :49983.
    • run_claude.py — headless one-shot Agent driver (upload optional seed → claude --print --allowedTools ... → stream stdout back).
    • resume_claude.py — pause/resume across two turns; verifies /workspace + /root/.claude/ survive the snapshot.
    • network_policy.py — recommended credential vault pattern: default-deny egress, allow only api.anthropic.com, CubeEgress inject attaches x-api-key on the wire so the key never enters the VM.
    • Bilingual README.md / README_zh.md, env.example, requirements.txt, env_utils.py.
  • Integration guide in both languages, following the _template.md frontmatter and referenced in the index tables:

Covers the acceptance criteria from #644:

  1. end-to-end integration guide (template build, deps, key injection, egress policy, session persistence via snapshots),
  2. runnable example project with build script and README,
  3. typical use cases + best practices,
  4. FAQ / troubleshooting section aligned with existing egress / auth / template mechanics,
  5. article dropped into the docs/guide/integrations system on the existing template.

Test plan

Verified locally without a live cluster (I don't have one on this laptop):

  • docker build --platform linux/amd64 succeeds (~125 s).
  • envd /health inside the running image returns 204 (readiness probe will pass).
  • claude --version -> 2.1.197 (Claude Code); envd -version -> 0.5.13; node -v -> v20.20.2.
  • git, python3, rg, jq all present.
  • claude --help confirms every flag used by the scripts (--print, --allowedTools, --verbose, --output-format stream-json, --dangerously-skip-permissions) is supported.
  • All four Python scripts pass py_compile.compile(..., doraise=True).
  • End-to-end run against a live CubeSandbox + Anthropic key (needs cluster access — please run python run_claude.py, python resume_claude.py, python network_policy.py before merge).

Notes for the maintainers

  • Doc filenames match kebab-case in both language dirs; frontmatter keys are identical between languages.
  • Example directory follows the same layout convention as openai-agents-example / openclaw-integration.
  • No changes to core code — docs + example only.

Closes part of #644 (Claude Code sub-direction). CodeBuddy / OpenCode remain open for other contributors.

Adds an end-to-end integration for running Anthropic Claude Code —
the terminal-native AI coding agent — inside CubeSandbox MicroVMs,
addressing part of TencentCloud#644 (Claude Code sub-direction).

- examples/claude-code-integration/: runnable project with a
  cubesandbox-base + Node 20 + @anthropic-ai/claude-code Dockerfile,
  headless one-shot demo (run_claude.py), pause/resume across turns
  (resume_claude.py), and CubeEgress inject rule so the API key never
  enters the sandbox (network_policy.py). English + Chinese README.
- docs/guide/integrations/claude-code.md and its zh counterpart:
  full integration guide covering template build, direct-vs-vault key
  flows, snapshot-based long-task resume, and troubleshooting; both
  language index pages updated.

Verified locally: docker build succeeds; envd /health returns 204;
claude --version reports 2.1.197; every CLI flag used in the scripts
(--print, --allowedTools, --verbose, --output-format stream-json) is
supported by the current CLI; all Python scripts compile with py_compile.
@LangQi99
LangQi99 requested a review from tinklone as a code owner July 1, 2026 07:40
"destroying it. The sandbox_id is printed on exit.",
)
p.add_argument(
"--verbose",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (rename flag): The argparse flag --verbose actually enables --output-format stream-json, transforming the output into machine-readable JSON events. These are semantically different concerns — verbosity and output format. A user reaching for --verbose to debug something will get JSON they may not expect.

Consider renaming to --stream-json to accurately describe what it does, or decouple the two so each can be set independently:

key from this dict entirely and let CubeEgress attach it on the wire.
"""
envs: dict[str, str] = {}
for name in (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undocumented env vars risk: build_agent_env() silently forwards CLAUDE_CODE_USE_BEDROCK and CLAUDE_CODE_USE_VERTEX into the sandbox, but neither the integration guide nor the README environment variable tables documents these. A user pointing ANTHROPIC_BASE_URL at a Bedrock endpoint without also knowing to set CLAUDE_CODE_USE_BEDROCK=true will get an authentication failure from Claude Code — the CLI uses different auth logic for Bedrock vs direct Anthropic. Same applies to Vertex.

Please add these to the env variable tables in both the guide (§3) and the README (§2), noting which gateway each enables.

envs = build_agent_env()

print(f"[cube] creating sandbox from template={template_id}")
sandbox = Sandbox.create(template=template_id, timeout=1800)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sandbox leak risk: If Sandbox.create() succeeds but accessing .sandbox_id raises (e.g., transient API error), sandbox is assigned but code never enters the try block, so finally cleanup never runs. Move the .sandbox_id access inside the try block to guarantee cleanup.

print("\n=== rationale.md ===")
print(rationale.stdout)

return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exit code not propagated: main() always returns 0, but the two run_claude() calls at lines 78 and 92 can fail. The exit codes are printed to stdout but never captured or checked. A user running python resume_claude.py && echo "ok" would see "ok" even when the agent task failed. Consider tracking accumulated failures and returning a non-zero exit code when any invocation fails.

&& curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* \
&& npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npm cache bloat: npm install -g downloads and caches the package tarball in /root/.npm/. This cache is never cleaned, permanently inflating the image by ~10-30 MB. Since apt-get cleanup (rm -rf /var/lib/apt/lists/*) is already on line 33 in the same RUN chain, adding npm cache clean --force after claude --version would eliminate the bloat without adding a layer.

@@ -0,0 +1,24 @@
# Required: Cube API server address

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: missing .gitignore for .env in the example directory. The env.example instructs users to copy it to .env and fill in secrets (API keys, template IDs, cluster URLs). Without a .gitignore in examples/claude-code-integration/, git status will show the .env file and it could be accidentally committed. Consider adding a .gitignore with .env in this directory as a safety measure for users who fork/clone the repo.

@cubesandboxbot

cubesandboxbot Bot commented Jul 1, 2026

Copy link
Copy Markdown

Code review: PR #694 — Claude Code + CubeSandbox integration

Overall impression: a well-structured, comprehensive PR. The code quality is high, the documentation is thorough and consistent across both languages, and the security-sensitive patterns (shlex.quote, credential vault, per-exec env forwarding) are handled correctly. Below are the findings I consider noteworthy after reviewing the reviewer reports.


Noteworthy issues

1. Sandbox resource leak with --pause + early error (run_claude.py:179-193)

When --pause is requested but an error occurs before sandbox.pause() is reached (e.g., preflight fails), the finally block skips cleanup because not args.pause is False. The sandbox stays alive until timeout. Track whether pause was actually performed rather than guarding on the flag alone.

2. Non-reproducible Docker builds (Dockerfile:25)

ARG CLAUDE_CODE_VERSION=latest means the same docker build produces different images over time. A breaking upstream change becomes a silent CI/CD failure. Pin a specific version as the default, and encourage --build-arg in README build instructions for production use.

3. Doc snippet doesn't match version table's --build-arg claim (docs/guide/integrations/claude-code.md:96-111)

The version table advertises version pinning via --build-arg CLAUDE_CODE_VERSION=x.y.z, but the Dockerfile snippet in the guide hardcodes npm install -g @anthropic-ai/claude-code with no variable. The actual Dockerfile does use the ARG. Please align the snippet and the table.

4. network_policy.py docstring understates key storage (network_policy.py:14-16)

The docstring says the key "only lives in the operator's rule list", but the rule list (containing the key) is transmitted to and stored by the CubeAPI control plane as part of Sandbox.create(network={"rules": ...}). Users should understand this trade-off: the key never enters the VM's process space, but it does pass through the Cube control plane. Consider updating the docstring and README section 5 to state this explicitly.

5. CLAUDE_WORKSPACE and SSL_CERT_FILE missing from env var tables

These variables are documented in env.example and consumed by the scripts, but absent from all four env var tables (both READMEs and both integration guides). Either add them to the tables or remove them from env.example if they are not intended for end-user documentation.


Minor / optional suggestions

  • No non-root user in Dockerfile — Claude Code and all spawned processes run as root inside the VM. While KVM isolation prevents host escape, adding a USER directive (e.g. a claude user) would provide defense-in-depth within the sandbox boundary. Would require adjusting the user="root" parameters in the driver scripts.
  • upload_seed has no file size limit (run_claude.py:100-107) — Consider adding a size check or using streaming to avoid loading the entire file into memory. Low risk since --seed is a developer-facing option.

Review performed by automated reviewer agents (code-quality, security, documentation-accuracy). Findings synthesized and assessed by the lead reviewer.

Applies six actionable review comments from cubesandboxbot on TencentCloud#694:

- run_claude.py: rename `--verbose` to `--stream-json` so the flag name
  actually describes what it does (enable stream-json output). Verbosity
  and output format are now decoupled semantically.
- env_utils.py + docs + env.example: document `CLAUDE_CODE_USE_BEDROCK`
  and `CLAUDE_CODE_USE_VERTEX`. Both are silently forwarded by the helper,
  and users pointing `ANTHROPIC_BASE_URL` at a Bedrock/Vertex endpoint
  need to know these must also be set. Added to env var tables in the
  bilingual guides and READMEs, with mutual-exclusion note.
- resume_claude.py: move `.sandbox_id` access inside the try block so a
  transient error there can't leak the sandbox past `finally`. Track
  per-turn agent failures and propagate a non-zero exit code to callers.
- Dockerfile: `npm cache clean --force` + `rm -rf /root/.npm` after the
  global install. Local rebuild dropped image size from 1.33 GB to 287 MB.
- examples/claude-code-integration/.gitignore: ignore .env, __pycache__,
  and *.pyc so a user who copies env.example -> .env cannot accidentally
  commit their API key.

Verified locally: docker rebuild succeeds; envd /health still 204;
claude --version still 2.1.197; cache is gone from /root/.npm/_cacache;
all four Python scripts still compile.
@LangQi99

LangQi99 commented Jul 1, 2026

Copy link
Copy Markdown
Author

Thanks for the review — pushed 9d10891 addressing all six comments:

# Comment Fix
1 --verbose doesn't mean verbosity, it means stream-json output Renamed to --stream-json; docstring/help/README/guide all updated
2 CLAUDE_CODE_USE_BEDROCK / CLAUDE_CODE_USE_VERTEX silently forwarded but undocumented Added to env-var tables in both READMEs and both integration guides; env.example now lists them with mutual-exclusion note
3 resume_claude.py sandbox leak if .sandbox_id access raised Moved .sandbox_id inside the try; also propagate accumulated per-turn exit codes
4 run_claude() exit codes swallowed Now returned + summed; main() returns non-zero when any turn failed
5 npm install -g bloat in /root/.npm Added npm cache clean --force && rm -rf /root/.npm in the same RUN chain — local rebuild dropped image size from 1.33 GB → 287 MB
6 Missing .gitignore for .env Added examples/claude-code-integration/.gitignore covering .env, __pycache__/, *.pyc

Local re-verification after the changes:

  • docker build --platform linux/amd64 still succeeds
  • envd /health inside the running image still returns 204
  • claude --version still 2.1.197 (Claude Code)
  • test -d /root/.npm/_cacache returns "cache cleaned"
  • All four Python scripts still py_compile

The end-to-end run against a live cluster + real Anthropic key still needs a maintainer with cluster access, since I don't have one on hand.


envs = build_agent_env()
print(f"[cube] creating sandbox from template={template_id}")
with Sandbox.create(template=template_id, timeout=args.timeout) as sandbox:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: --pause flag is defeated by the context manager

When --pause is used, the sandbox is paused on line 171, but then the with block exits, and the context manager's __exit__ almost certainly calls kill() — destroying the just-paused sandbox. The --pause flag is effectively a no-op.

Compare with resume_claude.py, which correctly manages the sandbox lifecycle via try/finally (no context manager) precisely so the sandbox can outlive the lexical scope.

Recommendation: Either remove the --pause flag, or manage the sandbox lifecycle manually (like resume_claude.py) when pause is active:

sandbox = Sandbox.create(template=template_id, timeout=args.timeout)
try:
    ...
    if args.pause:
        sandbox.pause()
finally:
    if not args.pause:
        sandbox.kill()

on_stderr=lambda m: sys.stderr.write(m),
)
print(f"\n[claude] exit_code={result.exit_code}")
return result.exit_code or 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: result.exit_code or 0 conflates None with 0

If the E2B SDK returns None for exit_code (which some command-execution APIs do in error scenarios), None or 0 evaluates to 0, and the caller treats it as success. The failures counter on lines 82/96 won't increment, masking a real failure.

Recommendation: Use this instead to distinguish "exit code 0 (success)" from "no exit code returned (error)":

return result.exit_code if result.exit_code is not None else 1

print(f"[preflight] {result.stdout.strip()}")


def upload_seed(sandbox: Sandbox, workspace: str, seed: str) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: --seed can upload arbitrary local files into the sandbox

The upload_seed function reads any local file the user specifies and writes it into the sandbox where the Claude Code agent can read and potentially exfiltrate it (by including file contents in its API calls to api.anthropic.com). While the destination safely uses only seed_path.name (preventing remote path traversal), there is no source-path restriction. A user could inadvertently upload SSH keys, cloud credentials, or other sensitive files.

Recommendation: Either add a warning in the --seed help text like:

"WARNING: This file will be readable by the agent inside the sandbox. "
"Do not point this at credentials, SSH keys, or other secrets."

or validate that the file is within an expected project directory.

envs = build_agent_env()

print(f"[cube] creating sandbox from template={template_id}")
sandbox = Sandbox.create(template=template_id, timeout=1800)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup risk: Sandbox.create() outside a context manager

Unlike run_claude.py, this script uses a bare Sandbox.create() call. If Sandbox.create() itself throws, sandbox is never assigned, causing a NameError in the finally block that silently eats the original exception. Additionally, if Sandbox.connect(sid) fails on line 95, sandbox still references the paused sandbox, and calling .kill() on it may not properly release resources.

Recommendation: Guard the finally block with a check that sandbox was assigned before calling .kill():

finally:
    if 'sandbox' in locals() and sandbox:
        try:
            sandbox.kill()
        except Exception as exc:
            print(f"[cleanup] sandbox.kill() raised {exc!r}")


envs = build_agent_env()
print(f"[cube] creating sandbox from template={template_id}")
with Sandbox.create(template=template_id, timeout=args.timeout) as sandbox:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: --pause flag is defeated by the context manager

When --pause is used, the sandbox is paused on line 171, but then the with block exits, and the context manager's __exit__ almost certainly calls kill() — destroying the just-paused sandbox. The --pause flag is effectively a no-op.

Compare with resume_claude.py, which correctly manages the sandbox lifecycle via try/finally (no context manager) precisely so the sandbox can outlive the lexical scope.

Recommendation: Either remove the --pause flag, or manage the sandbox lifecycle manually (like resume_claude.py) when pause is active:

on_stderr=lambda m: sys.stderr.write(m),
)
print(f"\n[claude] exit_code={result.exit_code}")
return result.exit_code or 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: result.exit_code or 0 conflates None with 0

If the E2B SDK returns None for exit_code (which some command-execution APIs do in error scenarios), None or 0 evaluates to 0, and the caller treats it as success. The failures counter on lines 82/96 won't increment, masking a real failure.

Recommendation: Use return result.exit_code if result.exit_code is not None else 1 to distinguish "exit code 0 (success)" from "no exit code returned (error)."

print(f"[preflight] {result.stdout.strip()}")


def upload_seed(sandbox: Sandbox, workspace: str, seed: str) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: --seed can upload arbitrary local files into the sandbox

The upload_seed function reads any local file the user specifies and writes it into the sandbox where the Claude Code agent can read and potentially exfiltrate it (by including file contents in its API calls to api.anthropic.com). While the destination safely uses only seed_path.name (preventing remote path traversal), there is no source-path restriction. A user could inadvertently upload SSH keys, cloud credentials, or other sensitive files.

Recommendation: Either add a warning in the --seed help text, or validate that the file is within an expected project directory.

envs = build_agent_env()

print(f"[cube] creating sandbox from template={template_id}")
sandbox = Sandbox.create(template=template_id, timeout=1800)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup risk: Sandbox.create() outside a context manager

Unlike run_claude.py, this script uses a bare Sandbox.create() call (not inside a with block). If Sandbox.create() itself throws, sandbox is never assigned, causing a NameError in the finally block that silently eats the original exception. Additionally, if Sandbox.connect(sid) fails on line 95, sandbox still references the paused sandbox, and calling .kill() on a paused sandbox may not properly release resources.

Recommendation: When Sandbox.create() is not inside a with statement, guard the finally block with a check that sandbox was assigned if 'sandbox' in locals() and sandbox: before calling .kill().

Applies four follow-up review comments on TencentCloud#694 (bot round 2):

- run_claude.py `--pause`: was a no-op because the `with Sandbox.create()`
  context manager killed the sandbox on scope exit. Switch to explicit
  try/finally with `sandbox.kill()` only when `--pause` is off, so a paused
  sandbox actually survives the script.
- run_claude.py `--seed`: add a WARNING in the help text noting the file is
  readable by the agent inside the sandbox and should not point at
  credentials, SSH keys, or other secrets.
- resume_claude.py exit code: `result.exit_code or 0` conflates None with 0
  and masks a failure when the SDK returns no exit code. Return 1 in that
  case, otherwise return the real exit_code.
- resume_claude.py finally guard: move `Sandbox.create(...)` inside the
  `try` and skip `sandbox.kill()` when `sandbox is None`, so a failure in
  `create()` no longer causes a NameError that swallows the original
  exception.
@LangQi99

LangQi99 commented Jul 2, 2026

Copy link
Copy Markdown
Author

Round-2 auto-review surfaced four real bugs in 9d10891. Pushed a514206 fixing all four:

# Bug Fix
1 run_claude.py --pause was a no-op — with Sandbox.create() __exit__ killed the sandbox immediately after pause Switched to explicit try/finally; only call sandbox.kill() when --pause is off, so a paused sandbox actually survives the script
2 resume_claude.py result.exit_code or 0 conflated None (SDK error) with 0 (success) Now returns 1 when exit_code is None, otherwise the real code — failures counter now increments as intended
3 run_claude.py --seed had no warning that seeded files are readable by the agent (potential exfil path for SSH keys / creds) Added a WARNING clause to the --seed help text
4 resume_claude.py finally block would NameError on sandbox.kill() if Sandbox.create() itself raised, silently swallowing the original exception Moved Sandbox.create(...) inside the try; finally now guards with if sandbox is not None

Verified locally: all four Python scripts still py_compile clean. The image build hasn't changed, so the earlier smoke test (envd /health = 204, claude --version = 2.1.197, cache cleaned, 287 MB) still applies.

Comment on lines +179 to +188
if args.pause:
info = sandbox.pause()
print(json.dumps(
{"paused": True, "sandbox_id": sandbox.sandbox_id, "info": info},
default=str,
))

exit_code = result.exit_code
return 0 if exit_code == 0 else (exit_code if exit_code is not None else 1)
finally:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resource leak: sandbox not killed when --pause is combined with an early error.

If a user passes --pause but preflight_agent() fails (or any error occurs before sandbox.pause() is reached), the finally block skips cleanup because not args.pause is still False. The sandbox remains alive until its timeout expires, wasting cluster resources.

Consider tracking whether the pause was actually performed:

paused = False
try:
    ...
    if args.pause:
        info = sandbox.pause()
        paused = True
        ...
finally:
    if not paused:
        sandbox.kill()


ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_MAJOR=20
ARG CLAUDE_CODE_VERSION=latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default latest produces non-reproducible builds.

CLAUDE_CODE_VERSION=latest means the same docker build command produces different images on different days. A breaking change upstream can silently break CI/CD. Consider pinning a specific version as the default (and documenting it in the README build steps), or at minimum adding a comment urging users to pin for production deployments.


ENV CLAUDE_CODE_HOME=/root/.claude
WORKDIR /workspace
EXPOSE 49983

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code snippet doesn't support the --build-arg version pinning advertised in the version table.

The version table (line 32) says the CLI version can be pinned via --build-arg CLAUDE_CODE_VERSION=x.y.z, but the Dockerfile snippet here uses a hardcoded npm install -g @anthropic-ai/claude-code with no ARG or variable reference. The actual Dockerfile does include ARG CLAUDE_CODE_VERSION=latest and @${CLAUDE_CODE_VERSION} — please align the snippet with the real file so readers who copy it also get reproducible builds.

@fslongjin

Copy link
Copy Markdown
Member

Refs #644

@fslongjin

Copy link
Copy Markdown
Member

Hello, thank you for your contribution! Since we have a large number of participants in this event, to speed up the PR review process, could you please attach some screenshots and logs (both are needed) showing the verification process and results for this PR? Thank you for your participation!

@fslongjin fslongjin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello~Can you add more test results/screenshots which can prove this feature can work in a REAL CubeSandbox cluster?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants