Description
The Go-based container runtime mangles numeric and boolean values with %!s() formatting at two separate layers:
-
cloudflare:workers env module layer: Object.entries() on the cloudflare:workers env import already returns secrets as mangled strings (e.g., %!s(uint64=1) instead of "1"). This means values are corrupted before any SDK method is called.
-
setEnvVars() / exec() layer: Values passed through setEnvVars() or embedded in exec() command strings are re-formatted by the Go runtime, mangling typed-looking values even when they are correct JS strings.
This double-mangling breaks any tool that checks env var values by exact string match.
Reproduction
Layer 1: cloudflare:workers env module returns mangled values
import { env as workerEnv } from "cloudflare:workers";
// Secrets were uploaded via `wrangler secret bulk` with explicit JSON string values:
// { "ENABLE_FEATURE": "1", "MAX_TOKENS": "32000", "VERBOSE_MODE": "true" }
for (const [key, raw] of Object.entries(workerEnv)) {
console.log(`${key}=${String(raw)}`);
}
// Output:
// ENABLE_FEATURE=%!s(uint64=1) ← already mangled at JS level
// MAX_TOKENS=%!s(uint64=32000) ← already mangled at JS level
// VERBOSE_MODE=%!s(bool=true) ← already mangled at JS level
// MY_API_URL=https://api.example.com ← string values are fine
Layer 2: setEnvVars() re-mangles during exec
const sandbox = getSandbox(env.MY_SANDBOX, sessionId);
// Even with correct JS string values:
await sandbox.setEnvVars({
ENABLE_FEATURE: "1",
MAX_TOKENS: "32000",
VERBOSE_MODE: "true",
});
// Inside the container:
const result = await sandbox.exec('echo $ENABLE_FEATURE');
// stdout: %!s(uint64=1) ← mangled by Go runtime during exec
Diagnostic output from inside the container
ENABLE_FEATURE=%!s(uint64=1)
MAX_TOKENS=%!s(uint64=32000)
DISABLE_CACHE=%!s(uint64=0)
VERBOSE_MODE=%!s(bool=true)
MY_API_URL=https://api.example.com ← string values are fine
MY_REGION=us-east-1 ← string values are fine
Only values that "look like" Go types (numbers, booleans) are affected. Pure string values like URLs, API keys, and ARNs pass through correctly.
Root cause analysis
The %!s() pattern is Go's fmt.Sprintf("%s", value) output for non-string types. This indicates the Go runtime is parsing or re-interpreting string values as typed Go values at two points:
- When exposing secrets to the Worker's JS runtime via the
cloudflare:workers env module
- When processing commands sent to
/api/execute by the Sandbox SDK
What we've ruled out
- Not a JS-side issue:
String() coercion and explicit Record<string, string> typing don't help — the values are already mangled before any SDK call.
- Not a Cloudflare secret storage issue: Switching from
wrangler secret put (stdin) to wrangler secret bulk (JSON with explicit string types) did not fix it. The mangling happens downstream.
- Not a
shellEscape() issue: The SDK's shellEscape() correctly wraps values in single quotes. The command string export KEY='1' is correct when sent to /api/execute.
SDK code path (for reference)
setEnvVars() in sandbox.ts:316-354 iterates entries and calls:
const exportCommand = `export ${key}=${shellEscape(value)}`;
const result = await this.client.commands.execute(exportCommand, this.defaultSession);
The commands.execute() POSTs to /api/execute with { command: "export KEY='1'", sessionId: "..." }. The Go runtime processes this request and produces %!s(uint64=1) in the container.
Workaround
Both layers must be addressed:
- Demangle values read from
cloudflare:workers env using a regex to reverse the %!s(type=value) pattern:
function demangleEnvValue(value: string): string {
const match = value.match(/^%!s\(\w+=(.+)\)$/);
return match ? match[1] : value;
}
- Base64-encode all
export statements in JavaScript and decode inside the container via exec() to bypass the exec-layer mangling:
const envLines = Object.entries(envVars)
.map(([key, value]) => `export ${key}='${value.replace(/'/g, "'\\''")}'`)
.join("\n");
const envB64 = btoa(envLines);
const loadEnv = `echo '${envB64}' | base64 -d > /tmp/env.sh && . /tmp/env.sh`;
await sandbox.exec(`${loadEnv} && my-actual-command`);
This works because the Go runtime cannot re-interpret the opaque base64 payload. The export statements only become shell syntax inside the container's own shell after base64 -d.
Environment
@cloudflare/sandbox SDK (latest as of Feb 2026)
- Cloudflare Containers (Durable Objects-backed)
- Secrets set via both
wrangler secret put and wrangler secret bulk
Related
Description
The Go-based container runtime mangles numeric and boolean values with
%!s()formatting at two separate layers:cloudflare:workersenv module layer:Object.entries()on thecloudflare:workersenv import already returns secrets as mangled strings (e.g.,%!s(uint64=1)instead of"1"). This means values are corrupted before any SDK method is called.setEnvVars()/exec()layer: Values passed throughsetEnvVars()or embedded inexec()command strings are re-formatted by the Go runtime, mangling typed-looking values even when they are correct JS strings.This double-mangling breaks any tool that checks env var values by exact string match.
Reproduction
Layer 1:
cloudflare:workersenv module returns mangled valuesLayer 2:
setEnvVars()re-mangles during execDiagnostic output from inside the container
Only values that "look like" Go types (numbers, booleans) are affected. Pure string values like URLs, API keys, and ARNs pass through correctly.
Root cause analysis
The
%!s()pattern is Go'sfmt.Sprintf("%s", value)output for non-string types. This indicates the Go runtime is parsing or re-interpreting string values as typed Go values at two points:cloudflare:workersenv module/api/executeby the Sandbox SDKWhat we've ruled out
String()coercion and explicitRecord<string, string>typing don't help — the values are already mangled before any SDK call.wrangler secret put(stdin) towrangler secret bulk(JSON with explicit string types) did not fix it. The mangling happens downstream.shellEscape()issue: The SDK'sshellEscape()correctly wraps values in single quotes. The command stringexport KEY='1'is correct when sent to/api/execute.SDK code path (for reference)
setEnvVars()insandbox.ts:316-354iterates entries and calls:The
commands.execute()POSTs to/api/executewith{ command: "export KEY='1'", sessionId: "..." }. The Go runtime processes this request and produces%!s(uint64=1)in the container.Workaround
Both layers must be addressed:
cloudflare:workersenv using a regex to reverse the%!s(type=value)pattern:exportstatements in JavaScript and decode inside the container viaexec()to bypass the exec-layer mangling:This works because the Go runtime cannot re-interpret the opaque base64 payload. The
exportstatements only become shell syntax inside the container's own shell afterbase64 -d.Environment
@cloudflare/sandboxSDK (latest as of Feb 2026)wrangler secret putandwrangler secret bulkRelated
setEnvVarsprinting env vars in plaintext (different symptom, same underlying runtime behavior)