Skip to content

Go runtime mangles numeric/boolean values with %!s() formatting at both env module and exec layers #387

@renehernandez

Description

@renehernandez

Description

The Go-based container runtime mangles numeric and boolean values with %!s() formatting at two separate layers:

  1. 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.

  2. 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:

  1. When exposing secrets to the Worker's JS runtime via the cloudflare:workers env module
  2. When processing commands sent to /api/execute by the Sandbox SDK

What we've ruled out

  1. 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.
  2. 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.
  3. 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:

  1. 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;
}
  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions