paperclipai/paperclip · error

Invalid sandbox environment variable key: ${key}

Error message

Invalid sandbox environment variable key: ${key}

What it means

Thrown by the Cloudflare sandbox bridge's `buildLoginShellScript` (exec.ts:39) when an environment-variable key fails the POSIX-identifier regex `^[A-Za-z_][A-Za-z0-9_]*$`. Each env key is interpolated bare into the login-shell wrapper as `KEY=value`, so an invalid key would either break the shell parse or inject shell metacharacters; the guard rejects it before script assembly.

Source

Thrown at packages/plugins/sandbox-providers/cloudflare/bridge-template/src/exec.ts:39

}

function randomToken(): string {
  const uuid = globalThis.crypto?.randomUUID?.();
  if (typeof uuid === "string" && uuid.length > 0) return uuid.replace(/[^a-zA-Z0-9-]/g, "");
  return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}

export function buildLoginShellScript(input: {
  command: string;
  args: string[];
  cwd?: string;
  env?: Record<string, string>;
  stdinFile?: string | null;
}): string {
  const env = input.env ?? {};
  for (const key of Object.keys(env)) {
    if (!isValidShellEnvKey(key)) {
      throw new Error(`Invalid sandbox environment variable key: ${key}`);
    }
  }

  const envArgs = Object.entries(env)
    .filter((entry): entry is [string, string] => typeof entry[1] === "string")
    .map(([key, value]) => `${key}=${shellQuote(value)}`);
  const commandParts = [shellQuote(input.command), ...input.args.map(shellQuote)].join(" ");
  const stdinRedirect = input.stdinFile ? ` < ${shellQuote(input.stdinFile)}` : "";
  // Source the common login profiles before exec so the command runs with the
  // interactive-shell PATH. The wrapper sources no `nvm.sh`; the sandbox image
  // supplies node on the PATH.
  const lines = [
    'if [ -f /etc/profile ]; then . /etc/profile >/dev/null 2>&1 || true; fi',
    'if [ -f "$HOME/.profile" ]; then . "$HOME/.profile" >/dev/null 2>&1 || true; fi',
    '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',
    'if [ -f "$HOME/.zprofile" ]; then . "$HOME/.zprofile" >/dev/null 2>&1 || true; fi',
  ];
  if (input.cwd) {

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Rename env keys to match `^[A-Za-z_][A-Za-z0-9_]*$` (uppercase SNAKE_CASE is conventional): `MY-VAR` -> `MY_VAR`.
  2. Filter/transform keys at the call site before sending the exec request: strip or replace disallowed characters.
  3. Reject empty-string keys and keys starting with a digit.

Example fix

// before
env: { "MY-VAR": "1", "2ND": "x" }
// after
env: { MY_VAR: "1", SECOND: "x" }
Defensive patterns

Strategy: validation

Validate before calling

const SHELL_ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
function sanitizeEnvKeys(env) {
  const out = {};
  for (const [k, v] of Object.entries(env ?? {})) {
    if (!SHELL_ENV_KEY.test(k)) throw new Error(`Invalid sandbox env key: ${k}`);
    if (typeof v === "string") out[k] = v;
  }
  return out;
}

Type guard

function isShellEnvKey(k: string): boolean {
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(k);
}
function isValidEnvRecord(env: unknown): env is Record<string, string> {
  if (typeof env !== "object" || env === null) return false;
  return Object.entries(env).every(([k, v]) => isShellEnvKey(k) && typeof v === "string");
}

Prevention

When it happens

Trigger: Posting an `/exec` (or probe/lease setup) request whose `env` object contains a key with characters outside `[A-Za-z0-9_]`, a leading digit, an empty string, dashes (e.g. `MY-VAR`), dots, or unicode. The check runs before the command exec wrapper is built.

Common situations: Forwarding a process.env-style object that includes dotted/kebab keys (npm/package config vars), copying a key with a typo or stray whitespace, or a caller that lets arbitrary user input populate env keys.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/d08dda10505ffb58. Report an issue: GitHub.