tsx CLI fails in Codex sandbox with EPERM when creating IPC pipe

Open 💬 9 comments Opened Dec 19, 2025 by lastmjs
💡 Likely answer: A maintainer (davidgilbertson, contributor) responded on this thread — see the highlighted reply below.

Summary

In the Codex CLI sandbox, the tsx CLI fails when it attempts to create its IPC pipe. The intent of this issue is to ensure tsx can run normally inside the sandbox (or that the sandbox provides a compatible fallback/permission), because tsx is a common TypeScript runner.

Environment

  • Codex CLI sandbox (Linux; restricted IPC/socket permissions)
  • Node.js v22.18.0
  • tsx v4.21.0

Minimal reproducible example

mkdir -p /tmp/tsx-repro && cd /tmp/tsx-repro
npm init -y
npm i -D tsx typescript
mkdir -p src
cat > src/audit.ts <<'EOF'
console.log("ok")
EOF
npx tsx src/audit.ts

Actual result

tsx fails trying to create its IPC pipe:

Error: listen EPERM: operation not permitted /tmp/tsx-1000/204940.pipe
    at Server.setupListenHandle [as _listen2] (node:net:1918:21)
    at listenInCluster (node:net:1997:12)
    at Server.listen (node:net:2119:5)
    at .../node_modules/tsx/dist/cli.mjs:53:31537
    at createIpcServer (.../node_modules/tsx/dist/cli.mjs:53:31515)

Expected result

tsx should run normally inside the Codex sandbox (or degrade gracefully if IPC is disallowed).

Workaround

node --import tsx src/audit.ts works in the same environment.

Notes

It looks like tsx always tries to create a named pipe under /tmp/tsx-UID/*.pipe. The Codex sandbox disallows listen() on that path. A Codex-level allowance for local IPC sockets, or a compatibility mode/flag to disable IPC, would fix this.

View original on GitHub ↗

9 Comments

davidgilbertson contributor · 6 months ago

Not sure if this is related, but I get an EPERM error when trying to run tests. Running !npm test in a codex session works fine. But if I ask codex to run npm test that fails.

$ '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command '"'"'npm test'"'"
Loading profile: C:\Users\david\OneDrive\Documents\PowerShell\Microsoft.PowerShell_profile.ps1

> test
> npm --workspace server run test && npm --workspace client run test


> test
> vitest run

^[[31mfailed to load config from C:\Users\david\dev\trackio_viewer\server\vitest.config.ts^[[39m

^[[31m⎯⎯⎯⎯⎯⎯⎯^[[39m^[[1m^[[41m Startup Error ^[[49m^[[22m^[[31m⎯⎯⎯⎯⎯⎯⎯⎯^[[39m
Error: spawn EPERM
    at ChildProcess.spawn (node:internal/child_process:421:11)
    at Object.spawn (node:child_process:796:9)
    at ensureServiceIsRunning (C:\Users\david\dev\trackio_viewer\node_modules\esbuild\lib\main.js:1978:29)
    at build (C:\Users\david\dev\trackio_viewer\node_modules\esbuild\lib\main.js:1876:26)
    at bundleConfigFile (file:///C:/Users/david/dev/trackio_viewer/node_modules/vite/dist/node/chunks/config.js:35806:23)
    at bundleAndLoadConfigFile (file:///C:/Users/david/dev/trackio_viewer/node_modules/vite/dist/node/chunks/config.js:35793:24)
    at loadConfigFromFile (file:///C:/Users/david/dev/trackio_viewer/node_modules/vite/dist/node/chunks/config.js:35762:179)
    at resolveConfig (file:///C:/Users/david/dev/trackio_viewer/node_modules/vite/dist/node/chunks/config.js:35411:28)
    at _createServer (file:///C:/Users/david/dev/trackio_viewer/node_modules/vite/dist/node/chunks/config.js:25361:73)
    at createServer$2 (file:///C:/Users/david/dev/trackio_viewer/node_modules/vite/dist/node/chunks/config.js:25358:9) {
  errno: -4048,
  code: 'EPERM',
  syscall: 'spawn'
}

npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path C:\Users\david\dev\trackio_viewer\server
npm error workspace trackio-server
npm error location C:\Users\david\dev\trackio_viewer\server
npm error command failed
npm error command C:\WINDOWS\system32\cmd.exe /d /s /c vitest run

To reproduce this, in a repo with esbuild installed from npm, create a testing.js file:

const esbuild = require('esbuild');

const run = async () => {
  console.log('esbuild version', esbuild.version);
  await esbuild.build({
    bundle: true,
    format: 'cjs',
    platform: 'node',
    write: false,
    stdin: {
      contents: 'export const value = 1;',
      sourcefile: 'spawnTestInput.ts',
      loader: 'ts',
    },
  });
};

run().catch((err) => {
  console.error('esbuild error:', err);
  process.exitCode = 1;
});

In a codex session, !node ./testing.js should work, but if you ask codex to run it, it will fail.

davidgilbertson contributor · 6 months ago

I'm taking a look at this, the minimal code to reproduce the error:

const { spawn } = require("child_process");

spawn("where.exe", ["where"], {
  stdio: ["pipe", "inherit", "inherit"],
});

Codex won't be able to run that, it gets EPERM thanks to the anonymous pipe.

A similar call with Python is fine, so the issue isn't anonymous pipes _per se_, it's something about the _way_ that Node/libuv creates them, and how that interacts with the Windows sandbox setup (the non-elevated one).

davidgilbertson contributor · 6 months ago

I think this should be fixed by https://github.com/openai/codex/pull/8280, not released yet (as of 0.77.0)

etraut-openai contributor · 6 months ago

@davidgilbertson, the PR you mentioned is specific to Windows, so it won't have any effect on Linux.

davidgilbertson contributor · 6 months ago

Ah, of course, I forgot that OP's issue was linux.

I've since learnt that #8280 doesn't fix the issue I was having either. I'm sure this is known already, but in case it isn't (the code doesn't allude to it), the issue with pipes is as follows (particularly in the case I posted above):

  • Node will use libuv to create the pipe, and it creates a named pipe (as opposed to an anonymous pipe).
  • libuv then goes on to request WRITE_DAC permissions on the pipe (maybe worth raising this as an issue with them for the long term?).
  • Named pipes don't use the default DACL defined for the access token, they get a system-defined named-pipe DACL (if an SD isn't provided, which is the case with libuv).
  • That system-defined DACL won't grant the Codex sandbox's restricted/capability SID the required rights
  • Thus, an EPERM error

But the recent change to remember to run a command without the sandbox seems to work well. (Although I've noticed this is opaque to the agent, so if you ask it if it ran the command in a sandbox it will say yes.)

aorsten · 6 months ago

Are there any known workarounds for this issue, so I can enable my sandboxed codex to run my vitest tests without crashing in the EPERM issue?

amluto · 5 months ago

I suspect I'm hitting the same issue in a different context. Here's a fully standalone test case:

sandbox_unix_bind_test.c

It compiles, it runs just fine outside the sandbox, and it really ought to work inside the sandbox as well.

SRNissen · 4 months ago

I may have a related problem - I asked codex cli to introspect on why a script succeeds when I run it in the terminal, but fails when codex tui launches the same command.

Codex's report contains a lot of similar verbage - spawn and empty pipes.

1. The failure is not because the file is actually missing. dist/out/src/extension.js exists when vsce runs. 2. The failure is not a generic write/network/auth issue. touch works, tsc and prepare-dist.mjs run, and vsce login/logout is unrelated to this packaging stage. 3. The failure is specifically in vsce’s dependency-enabled packaging path. vsce package --no-dependencies succeeds, while plain vsce package fails with “entrypoint missing”. 4. Why that matters: plain vsce package internally runs npm dependency discovery (npm list --production --parseable ...) before collecting files. When that dependency list is empty, file collection collapses and entrypoint validation fails. 5. In this runtime, the same npm command behaves differently depending on how it is launched. From a direct shell: it prints the dist path. From Node child process (child_process.exec/spawn with piped stdio): it exits 0 with empty stdout/stderr. 6. It is broader than npm. Node-based CLIs launched as child processes with stdio: pipe also lose JS stream output (console.log, process.stdout.write), even though process exit code is 0. 7. Low-level probe result: in those child processes, process.stdout._handle and process.stderr._handle are false. process.stdout.write(...) returns true but nothing is emitted. fs.writeSync(1, ...) still emits, which is why some binaries still show output. 8. Causal chain I currently believe is correct: sandbox/exec runtime + child-process piped stdio behavior -> Node JS stream output dropped -> npm list output becomes empty -> vsce dependency discovery returns empty -> vsce sees no entrypoint file -> compile fails. 9. Confidence level: high on symptoms and chain; medium on exact root-cause implementation detail inside the sandbox/Node stdio plumbing. 10. Practical implication: your workaround (--no-dependencies) is valid and deterministic for this repo, but yes, I agree with your goal to restore full capability generally. The right fix target is runtime subprocess/stdout behavior, not this project’s TypeScript or packaging layout.

Where I would particularly hightlight this part:

5. In this runtime, the same npm command behaves differently depending on how it is launched. From a direct shell: it prints the dist path. From Node child process (child_process.exec/spawn with piped stdio): it exits 0 with empty stdout/stderr.

---

Chewed through a solid 40% of its context window to work the problem, but the report it got me is better researched and argued than many I've seen human engineers produce - it suggested I report the problem, and I found this issue when I was looking to see if somebody already had.

michaelwills · 4 months ago

Just a note that this is appears to be cross-platform as it is an issue for the Codex app on macOS as well.