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
- 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.
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
- 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.
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
- Environment variable ${envName} is empty or not set.
- Environment variable ${opts.apiKeyEnv.trim()} is not set
- Environment variable ${opts.valueEnv} is empty or unset.
- Paperclip skills must be directories.
- Failed to merge concurrent SSH git histories for ${currentHe
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/841f65d50d4dbc5c.
Report an issue: GitHub.