You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
agent env: /tmp/opencode advertised as writable scratch but owned by another user #44696
The agent environment banner advertises /tmp/opencode as "pre-created and approved for external access" and directs agents to prefer it over generic system temp dirs. On this host the directory exists but is owned by a different user than the session (uid 1000), so the agent cannot write to it:
$ printf 'probe' > /tmp/opencode/probe1.txt
/bin/bash: line 1: /tmp/opencode/probe1.txt: Permission denied
$ id
uid=1000(cunabu) ...
$ ls -ld /tmp/opencode
drwxrwxr-x 2 iconoclasta iconoclasta ...
Either /tmp/opencode is owned/mode'd so the session user can write to it, or the banner stops claiming it is approved for access when it is not.
Actual Behavior
A sanctioned scratch location is unusable; agents are steered toward a path that fails, then must fall back to un-advertised locations.
Additional Context
Low priority and mostly host-provisioning, but it is an instruction-vs-reality mismatch in shipped surface text. If /tmp/opencode is created by harness/host tooling rather than by the user, it should be created with the session user as owner; if it is expected to be pre-existing, docs should say so and note the permission requirement.
This issue might be a duplicate of existing issues. Please check:
Agent writes tmp files to /tmp instead of /tmp/opencode #44644: Also concerns agents and the /tmp/opencode scratch directory — that issue reports agents writing to /tmp instead of /tmp/opencode, while this issue reports /tmp/opencode being owned by a different user. They are related and may benefit from a shared fix (e.g. ensuring the directory is created with the correct ownership/permissions by the session user or host tooling).
Reproduced on 2.0.28 (Linux x86_64, multi-user host where /tmp/opencode is drwxrwxr-x 2 ubuntu ubuntu and a second user runs as uid=2571).
Beyond the EACCES failure for the second user, there are a few multi-tenant security implications with how Global.Path.tmp and os.tmpdir() are currently handled on shared POSIX hosts:
Global.Path.tmp (/tmp/opencode) has no 0o700 mode or UID/symlink verification (packages/util/src/global.ts:68-74):
fs.promises.mkdir(service.tmp, { recursive: true }) uses the default umask (0755 / 0775), so any files written into /tmp/opencode by the owning user (agent scratch files, web/desktop attachment uploads at ${info.paths.tmp}/uploads/... in packages/app/src/composer/attachments/destination.ts, etc.) are world-readable (0644) to other local users.
Because mkdir(..., { recursive: true }) succeeds if the directory already exists without checking lstat ownership (uid === process.getuid()) or permissions, another local user can pre-create /tmp/opencode with 0777 (or as a symlink that fs.promises.realpath(service.tmp) resolves and injects into the system prompt via packages/core/src/instructions/builtins.ts:43), allowing cross-user reading/tampering of scratch files.
Note for fix(util): use a per-user tmp directory #52973 (opencode-<uid>): even with opencode-<uid> in os.tmpdir(), acquire() in packages/util/src/global.ts should create service.tmp with mode: 0o700 and verify via lstat that the directory is not a symlink and is owned by process.getuid(), since /tmp/opencode-<uid> is still a predictable name in world-writable /tmp.
Other direct os.tmpdir() writes bypass Global.Path.tmp without 0o600:
packages/tui/src/editor.ts:17-18: writes the user's external $EDITOR prompt buffer to path.join(os.tmpdir(), "${Date.now()}.md") with default umask (0644, world-readable) and a predictable millisecond filename without flag: "wx".
packages/core/src/plugin/vcs/git.ts:562-569: writes ${os.tmpdir()}/opencode-untracked-${randomUUID()}.{list,index} with default umask (0644) in world-listable /tmp.
One more shape of this problem beyond the cross-user permission failure: /tmp/opencode\ is also a single flat namespace shared by all of one user's concurrent sessions. Even single-user, two sessions writing scratch or harness-created directories (the /tmp/opencode/t\ pattern) collide in the same way — there is no per-session namespace under the temp root.
Workarounds that avoid /tmp\ entirely: set \worktree.directory\ (already in the config schema) to a home-relative parent so new worktrees never land in temp, and have agents keep per-session scratch under a home-level directory (e.g. ~/.opencode/tmp/). The multi-user permission failure itself can only be fixed by moving off shared /tmp, which is what this issue tracks.
Considering how this affects more than just OpenCode, I ended up writing a systemd service and shell hooks to set TMPDIR to /tmp/user/<uid> and OpenCode seems to be following that (similar to pam_tmpdir but doesn't need a SUID helper). I'm not sure why distros don't do this by default.
Thanks for the report. I reproduced this on @opencode/cli 2.2.0 (the beta build you reported isn't published on npm) and on the latest dev (a882cf9).
Steps, with two ordinary users on one Linux host:
alice runs OpenCode once (opencode debug paths is enough). That creates /tmp/opencode as drwxrwxr-x alice alice.
bob starts a session. His system prompt still says /tmp/opencode is "pre-created and approved for external access", but the agent's printf probe > /tmp/opencode/probe1.txt fails with Permission denied.
OpenCode places its temp directory at os.tmpdir()/opencode, a single path shared by every user on the host. It never checks who owns that directory, or whether the current user can write to it, before advertising it. The recording and screenshot below show the run, and the script below reproduces it.
recording.mp4
Reproduction script (drive.ts)
// Reproduces #44696: on a multi-user host, /tmp/opencode belongs to whichever user// ran OpenCode first, yet every other user's agent is told it is writable scratch.// Setup (two ordinary users, alice and bob):// rm -rf /tmp/opencode// su alice -c "opencode debug paths" # alice now owns /tmp/opencode (drwxrwxr-x)// mkdir -p /tmp/opencode-drive && chmod 1777 /tmp/opencode-drive// su bob -c "cd /workspace/repro && opencode-drive run drive.ts"// Expected per the banner: the probe write succeeds. Actual: Permission denied.import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"letbanner="(banner not found)"letprobed=falseexportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),tui: {recording: true},project: {git: true,files: {"README.md": "probe\n"}},},({ ui, llm })=>Effect.gen(function*(){yield*llm.serve((request)=>{consttext=JSON.stringify(request)constm=text.match(/Prefer[^"\\]*?overgenericsystemtemporarydirectories[^"\\]*/)if(m)banner=m[0]if(!m)returnStream.make(Llm.text("Probe"))if(!probed&&(probed=true))returnStream.make(Llm.toolCall({index: 0,id: "call_probe",name: "shell",input: {command: "id -un; ls -ld /tmp/opencode; printf probe > /tmp/opencode/probe1.txt",description: "Write probe to advertised tmp"},}),Llm.finish("tool-calls"),)returnStream.make(Llm.text(`BANNER SEEN: ${banner}`))})yield*ui.submit("Write a probe file into the advertised scratch directory")yield*Effect.sleep("8 seconds")yield*ui.waitFor("Permission denied",{timeout: 20000})yield*ui.waitFor("BANNER SEEN",{timeout: 20000})yield*ui.screenshot("permission-denied")console.log("BANNER:",banner)}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Additional data point on 2.2.6, plus a concrete mktemp fix.
The session runs as uid 1000. Another local user (uid 1001) owns /tmp/opencode:
$ stat -c '%u %a' /tmp/opencode
1001 755
$ mktemp /tmp/opencode/commit.XXXXXXXX
mktemp: failed to create file via template '/tmp/opencode/commit.XXXXXXXX': Permission denied
0755 blocks writes from every other user. If the owner pre-creates the path as 0777 or as a symlink, scratch files become readable and replaceable by that user. A predictable name in a world-writable directory cannot be treated as safe.
Suggested fix:
Stop pre-creating and advertising a fixed path under os.tmpdir().
Use a per-user base. On Linux, $XDG_RUNTIME_DIR is already private (0700) and typically cleared at logout. On macOS, $TMPDIR is already per-user. Fall back to os.tmpdir() only when neither exists.
Create one directory per server instance with mktemp -d. It picks a random name and creates the directory atomically with mode 0700:
Use the server port as the per-instance key. opencode serve already listens on an ephemeral loopback port, so the server can pass its own. Keep the random suffix because ports get reused.
If a stable path is required for external access, use $XDG_RUNTIME_DIR/opencode/<port>, create it with install -d -m 700, then lstat it on reuse. Reject symlinks and require uid === process.getuid() and (mode & 0o077) === 0.
On Windows, %TEMP% is already per-user. Use the platform equivalent of mktemp -d.
The opencode-<uid> proposal above still uses a predictable name. mktemp -d removes the prediction.
Do not use ~/.cache/opencode or ~/.opencode/tmp as the runtime scratch root. They persist across logout. ~/.opencode/tmp is fine for durable, user-owned files, but it still needs per-session subdirectories with 0700.
Activity
github-actions commentedon Aug 24, 2026
This issue might be a duplicate of existing issues. Please check:
/tmp/opencodescratch directory — that issue reports agents writing to/tmpinstead of/tmp/opencode, while this issue reports/tmp/opencodebeing owned by a different user. They are related and may benefit from a shared fix (e.g. ensuring the directory is created with the correct ownership/permissions by the session user or host tooling).Shra1V32 commentedon Oct 13, 2026
Reproduced on
2.0.28(Linux x86_64, multi-user host where/tmp/opencodeisdrwxrwxr-x 2 ubuntu ubuntuand a second user runs asuid=2571).Beyond the
EACCESfailure for the second user, there are a few multi-tenant security implications with howGlobal.Path.tmpandos.tmpdir()are currently handled on shared POSIX hosts:Global.Path.tmp(/tmp/opencode) has no0o700mode or UID/symlink verification (packages/util/src/global.ts:68-74):fs.promises.mkdir(service.tmp, { recursive: true })uses the default umask (0755/0775), so any files written into/tmp/opencodeby the owning user (agent scratch files, web/desktop attachment uploads at${info.paths.tmp}/uploads/...inpackages/app/src/composer/attachments/destination.ts, etc.) are world-readable (0644) to other local users.mkdir(..., { recursive: true })succeeds if the directory already exists without checkinglstatownership (uid === process.getuid()) or permissions, another local user can pre-create/tmp/opencodewith0777(or as a symlink thatfs.promises.realpath(service.tmp)resolves and injects into the system prompt viapackages/core/src/instructions/builtins.ts:43), allowing cross-user reading/tampering of scratch files.opencode-<uid>): even withopencode-<uid>inos.tmpdir(),acquire()inpackages/util/src/global.tsshould createservice.tmpwithmode: 0o700and verify vialstatthat the directory is not a symlink and is owned byprocess.getuid(), since/tmp/opencode-<uid>is still a predictable name in world-writable/tmp.Other direct
os.tmpdir()writes bypassGlobal.Path.tmpwithout0o600:packages/tui/src/editor.ts:17-18: writes the user's external$EDITORprompt buffer topath.join(os.tmpdir(), "${Date.now()}.md")with default umask (0644, world-readable) and a predictable millisecond filename withoutflag: "wx".packages/core/src/plugin/vcs/git.ts:562-569: writes${os.tmpdir()}/opencode-untracked-${randomUUID()}.{list,index}with default umask (0644) in world-listable/tmp.lyckantropen commentedon Oct 25, 2026
One more shape of this problem beyond the cross-user permission failure: /tmp/opencode\ is also a single flat namespace shared by all of one user's concurrent sessions. Even single-user, two sessions writing scratch or harness-created directories (the /tmp/opencode/t\ pattern) collide in the same way — there is no per-session namespace under the temp root.
Workarounds that avoid /tmp\ entirely: set \worktree.directory\ (already in the config schema) to a home-relative parent so new worktrees never land in temp, and have agents keep per-session scratch under a home-level directory (e.g. ~/.opencode/tmp/). The multi-user permission failure itself can only be fixed by moving off shared /tmp, which is what this issue tracks.
sudoer777 commentedon Oct 28, 2026
Considering how this affects more than just OpenCode, I ended up writing a systemd service and shell hooks to set TMPDIR to
/tmp/user/<uid>and OpenCode seems to be following that (similar to pam_tmpdir but doesn't need a SUID helper). I'm not sure why distros don't do this by default.opencodejr commentedon Oct 28, 2026
Thanks for the report. I reproduced this on
@opencode/cli2.2.0 (the beta build you reported isn't published on npm) and on the latestdev(a882cf9).Steps, with two ordinary users on one Linux host:
aliceruns OpenCode once (opencode debug pathsis enough). That creates/tmp/opencodeasdrwxrwxr-x alice alice.bobstarts a session. His system prompt still says/tmp/opencodeis "pre-created and approved for external access", but the agent'sprintf probe > /tmp/opencode/probe1.txtfails withPermission denied.OpenCode places its temp directory at
os.tmpdir()/opencode, a single path shared by every user on the host. It never checks who owns that directory, or whether the current user can write to it, before advertising it. The recording and screenshot below show the run, and the script below reproduces it.recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.denizzzka commentedon Nov 4, 2026
Maybe it’s better to place it to
/var/tmp/opencodeor~/.cache/opencode/?xenoterracide commentedon Nov 7, 2026
Additional data point on
2.2.6, plus a concretemktempfix.The session runs as uid 1000. Another local user (uid 1001) owns
/tmp/opencode:0755 blocks writes from every other user. If the owner pre-creates the path as 0777 or as a symlink, scratch files become readable and replaceable by that user. A predictable name in a world-writable directory cannot be treated as safe.
Suggested fix:
os.tmpdir().$XDG_RUNTIME_DIRis already private (0700) and typically cleared at logout. On macOS,$TMPDIRis already per-user. Fall back toos.tmpdir()only when neither exists.mktemp -d. It picks a random name and creates the directory atomically with mode 0700:opencode servealready listens on an ephemeral loopback port, so the server can pass its own. Keep the random suffix because ports get reused.$XDG_RUNTIME_DIR/opencode/<port>, create it withinstall -d -m 700, thenlstatit on reuse. Reject symlinks and requireuid === process.getuid()and(mode & 0o077) === 0.%TEMP%is already per-user. Use the platform equivalent ofmktemp -d.The
opencode-<uid>proposal above still uses a predictable name.mktemp -dremoves the prediction.Do not use
~/.cache/opencodeor~/.opencode/tmpas the runtime scratch root. They persist across logout.~/.opencode/tmpis fine for durable, user-owned files, but it still needs per-session subdirectories with 0700.Verified mode here:
This fixes both the cross-user failure and the cross-session collision.