Skip to content

[bug]: atomic config writes replace a symlinked opencode.json with a regular file #265

Description

@Moret84

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions