Description
readFileStringSafe does not catch EISDIR — TUI crashes on every launch with Unexpected server error (exit 133 / SIGTRAP)
Environment
| Field |
Value |
| opencode version |
1.17.3, 1.17.4 (both confirmed) |
| Install method |
Homebrew / npm global |
| OS |
macOS 15.7.7 (24G720) — Intel x86_64 |
| Shell |
fish |
| Terminal |
Ghostty inside tmux |
Summary
The TUI fails immediately on every launch with:
Error: Unexpected server error. Check server logs for details.
at <anonymous> (/$bunfs/root/chunk-e6x2d5p2.js:8:7615)
at processTicksAndRejections (native:7:39)
The process exits with code 133 (SIGTRAP). The root cause is a one-line regression in packages/core/src/fs-util.ts: readFileStringSafe catches "NotFound" (ENOENT) but not "BadResource" (EISDIR). During instance bootstrap, opencode probes the config directory path itself as a legacy bare config file. That path is always a directory, so the read returns EISDIR, which goes uncaught, producing an unhandled promise rejection that Bun terminates with SIGTRAP.
Steps to reproduce
# Fresh install, no prior config
brew install opencode # or npm install -g opencode-ai
opencode
# → crashes immediately
Also reproducible by running opencode debug config:
Error: Unexpected error
BadResource: FileSystem.readFile (/Users/<you>/.config/opencode)
Expected behaviour
The TUI launches.
Actual behaviour
The process exits 133 / SIGTRAP on every launch. No amount of cleanup fixes it: the server recreates ~/.config/opencode/ as a directory at startup (via mkdir -p), so the failing probe always hits a directory and always throws.
Root cause
Regression introduced by commit 13ac849db ("refactor(config+core): drop ConfigPaths.readFile, add AppFileSystem.readFileStringSafe, flatten TuiConfig.loadState").
The old ConfigPaths.readFile caught all non-ENOENT errors and silently skipped them. The replacement, readFileStringSafe in packages/core/src/fs-util.ts, only catches "NotFound" (ENOENT):
// packages/core/src/fs-util.ts (current — broken)
const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path) {
return yield* fs
.readFileString(path)
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
})
The platform error code mapping (same file) is:
case "EISDIR": return "BadResource"; // ← not caught
case "ENOENT": return "NotFound"; // ← caught
During instance bootstrap (Config.loadInstanceState → InstanceBootstrap → InstanceStore.boot → Server.listen), Config.get probes four paths for the global config. The fourth is the bare XDG path - $XDG_CONFIG_HOME/opencode (~/.config/opencode) — kept for backwards compatibility with the pre-directory config format. That path is a directory (the server creates it at startup for its own config files), so readFileString returns EISDIR → "BadResource" → not caught → unhandled rejection → Bun SIGTRAP.
Server log confirming this (visible with opencode serve --print-logs --log-level DEBUG):
message=loading path=/Users/<you>/.config/opencode/config.json ← ok (ENOENT, caught)
message=loading path=/Users/<you>/.config/opencode/opencode.json ← ok (ENOENT, caught)
message=loading path=/Users/<you>/.config/opencode/opencode.jsonc ← ok (ENOENT, caught)
message=loading path=/Users/<you>/.config/opencode ← EISDIR, NOT caught
message=failed error="PlatformError: BadResource: FileSystem.readFile (/Users/<you>/.config/opencode) (cause: Error: EISDIR: illegal operation on a directory, read)"
Full stack from the server log:
at Config.loadInstanceState (chunk-s77sw9gv.js:5:5063)
at Config.state
at Config.get
at InstanceBootstrap
at InstanceStore.boot
at InstanceStore.load
at Server.listen
Fix
One-line change in packages/core/src/fs-util.ts - add "BadResource" to the caught reasons:
const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path) {
return yield* fs
.readFileString(path)
- .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
+ .pipe(
+ Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)),
+ Effect.catchReason("PlatformError", "BadResource", () => Effect.succeed(undefined)),
+ )
})
"BadResource" covers both EISDIR and ENOTDIR, which is correct for a function named readFileStringSafe - if the path cannot be read as a file for any reason, returning undefined is the right behaviour.
Workaround (until fixed)
None that survive a restart. The server always recreates ~/.config/opencode/ as a directory at startup, so deleting the directory does not help.
Plugins
None
OpenCode version
1.17.4
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
macOS 15.7.7 (24G720) - Intel x86_64
Terminal
Ghostty
Description
readFileStringSafedoes not catchEISDIR— TUI crashes on every launch withUnexpected server error(exit 133 / SIGTRAP)Environment
Summary
The TUI fails immediately on every launch with:
The process exits with code 133 (SIGTRAP). The root cause is a one-line regression in
packages/core/src/fs-util.ts:readFileStringSafecatches"NotFound"(ENOENT) but not"BadResource"(EISDIR). During instance bootstrap, opencode probes the config directory path itself as a legacy bare config file. That path is always a directory, so the read returns EISDIR, which goes uncaught, producing an unhandled promise rejection that Bun terminates with SIGTRAP.Steps to reproduce
Also reproducible by running
opencode debug config:Expected behaviour
The TUI launches.
Actual behaviour
The process exits 133 / SIGTRAP on every launch. No amount of cleanup fixes it: the server recreates
~/.config/opencode/as a directory at startup (viamkdir -p), so the failing probe always hits a directory and always throws.Root cause
Regression introduced by commit
13ac849db("refactor(config+core): drop ConfigPaths.readFile, add AppFileSystem.readFileStringSafe, flatten TuiConfig.loadState").The old
ConfigPaths.readFilecaught all non-ENOENT errors and silently skipped them. The replacement,readFileStringSafeinpackages/core/src/fs-util.ts, only catches"NotFound"(ENOENT):The platform error code mapping (same file) is:
During instance bootstrap (
Config.loadInstanceState→InstanceBootstrap→InstanceStore.boot→Server.listen),Config.getprobes four paths for the global config. The fourth is the bare XDG path -$XDG_CONFIG_HOME/opencode(~/.config/opencode) — kept for backwards compatibility with the pre-directory config format. That path is a directory (the server creates it at startup for its own config files), soreadFileStringreturns EISDIR →"BadResource"→ not caught → unhandled rejection → Bun SIGTRAP.Server log confirming this (visible with
opencode serve --print-logs --log-level DEBUG):Full stack from the server log:
Fix
One-line change in
packages/core/src/fs-util.ts- add"BadResource"to the caught reasons:const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path) { return yield* fs .readFileString(path) - .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))) + .pipe( + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)), + Effect.catchReason("PlatformError", "BadResource", () => Effect.succeed(undefined)), + ) })"BadResource"covers both EISDIR and ENOTDIR, which is correct for a function namedreadFileStringSafe- if the path cannot be read as a file for any reason, returningundefinedis the right behaviour.Workaround (until fixed)
None that survive a restart. The server always recreates
~/.config/opencode/as a directory at startup, so deleting the directory does not help.Plugins
None
OpenCode version
1.17.4
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
macOS 15.7.7 (24G720) - Intel x86_64
Terminal
Ghostty