claude-vm-shim: pass per-session env via runtime-hook file, not cmdline

The rc parent sets a per-session JWT in CLAUDE_CODE_SESSION_ACCESS_TOKEN
plus several behavior flags (CLAUDE_CODE_ENVIRONMENT_KIND=bridge,
CLAUDE_CODE_USE_CCR_V2, CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2,
CLAUDE_CODE_WORKER_EPOCH, CLAUDE_CODE_SESSION_ID) when spawning the
cloud-session subprocess. Without them, the in-VM claude can't
authenticate to api.anthropic.com/v1/code/sessions/cse_… and the SDK
stalls at `SDKStartup: phase=connecting_transport`.

Original attempt was to forward these via `--env` to agent-vm, but the
JWT is ~700 chars and the bundle pushes the kernel cmdline past
COMMAND_LINE_SIZE=2048, after which agentd never connects and the VM
times out at handshake.

This commit uses agent-vm's existing `.agent-vm.runtime.sh` hook
mechanism: the bash prelude sources `<project>/.agent-vm.runtime.sh`
before exec'ing claude. The dispatcher writes the CLAUDE_CODE_* env
exports into that file in the workspace dir; agent-vm picks it up
automatically. No cmdline pressure.

Bonus: TLS bypass and leak-creds patches are no longer needed —
ENVIRONMENT_KIND=bridge + the JWT in env let the SDK auth and talk to
api.anthropic.com through the standard MITM substitution layer.

Adds a tiny env-dump helper (gated by CLAUDE_VM_SHIM_LOG) for future
diagnostics.

Verified e2e: rc parent shows "Capacity: 1/32 · Attached" with an
in-VM-generated session name, no more `connecting_transport` stall.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Evgeny Boger 2026-05-28 14:28:35 +00:00
parent e84b83cc4d
commit 3282eaac80

View file

@ -136,6 +136,14 @@ fn run_vm_cloud_session(forwarded: &[OsString]) -> std::io::Result<i32> {
let vm_workdir = format!("{workdir_root}/{}", std::process::id());
let _ = std::fs::create_dir_all(&vm_workdir);
// Drop a `.agent-vm.runtime.sh` hook in the workdir. agent-vm's bash
// prelude sources this before exec'ing claude, so we can publish
// `CLAUDE_CODE_*` env vars (notably the per-session JWT in
// CLAUDE_CODE_SESSION_ACCESS_TOKEN) without putting them on the kernel
// cmdline — stock x86 COMMAND_LINE_SIZE=2048 truncates and the VM
// never boots if we add ~11 of these via `agent-vm --env`.
write_cloud_runtime_hook(&vm_workdir);
let agent_vm = resolve_agent_vm_binary();
let msb_path = resolve_msb_path();
@ -156,16 +164,10 @@ fn run_vm_cloud_session(forwarded: &[OsString]) -> std::io::Result<i32> {
// Without this the in-guest claude can't read user messages from
// the cloud session.
.env("AGENT_VM_FORWARD_STDIN", "1")
// Disable TLS MITM for the Anthropic cloud-session SDK endpoint.
// The streaming transport at /v1/code/sessions/cse_… pins
// Anthropic's real certificate and stalls at `connecting_transport`
// through the microsandbox CA-signed MITM cert. Allowing this
// host to bypass interception is correct: cloud sessions
// authenticate with the real OAuth token directly (no
// placeholder injection involved).
.env("AGENT_VM_TLS_BYPASS", "api.anthropic.com")
.arg("claude")
.arg("--no-git")
.arg("--memory").arg("1")
.arg("--cpus").arg("1")
.current_dir(&vm_workdir)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
@ -200,6 +202,55 @@ fn run_vm_cloud_session(forwarded: &[OsString]) -> std::io::Result<i32> {
Ok(code)
}
/// Write a `.agent-vm.runtime.sh` hook into the workdir that exports the
/// `CLAUDE_CODE_*` env vars the rc parent set for this per-session subprocess.
/// agent-vm's bash prelude sources this file before exec'ing claude.
fn dump_env_for_debug() {
if let Ok(p) = std::env::var("CLAUDE_VM_SHIM_LOG") {
if !p.is_empty() {
let dump_path = format!("{p}.env-pid{}", std::process::id());
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&dump_path)
{
let mut vars: Vec<_> = std::env::vars().collect();
vars.sort();
for (k, v) in vars {
let _ = writeln!(f, "{k}={v}");
}
}
}
}
}
fn write_cloud_runtime_hook(vm_workdir: &str) {
dump_env_for_debug();
let path = format!("{vm_workdir}/.agent-vm.runtime.sh");
let mut script = String::from("#!/bin/sh\n# auto-generated by claude-vm-shim/dispatcher\n");
for (name, value) in std::env::vars() {
if !name.starts_with("CLAUDE_CODE_") || value.is_empty() {
continue;
}
// Skip vars that would confuse the in-VM child or are host-specific.
if matches!(name.as_str(), "CLAUDE_CODE_EXECPATH") {
continue;
}
// Single-quote escape: end quote, escaped quote, reopen quote.
let escaped = value.replace('\'', "'\\''");
script.push_str(&format!("export {name}='{escaped}'\n"));
}
let _ = std::fs::write(&path, script);
use std::os::unix::fs::PermissionsExt;
if let Ok(mut p) = std::fs::metadata(&path).map(|m| m.permissions()) {
p.set_mode(0o755);
let _ = std::fs::set_permissions(&path, p);
}
debug_log(&format!("wrote cloud runtime hook → {path}"));
}
fn resolve_msb_path() -> Option<String> {
if let Ok(p) = std::env::var("CLAUDE_VM_SHIM_MSB_PATH") {
if !p.is_empty() {