From 028c5aa00a3ef61d4ff9ce4df7030cbc4fbb79ac Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 5 May 2026 08:00:19 -0700 Subject: [PATCH] Stop leaking host process.env into the remote OpenCode SSH probe (#5274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The OpenCode adapter runs against local, SSH, and sandbox execution targets > - The Test path's hello probe spreads the Paperclip host's `process.env` into the remote process env, which over SSH gets exported on the remote shell > - On a Linux SSH target, `HOME=/Users/...` and a host XDG_CONFIG_HOME pointing at a macOS `/var/folders/...` temp dir cause OpenCode to walk a host-only path and fail with `EACCES: permission denied, mkdir '/Users'` > - This pull request stops the leak by passing only user-configured adapter env to the probe when the target is remote, matching the pattern already used by claude-local, codex-local, and gemini-local > - The benefit is the OpenCode hello probe now passes end-to-end against an SSH target without spurious filesystem errors ## What Changed - `prepareOpenCodeRuntimeConfig` short-circuits when the target is remote — the host-fs temp config dir is meaningless and harmful for a remote target - `test.ts` passes only the user-configured adapter env (no host `process.env` spread) to `runAdapterExecutionTargetProcess` when `targetIsRemote` - Local probes still get the full `runtimeEnv` so headless permission injection keeps working ## Verification - `pnpm vitest run --no-coverage --project @paperclipai/adapter-opencode-local` - `pnpm typecheck` clean - Manual: SSH OpenCode hello probe goes from `EACCES … mkdir '/Users'` to `opencode_hello_probe_passed` ## Risks Low risk — local probe behavior is unchanged; the change only narrows the env passed to remote targets, matching the pattern already shipped in sibling adapters. ## Model Used Claude Opus 4.7 (1M context) ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable — pattern mirrors existing sibling tests - [x] If this change affects the UI, I have included before/after screenshots — N/A (no UI) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --- .../opencode-local/src/server/runtime-config.ts | 14 ++++++++++++++ .../adapters/opencode-local/src/server/test.ts | 11 +++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/adapters/opencode-local/src/server/runtime-config.ts b/packages/adapters/opencode-local/src/server/runtime-config.ts index bc903e8340..6f9a338b32 100644 --- a/packages/adapters/opencode-local/src/server/runtime-config.ts +++ b/packages/adapters/opencode-local/src/server/runtime-config.ts @@ -34,6 +34,7 @@ async function readJsonObject(filepath: string): Promise export async function prepareOpenCodeRuntimeConfig(input: { env: Record; config: Record; + targetIsRemote?: boolean; }): Promise { const skipPermissions = asBoolean(input.config.dangerouslySkipPermissions, true); if (!skipPermissions) { @@ -44,6 +45,19 @@ export async function prepareOpenCodeRuntimeConfig(input: { }; } + // For remote execution targets the host XDG_CONFIG_HOME path is meaningless + // (and actively harmful — it leaks a macOS-only path into the remote Linux + // env). Callers that need to ship a runtime opencode config to the remote + // box do that via prepareAdapterExecutionTargetRuntime in execute.ts; this + // host-fs helper is local-only. + if (input.targetIsRemote) { + return { + env: input.env, + notes: [], + cleanup: async () => {}, + }; + } + const sourceConfigDir = path.join(resolveXdgConfigHome(input.env), "opencode"); const runtimeConfigHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-config-")); const runtimeConfigDir = path.join(runtimeConfigHome, "opencode"); diff --git a/packages/adapters/opencode-local/src/server/test.ts b/packages/adapters/opencode-local/src/server/test.ts index f72054dd83..d9d4245683 100644 --- a/packages/adapters/opencode-local/src/server/test.ts +++ b/packages/adapters/opencode-local/src/server/test.ts @@ -116,7 +116,7 @@ export async function testEnvironment( // Prevent OpenCode from writing an opencode.json into the working directory. env.OPENCODE_DISABLE_PROJECT_CONFIG = "true"; - const preparedRuntimeConfig = await prepareOpenCodeRuntimeConfig({ env, config }); + const preparedRuntimeConfig = await prepareOpenCodeRuntimeConfig({ env, config, targetIsRemote }); if (asBoolean(config.dangerouslySkipPermissions, true)) { checks.push({ code: "opencode_headless_permissions_enabled", @@ -279,6 +279,13 @@ export async function testEnvironment( if (variant) args.push("--variant", variant); if (extraArgs.length > 0) args.push(...extraArgs); + // For remote targets, do NOT spread the host process.env into the + // probe env: it leaks macOS-only paths (HOME=/Users/..., host + // XDG_CONFIG_HOME, TMPDIR, etc.) into the remote shell, which causes + // opencode on the remote box to try to mkdir host paths like /Users. + // Match the pattern used by claude_local / codex_local / gemini_local + // probes: send only the user-configured adapter env across SSH. + const probeEnv = targetIsRemote ? preparedRuntimeConfig.env : runtimeEnv; try { const probe = await runAdapterExecutionTargetProcess( runId, @@ -287,7 +294,7 @@ export async function testEnvironment( args, { cwd, - env: runtimeEnv, + env: probeEnv, timeoutSec: 60, graceSec: 5, stdin: "Respond with hello.",