Summary
&> /dev/stdout, &> /dev/stderr, &>> /dev/stdout and &>> /dev/stderr write both streams to a regular file at that path instead of to the stream. #562 made /dev/null a device for every operator. > and 2> already treat /dev/stdout and /dev/stderr as streams, but the &>/&>> case in applyRedirections handles only /dev/null and sends everything else to a file sink.
The symptoms depend on the filesystem:
- Default
InMemoryFs: the output disappears. It lands in a regular file named/dev/stdoutor/dev/stderr, and later&>>redirects keep appending to it. - A filesystem that
initFilesystemcan't seed (async-only, no/dev):bash.exec()rejects withENOENT: no such file or directory, '/dev'. That is the same failure #558 fixed for/dev/null.
Repro (main at 3ebd1c2, after #561 and #562)
The async-only filesystem is InMemoryFs behind a proxy that hides the *Sync methods, so /dev is never created, and whose writeFile/appendFile throw ENOENT when the parent directory is missing, like a disk- or database-backed IFileSystem.
| Script | default InMemoryFs | async-only fs | bash 5.1 |
|---|---|---|---|
{ echo out; echo err >&2; } &> /dev/stdout; echo after rc=$? | stdout after rc=0, out/err lost | exec() rejects: ENOENT ... '/dev' | stdout out err after rc=0 |
{ echo out; echo err >&2; } &> /dev/stderr; echo after rc=$? | stdout after rc=0, stderr empty | rejects | stdout after rc=0, stderr out err |
ls /nope &>> /dev/stdout; echo after rc=$? | stdout after rc=2, ls error lost | rejects | stdout ls: ... No such file or directory after rc=2 |
echo g &>> /dev/stderr; echo after rc=$? | stdout after rc=0, stderr empty | rejects | stdout after rc=0, stderr g |
echo x > /dev/stdout; echo after rc=$? | x after rc=0 | x after rc=0 | same |
echo x &> /dev/null; echo after rc=$? | after rc=0 | after rc=0 | same |
The last two rows are controls: > to a stream and &> to /dev/null already work.
Where the output goes on the default filesystem:
const b = new Bash();
await b.exec("echo one &> /dev/stdout; echo two &>> /dev/stderr");
const r = await b.exec("cat /dev/stdout; echo ---; cat /dev/stderr; echo ---; ls -l /dev");
// one
// ---
// two
// ---
// -rw-r--r-- 1 user user 4 ... stderr
// -rw-r--r-- 1 user user 4 ... stdoutWhere
packages/just-bash/src/interpreter/redirections.ts at 3ebd1c2:
- L1233–L1257: for
>,>|and>>,/dev/stdoutand/dev/stderrre-point the fd to the other fd's current sink, or do nothing when the target is the fd's own stream. - L1303–L1314: the
&>/&>>case handlesfilePath === DEV_NULLand builds{ kind: "file", path: filePath }for everything else, including the two stream paths.
Suggested fix
Mirror the > handling in the &>/&>> case:
/dev/stdout: both fds take fd 1's current sink (fd2Sink = fd1Sink)./dev/stderr: both fds take fd 2's current sink (fd1Sink = fd2Sink)./dev/null: discard, as now.- anything else: a file sink, as now.
We carry exactly this as a local patch over 3.6.0 (a two-line change to that case), with tests for &> and &>> to both streams, the exit status, no filesystem writes for the device paths, and an unchanged regular-file &>.
Impact for us
We serve just-bash sandboxes to AI agents over HTTP on top of a Postgres-backed IFileSystem that is async-only. cmd &> /dev/stderr shows up in agent-written scripts and in scripts from the repos they clone. Without the patch, each one fails the whole request and rolls back the script's file writes. With #562 merged, these are the only stream redirects we still have to patch.
Activity
0No activity yet.