Repository navigation
Description
Summary
When the global config directory ($XDG_CONFIG_HOME/opencode, e.g. ~/.config/opencode) is a symlink to a real directory, the service fails to subscribe its directory watcher with inotify_add_watch ... failed: Not a directory. As a result, edits to the global opencode.json are never hot-reloaded; only opencode reload or a service restart picks them up. A real (non-symlinked) directory works.
Environment
- opencode version: 2.0.27
- OS: Linux 6.18.40.1-microsoft-standard-WSL2 x86_64 (WSL2, ext4)
- Terminal: TERM=xterm-256color, COLORTERM=truecolor
- Shell: /bin/bash
- Install/channel: latest
- Active plugins: not relevant; the isolated reproduction below uses an empty config with no plugins
Reproduction
Isolated, using temporary XDG directories:
R=/tmp/symrepro; mkdir -p $R/real/opencode $R/xdg $R/data $R/state $R/cache $R/proj
echo '{"agent":{"probe":{"description":"v1","prompt":"x"}}}' > $R/real/opencode/opencode.json
ln -s $R/real/opencode $R/xdg/opencode # trailing slash or not makes no difference
cd $R/proj
XDG_CONFIG_HOME=$R/xdg XDG_DATA_HOME=$R/data XDG_STATE_HOME=$R/state XDG_CACHE_HOME=$R/cache \
opencode serve --hostname 127.0.0.1 --port 47123 --print-logs- In another shell, request
GET /api/agent?directory=$R/proj(basic authopencode:<printed password>). Theprobeagent has descriptionv1. - Edit the config:
sed -i 's/v1/v2/' $R/xdg/opencode/opencode.jsonand wait several seconds. - Request
GET /api/agentagain.
Expected Behavior
The config directory watcher starts (following the symlink), and step 3 returns description v2, just as it does when $R/xdg/opencode is a real directory.
Actual Behavior
The directory watcher fails when it starts, and the edit is never picked up (step 3 still returns v1):
level=INFO message="watcher subscribe" path=/tmp/symrepro/xdg/opencode type=directory ignores=3 role=server
level=ERROR message="failed to subscribe" directory=/tmp/symrepro/xdg/opencode cause="Error: inotify_add_watch on '/tmp/symrepro/xdg/opencode' failed: Not a directory
at Config.reconcileWatches (/$bunfs/root/chunk-xnspstqv.js:118:33644)
at ServerProcess.start (/$bunfs/root/chunk-c1a0vz4v.js:2:7540)"
$XDG_CONFIG_HOME/opencode |
Directory watcher | Value after edit |
|---|---|---|
| symlink → real dir | failed to subscribe … Not a directory |
v1 (stale) |
| symlink with trailing slash in target | same failure | not separately tested |
| real directory | watcher started … backend=inotify |
v2 |
File watchers on paths inside the symlinked directory (AGENTS.md, skills, using backend=node) start normally. Only the type=directory inotify watch on the symlink itself fails. This suggests the inotify backend subscribes with IN_DONT_FOLLOW/IN_ONLYDIR on the unresolved path. Resolving the directory with realpath before subscribing (and mapping events back) would probably fix it. This is an inference; I have not verified it in the source.
Additional Context
- Happens consistently on every service start and on config reload, for the real
~/.config/opencodesymlink (a dotfiles-style setup) and in the isolated reproduction above. - Workaround: run
opencode reloadoropencode service restartafter editing global config. - A symlinked global config dir is a common dotfiles pattern and was usable with V1.
- Related but distinct: plugin loader: local plugin hot-reload never re-evaluates when config dir is a symlink (stale Bun module cache) #52240 (stale plugin module cache under a symlinked config dir) and plugin: a local plugin whose directory symlink is retargeted is silently dropped until restart #53763 (retargeted plugin directory symlink). This issue is about the config directory watch failing to subscribe at all.
Activity
opencode-agent commentedon Oct 12, 2026
Thanks for the clear reproduction. I can confirm this on 2.0.27 on Linux, using the same setup with temporary XDG directories and
$XDG_CONFIG_HOME/opencodeas a symlink to a real directory:type=directorywatcher for the config directory fails withinotify_add_watch on '…/xdg/opencode' failed: Not a directory.opencode.jsonthrough the symlink,GET /api/agentstill returns the oldv1description.backend=inotifyand the edit reloads tov2.It still happens on the latest
dev(1438073), run from source. The script below reproduces it (./repro.sh symlinkversus./repro.sh real).Open PR #54093 resolves symlinked directories before subscribing the watcher and maps the event paths back, which looks like it targets this exact failure. Related but separate: #53920 (a symlinked
opencode.jsonfile in a real directory), #52240 and #53763 (plugins under symlinks).Reproduction script (repro.sh)
Run it:
bash repro.shVeld101 commentedon Oct 17, 2026
Correcting my earlier comment on this issue.
I wrote that "nothing needs mapping back" and that a fix in
ConfigWatch.planalone was enough. That was wrong, and my PR built on that assumption (#56294) has been closed as a duplicate of #54093.Resolving the watched directory fixes only half of the problem. It makes the subscription start, but the events then arrive under the resolved path while
Configentries keep the spelling they were discovered with (sources.globalis the unresolved~/.config/opencode). Every domain subscriber filters withFSUtil.contains(path.join(entry.path, ...), update.path), so the two spellings never match:That is
agent.ts:137,command.ts:152andsource.ts:195inpackages/core/src/config/plugin/. A plan-only fix therefore turns "subscription refused" into "events silently dropped" — the same broken hot-reload with a quieter failure mode.The real fix resolves the directory before subscribing and rebases emitted event paths back onto the subscribed spelling, which is what #54093 does:
What still holds from my earlier comment: the failure is an outright refused subscription, not a silent one, and it reproduces on a real inotify backend:
So #54093 is the fix to track here, not the plan-level change I proposed.
donvargax commentedon Oct 23, 2026
Still happening on 2.1.1 (Linux). It isn't limited to
$XDG_CONFIG_HOME/opencodeeither: pointingOPENCODE_CONFIG_DIRat a symlinked directory hits the same failure.OPENCODE_CONFIG_DIRpoints toprobedescription after editfailed to subscribe … inotify_add_watch … failed: Not a directoryv1(stale)watcher started … type=directory backend=inotifyv2Reproduction, using temporary directories only:
Setting
OPENCODE_CONFIG_DIR=$R/realinstead makes the edit show up asv2.The config itself is read through the symlink without trouble. Only the watch refuses to follow it, so following the link here wouldn't expose anything that isn't already being read. Resolving the path before subscribing, as #54093 does in the shared watcher, looks like it would cover this entry point too.
Workaround until then: set
OPENCODE_CONFIG_DIR(orXDG_CONFIG_HOME) to the resolved real path.haineb commentedon Nov 5, 2026
Repro on v2.2.5.
Setup
~/.config/opencodeis a symlink to a real directory (dotfiles layout):~/.config/opencode -> <workspace>/opencode-v2/opencode.jsoncreachable through both the global and the project config pathBehavior
Every service start and every
opencode reloadfails to subscribe the global config directory, so config edits are never hot-reloaded:~/.local/share/opencode/log/opencode.log; most recent 2026-11-04T17:06:02Zwatcher subscribe ... type=filefor each plugin file), so the failure is specific to the directory-level inotify watch on the symlinked rootopencode.jsoncedit requires a manualopencode reload(or service restart) to take effect; the in-file watcher path is silently deadopencode reloadafter each editEnvironment
Linux 6.18.54.1-microsoft-standard-WSL2 x86_64/bin/bash,TERM=xterm-256colorNote
#56294 was closed and is CI-green but still unmerged - so the bug persists in the latest release. This may be a good place to look to resolve.