paperclipai/paperclip · error · Error

Invalid SSH environment variable key: ${key}

Error message

Invalid SSH environment variable key: ${key}

What it means

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.

Source

Thrown at packages/adapter-utils/src/ssh.ts:1176

  config: SshConnectionConfig,
  remoteCommand: string,
  options: {
    env?: Record<string, string>;
    stdin?: string;
    timeoutMs?: number;
    maxBuffer?: number;
  } = {},
): Promise<SshCommandResult> {
  let cleanup: () => Promise<void> = () => Promise.resolve();
  try {
    const auth = await createSshAuthArgs(config);
    cleanup = auth.cleanup;
    const sshArgs = [...auth.args];
    const envEntries = Object.entries(options.env ?? {})
      .filter((entry): entry is [string, string] => typeof entry[1] === "string");
    for (const [key] of envEntries) {
      if (!isValidShellEnvKey(key)) {
        throw new Error(`Invalid SSH environment variable key: ${key}`);
      }
    }

    // Mirror buildSshSpawnTarget: source the login profiles first, then run
    // `env KEY=VAL cmd` so user-supplied identity overrides win over anything a
    // profile re-exports. The SSH target is an operator-configured host, not a
    // Paperclip sandbox image, so it can expose `node` or an agent CLI only
    // through a login profile; a non-login SSH command would miss that PATH.
    // Source `/etc/profile` first so a host that exposes the PATH through
    // `/etc/profile.d` scripts still resolves node and the agent CLI.
    // The script no longer sources `nvm.sh`; a profile that adds nvm still runs.
    // .bash_profile typically sources .bashrc itself; only source .bashrc
    // directly when no .bash_profile exists, so a host that adds nvm in
    // .bashrc still resolves node without a double-run of the setup.
    const envArgs = envEntries.map(([key, value]) => `${key}=${shellQuote(value)}`);
    const remoteScript = [
      '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',

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Sanitize env keys before calling runSshCommand: replace non-alphanumeric characters and uppercase, or drop keys that fail /^[A-Za-z_][A-Za-z0-9_]*$/.
  2. Rename offending config keys to use underscores (e.g. 'http-proxy' -> 'HTTP_PROXY').
  3. Validate the env object upstream at config-load time so invalid keys never reach the SSH layer.

Example fix

// before
await runSshCommand(spec, cmd, { env: { "http-proxy": "http://proxy:8080" } });
// after
await runSshCommand(spec, cmd, { env: { HTTP_PROXY: "http://proxy:8080" } });
Defensive patterns

Strategy: validation

Validate before calling

function sanitizeSshEnv(env) {
  const out = {};
  for (const [k, v] of Object.entries(env)) {
    if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(k) && typeof v === "string") out[k] = v;
  }
  return out;
}
await runSshCommand(spec, cmd, { env: sanitizeSshEnv(env) });

Type guard

function isValidShellEnvKey(key) {
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
}

Prevention

When it happens

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

Common situations: 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.

Related errors


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