{"record":{"id":"93af642618eecb99","repo":"paperclipai/paperclip","slug":"invalid-sandbox-environment-variable-key-key-93af64","errorCode":null,"errorMessage":"Invalid sandbox environment variable key: ${key}","messagePattern":"Invalid sandbox environment variable key: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/e2b/src/plugin.ts","lineNumber":170,"sourceCode":"  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);\n}\n\n// Source the user's login profiles before exec so commands run with the same\n// PATH the user sees in an interactive shell. e2b's `sandbox.commands.run`\n// otherwise spawns a non-login, non-interactive shell whose PATH does not\n// include npm-globals or anything else the template installs via\n// .profile/.bashrc — which makes the hello probe fail with\n// `exec: <cli>: not found` even when the binary is on disk. The wrapper no\n// longer sources `nvm.sh`; the sandbox image supplies `node` on the PATH.\nfunction buildLoginShellScript(input: {\n  command: string;\n  args: string[];\n  env?: Record<string, string>;\n}): string {\n  const env = input.env ?? {};\n  for (const key of Object.keys(env)) {\n    if (!isValidShellEnvKey(key)) {\n      throw new Error(`Invalid sandbox environment variable key: ${key}`);\n    }\n  }\n  const envArgs = Object.entries(env)\n    .filter((entry): entry is [string, string] => typeof entry[1] === \"string\")\n    .map(([key, value]) => `${key}=${shellQuote(value)}`);\n  const commandParts = [shellQuote(input.command), ...input.args.map(shellQuote)].join(\" \");\n  const execLine = envArgs.length > 0\n    ? `exec env ${envArgs.join(\" \")} ${commandParts}`\n    : `exec ${commandParts}`;\n  return [\n    'if [ -f /etc/profile ]; then . /etc/profile >/dev/null 2>&1 || true; fi',\n    'if [ -f \"$HOME/.profile\" ]; then . \"$HOME/.profile\" >/dev/null 2>&1 || true; fi',\n    // .bash_profile typically sources .bashrc itself; only source .bashrc\n    // directly when no .bash_profile exists to avoid re-running idempotency-\n    // sensitive setup (nvm, PATH prepends) twice on templates that wire\n    // .bash_profile -> .bashrc.\n    'if [ -f \"$HOME/.bash_profile\" ]; then . \"$HOME/.bash_profile\" >/dev/null 2>&1 || true; elif [ -f \"$HOME/.bashrc\" ]; then . \"$HOME/.bashrc\" >/dev/null 2>&1 || true; fi',\n    'if [ -f \"$HOME/.zprofile\" ]; then . \"$HOME/.zprofile\" >/dev/null 2>&1 || true; fi',","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/plugins/sandbox-providers/e2b/src/plugin.ts#L152-L188","documentation":"Thrown by buildLoginShellScript in the E2B provider when any key of the input.env map fails the isValidShellEnvKey regex ^[A-Za-z_][A-Za-z0-9_]*$. The keys are emitted verbatim into an `exec env KEY=...` line of a login-shell wrapper, so an invalid shell identifier would either break the wrapper or inject shell syntax; the plugin rejects it up front.","triggerScenarios":"Passing an env map to a sandbox exec/probe whose keys contain characters illegal in a POSIX shell variable name: dashes (MY-VAR), dots (my.var), leading digits (1ST), spaces, or non-ASCII.","commonSituations":"Driver config copied from a YAML/JSON that allowed arbitrary key names; user-supplied env vars forwarded without sanitization; keys like 'NODE_ENV' pass but 'npm-config-foo' or 'GIT.Committer' do not.","solutions":["Sanitize env keys before passing them to the sandbox: replace non-[A-Za-z0-9_] chars with '_', uppercase, and strip leading digits.","Drop env vars whose keys cannot be converted to valid shell identifiers instead of forwarding them.","Validate the env map at config-ingest time so the bad key is reported at save, not at exec."],"exampleFix":"// before\nconst env = { 'MY-VAR': 'x', '1ST': 'y' };\n\n// after\nfunction sanitizeEnvKey(k) {\n  const cleaned = k.toUpperCase().replace(/[^A-Z0-9_]/g, '_');\n  return cleaned.replace(/^[0-9]+/, '');\n}\nconst env = Object.fromEntries(\n  Object.entries(raw).map(([k, v]) => [sanitizeEnvKey(k), v]).filter(([k]) => k)\n);","handlingStrategy":"validation","validationCode":"const SHELL_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;\nfunction sanitizeEnvForShell(env: Record<string, string>): Record<string, string> {\n  const out: Record<string, string> = {};\n  for (const [k, v] of Object.entries(env)) {\n    const key = k.toUpperCase().replace(/[^A-Z0-9_]/g, '_').replace(/^[0-9]+/, '');\n    if (key && SHELL_KEY.test(key)) out[key] = v;\n  }\n  return out;\n}","typeGuard":"function isValidShellEnvKey(value: string): boolean {\n  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);\n}","tryCatchPattern":"try {\n  buildLoginShellScript({ command, args, env });\n} catch (err) {\n  if (err instanceof Error && /Invalid sandbox environment variable key/.test(err.message)) {\n    // strip/sanitize offending keys and retry\n    return buildLoginShellScript({ command, args, env: sanitizeEnvForShell(env) });\n  }\n  throw err;\n}","preventionTips":["Validate env keys at config-ingest time, not at exec time.","Reject or sanitize keys containing '-', '.', leading digits, or spaces."],"tags":["e2b","shell","env-vars","input-validation","injection-prevention"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}