-
Notifications
You must be signed in to change notification settings - Fork 69
Description
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: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 fineLayer 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 execDiagnostic 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:workersenv module - When processing commands sent to
/api/executeby the Sandbox SDK
What we've ruled out
- Not a JS-side issue:
String()coercion and explicitRecord<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) towrangler secret bulk(JSON with explicit string types) did not fix it. The mangling happens downstream. - Not a
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() 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:workersenv 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
exportstatements in JavaScript and decode inside the container viaexec()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/sandboxSDK (latest as of Feb 2026)- Cloudflare Containers (Durable Objects-backed)
- Secrets set via both
wrangler secret putandwrangler secret bulk
Related
- setEnvVars printing EnvVars in plainText #374 —
setEnvVarsprinting env vars in plaintext (different symptom, same underlying runtime behavior)