{"record":{"id":"841f65d50d4dbc5c","repo":"paperclipai/paperclip","slug":"invalid-ssh-environment-variable-key-key","errorCode":null,"errorMessage":"Invalid SSH environment variable key: ${key}","messagePattern":"Invalid SSH environment variable key: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/ssh.ts","lineNumber":1176,"sourceCode":"  config: SshConnectionConfig,\n  remoteCommand: string,\n  options: {\n    env?: Record<string, string>;\n    stdin?: string;\n    timeoutMs?: number;\n    maxBuffer?: number;\n  } = {},\n): Promise<SshCommandResult> {\n  let cleanup: () => Promise<void> = () => Promise.resolve();\n  try {\n    const auth = await createSshAuthArgs(config);\n    cleanup = auth.cleanup;\n    const sshArgs = [...auth.args];\n    const envEntries = Object.entries(options.env ?? {})\n      .filter((entry): entry is [string, string] => typeof entry[1] === \"string\");\n    for (const [key] of envEntries) {\n      if (!isValidShellEnvKey(key)) {\n        throw new Error(`Invalid SSH environment variable key: ${key}`);\n      }\n    }\n\n    // Mirror buildSshSpawnTarget: source the login profiles first, then run\n    // `env KEY=VAL cmd` so user-supplied identity overrides win over anything a\n    // profile re-exports. The SSH target is an operator-configured host, not a\n    // Paperclip sandbox image, so it can expose `node` or an agent CLI only\n    // through a login profile; a non-login SSH command would miss that PATH.\n    // Source `/etc/profile` first so a host that exposes the PATH through\n    // `/etc/profile.d` scripts still resolves node and the agent CLI.\n    // The script no longer sources `nvm.sh`; a profile that adds nvm still runs.\n    // .bash_profile typically sources .bashrc itself; only source .bashrc\n    // directly when no .bash_profile exists, so a host that adds nvm in\n    // .bashrc still resolves node without a double-run of the setup.\n    const envArgs = envEntries.map(([key, value]) => `${key}=${shellQuote(value)}`);\n    const remoteScript = [\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',","sourceCodeStart":1158,"sourceCodeEnd":1194,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/ssh.ts#L1158-L1194","documentation":"Thrown by runSshCommand when an environment variable key in options.env fails the isValidShellEnvKey check (regex /^[A-Za-z_][A-Za-z0-9_]*$/). Because the key is interpolated into a remote `env KEY=VAL ...` invocation, an invalid key could break shell parsing or inject characters; validating before formatting is the injection guard.","triggerScenarios":"Calling runSshCommand(config, cmd, { env: { 'BAD-KEY': 'x', '1LEADING_DIGIT': 'y', 'has space': 'z' } }). Any key with dashes, leading digits, spaces, dots, or other shell metacharacters trips the regex.","commonSituations":"Passing config keys verbatim from user input or JSON configs that include dashes (e.g. 'http-proxy'); copying env vars whose names are valid in Node but not in POSIX shells; a key accidentally including whitespace or a newline.","solutions":["Sanitize env keys before calling runSshCommand: replace non-alphanumeric characters and uppercase, or drop keys that fail /^[A-Za-z_][A-Za-z0-9_]*$/.","Rename offending config keys to use underscores (e.g. 'http-proxy' -> 'HTTP_PROXY').","Validate the env object upstream at config-load time so invalid keys never reach the SSH layer."],"exampleFix":"// before\nawait runSshCommand(spec, cmd, { env: { \"http-proxy\": \"http://proxy:8080\" } });\n// after\nawait runSshCommand(spec, cmd, { env: { HTTP_PROXY: \"http://proxy:8080\" } });","handlingStrategy":"validation","validationCode":"function sanitizeSshEnv(env) {\n  const out = {};\n  for (const [k, v] of Object.entries(env)) {\n    if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(k) && typeof v === \"string\") out[k] = v;\n  }\n  return out;\n}\nawait runSshCommand(spec, cmd, { env: sanitizeSshEnv(env) });","typeGuard":"function isValidShellEnvKey(key) {\n  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);\n}","tryCatchPattern":null,"preventionTips":["Filter env keys at the adapter boundary before they reach runSshCommand.","Normalize config keys to UPPER_SNAKE_CASE when deriving shell env.","Reject dash/dot/space keys at config-load time."],"tags":["ssh","env","validation","shell-injection","paperclip"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}