From 1549ad503e98bc35f4a9005ccb43e7521812c6ec Mon Sep 17 00:00:00 2001 From: gavrielc Date: Fri, 13 Feb 2026 22:46:42 +0200 Subject: [PATCH] security: pass secrets via SDK env option and delete temp file (#213) Pass secrets to the SDK via the `env` query option instead of setting process.env, so Bash subprocesses never inherit API keys. Delete /tmp/input.json immediately after reading to remove secrets from disk. Co-authored-by: Claude Opus 4.6 --- container/Dockerfile | 2 +- container/agent-runner/src/index.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/container/Dockerfile b/container/Dockerfile index d424ac5..d48d8d9 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -51,7 +51,7 @@ RUN npm run build RUN mkdir -p /workspace/group /workspace/global /workspace/extra /workspace/ipc/messages /workspace/ipc/tasks /workspace/ipc/input # Create entrypoint script -# Secrets are passed via stdin JSON and set in Node.js — no env files or temp files on disk +# Secrets are passed via stdin JSON — temp file is deleted immediately after Node reads it # Follow-up messages arrive via IPC files in /workspace/ipc/input/ RUN printf '#!/bin/bash\nset -e\ncd /app && npx tsc --outDir /tmp/dist 2>&1 >&2\nln -s /app/node_modules /tmp/dist/node_modules\nchmod -R a-w /tmp/dist\ncat > /tmp/input.json\nnode /tmp/dist/index.js < /tmp/input.json\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh diff --git a/container/agent-runner/src/index.ts b/container/agent-runner/src/index.ts index 82e1728..9579796 100644 --- a/container/agent-runner/src/index.ts +++ b/container/agent-runner/src/index.ts @@ -358,6 +358,7 @@ async function runQuery( sessionId: string | undefined, mcpServerPath: string, containerInput: ContainerInput, + sdkEnv: Record, resumeAt?: string, ): Promise<{ newSessionId?: string; lastAssistantUuid?: string; closedDuringQuery: boolean }> { const stream = new MessageStream(); @@ -432,6 +433,7 @@ async function runQuery( 'NotebookEdit', 'mcp__nanoclaw__*' ], + env: sdkEnv, permissionMode: 'bypassPermissions', allowDangerouslySkipPermissions: true, settingSources: ['project', 'user'], @@ -493,6 +495,8 @@ async function main(): Promise { try { const stdinData = await readStdin(); containerInput = JSON.parse(stdinData); + // Delete the temp file the entrypoint wrote — it contains secrets + try { fs.unlinkSync('/tmp/input.json'); } catch { /* may not exist */ } log(`Received input for group: ${containerInput.groupFolder}`); } catch (err) { writeOutput({ @@ -503,10 +507,11 @@ async function main(): Promise { process.exit(1); } - // Set secrets as env vars for the SDK (needed for API auth). - // The PreToolUse hook strips these from every Bash subprocess. + // Build SDK env: merge secrets into process.env for the SDK only. + // Secrets never touch process.env itself, so Bash subprocesses can't see them. + const sdkEnv: Record = { ...process.env }; for (const [key, value] of Object.entries(containerInput.secrets || {})) { - process.env[key] = value; + sdkEnv[key] = value; } const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -535,7 +540,7 @@ async function main(): Promise { while (true) { log(`Starting query (session: ${sessionId || 'new'}, resumeAt: ${resumeAt || 'latest'})...`); - const queryResult = await runQuery(prompt, sessionId, mcpServerPath, containerInput, resumeAt); + const queryResult = await runQuery(prompt, sessionId, mcpServerPath, containerInput, sdkEnv, resumeAt); if (queryResult.newSessionId) { sessionId = queryResult.newSessionId; }