diff --git a/packages/cli/src/shared/orchestrate.ts b/packages/cli/src/shared/orchestrate.ts index db3fdb218..6d9436978 100644 --- a/packages/cli/src/shared/orchestrate.ts +++ b/packages/cli/src/shared/orchestrate.ts @@ -666,6 +666,7 @@ async function postInstall( // Append skill env vars to .spawnrc so MCP servers can resolve ${VAR} at runtime const skillEnvPairs = (process.env.SPAWN_SKILL_ENV_PAIRS ?? "").split(",").filter(Boolean); if (skillEnvPairs.length > 0) { + const validKeyRe = /^[A-Z_][A-Z0-9_]*$/; const envLines = skillEnvPairs .map((pair) => { const eqIdx = pair.indexOf("="); @@ -673,15 +674,30 @@ async function postInstall( return ""; } const key = pair.slice(0, eqIdx); + if (!validKeyRe.test(key)) { + logWarn(`Skipping invalid skill env var key: ${key}`); + return ""; + } const val = pair.slice(eqIdx + 1); - return `export ${key}='${val.replace(/'/g, "'\\''")}'`; + const valB64 = Buffer.from(val).toString("base64"); + if (!/^[A-Za-z0-9+/=]+$/.test(valB64)) { + logWarn(`Skipping skill env var with invalid base64: ${key}`); + return ""; + } + return `export ${key}="$(echo '${valB64}' | base64 -d)"`; }) .filter(Boolean) .join("\n"); if (envLines) { - await asyncTryCatch(() => - cloud.runner.runServer(`printf '\\n# [spawn:skills]\\n${envLines}\\n' >> ~/.spawnrc`), - ); + const payload = `\n# [spawn:skills]\n${envLines}\n`; + const payloadB64 = Buffer.from(payload).toString("base64"); + if (!/^[A-Za-z0-9+/=]+$/.test(payloadB64)) { + logWarn("Unexpected characters in skill env payload base64"); + } else { + await asyncTryCatch(() => + cloud.runner.runServer(`printf '%s' '${payloadB64}' | base64 -d >> ~/.spawnrc`), + ); + } } } }