OpenCode version tested
1.18.29
opencode-quota version tested
4.4.1
Bug summary
writeTextAtomic() in src/lib/atomic-json.ts finishes with rename(tmp, path). rename(2) replaces the path itself instead of following it, so when the destination is a symlink the link is destroyed and replaced by a regular file.
This matters for opencode.json, because linking it into a dotfiles repository is a common setup. After any config write from this plugin (init-installer.ts uses writeJsonAtomic on the resolved config path), the link is gone: the file under ~/.config/opencode/ becomes a real file, the dotfiles copy stops receiving updates, and nothing reports it. On my machine this went unnoticed for five weeks; I only found it by comparing inode birth times.
The retryable-rename branch is worse, since it calls safeRm(path) before renaming, unlinking the destination outright.
Note that temp-file-plus-rename is the right pattern here. The defect is only the missing symlink resolution before the rename.
Steps to reproduce
mkdir -p /tmp/repro/dotfiles
echo '{"plugin":[]}' > /tmp/repro/dotfiles/opencode.json
ln -s /tmp/repro/dotfiles/opencode.json /tmp/repro/opencode.json
node --input-type=module -e "
import { writeJsonAtomic } from '<path-to>/@slkiser/opencode-quota/dist/lib/atomic-json.js';
await writeJsonAtomic('/tmp/repro/opencode.json', { plugin: ['x'] }, { trailingNewline: true });
"
ls -l /tmp/repro/opencode.json
cat /tmp/repro/dotfiles/opencode.json
Expected behavior
The write follows the symlink: /tmp/repro/opencode.json is still a symlink, and /tmp/repro/dotfiles/opencode.json contains {"plugin":["x"]}.
Actual behavior
/tmp/repro/opencode.json is now a regular file containing the new content, and /tmp/repro/dotfiles/opencode.json still contains {"plugin":[]}.
Relevant logs/output
avant: opencode.json -> /tmp/repro/dotfiles/opencode.json
apres: -rw-r--r-- 1 user wheel 30 opencode.json
contenu dotfiles: {"plugin":[]}
est un symlink ? False
Reproduced against the published 4.4.1 artifact resolved from the OpenCode plugin cache, on macOS (APFS), Node 24.18.1.
Suggested fix
Resolve the destination through its symlink chain before choosing the temp path and renaming. The temp file then lives in the real target's directory, so the write stays atomic on the same filesystem. The repository already resolves links elsewhere (src/lib/scoped-update.ts).
I have a PR ready with real-filesystem regression tests.
OpenCode version tested
1.18.29
opencode-quota version tested
4.4.1
Bug summary
writeTextAtomic()insrc/lib/atomic-json.tsfinishes withrename(tmp, path).rename(2)replaces the path itself instead of following it, so when the destination is a symlink the link is destroyed and replaced by a regular file.This matters for
opencode.json, because linking it into a dotfiles repository is a common setup. After any config write from this plugin (init-installer.tsuseswriteJsonAtomicon the resolved config path), the link is gone: the file under~/.config/opencode/becomes a real file, the dotfiles copy stops receiving updates, and nothing reports it. On my machine this went unnoticed for five weeks; I only found it by comparing inode birth times.The retryable-rename branch is worse, since it calls
safeRm(path)before renaming, unlinking the destination outright.Note that temp-file-plus-rename is the right pattern here. The defect is only the missing symlink resolution before the rename.
Steps to reproduce
Expected behavior
The write follows the symlink:
/tmp/repro/opencode.jsonis still a symlink, and/tmp/repro/dotfiles/opencode.jsoncontains{"plugin":["x"]}.Actual behavior
/tmp/repro/opencode.jsonis now a regular file containing the new content, and/tmp/repro/dotfiles/opencode.jsonstill contains{"plugin":[]}.Relevant logs/output
Reproduced against the published 4.4.1 artifact resolved from the OpenCode plugin cache, on macOS (APFS), Node 24.18.1.
Suggested fix
Resolve the destination through its symlink chain before choosing the temp path and renaming. The temp file then lives in the real target's directory, so the write stays atomic on the same filesystem. The repository already resolves links elsewhere (
src/lib/scoped-update.ts).I have a PR ready with real-filesystem regression tests.