[BUG] Symlinked @import is refused as external but the approval dialog never appears
What's Wrong?
A project-scope @import whose path is lexically inside the working directory
but resolves outside it through a symlinked directory is refused at load time,
yet never triggers the external-import approval dialog. The approval it requires
therefore cannot be granted, and the import stays silently unloadable.
The docs say:
An import in a project-level memory file is external when its path resolves outside your working directory [...] The first time Claude Code encounters external imports in a project, it shows an approval dialog listing the files.
That warning sits in the memory page's worktree passage, which recommends
sharing personal instructions across worktrees by importing from the home
directory:
If you work across multiple git worktrees of the same repository, a gitignored CLAUDE.local.md only exists in the worktree where you created it. To share personal instructions across worktrees, import a file from your home directory instead.
The case below is that same pattern reached through a symlink rather than a
home-directory path, and it is the variant that cannot ask.
The refusal treats the import as external. The prompt does not. Nothing else
reports the discrepancy — no warning, no log line, and /context lists the
importing file as loaded, so the only visible symptom is that content the file
imports is absent from the session.
This is reachable in an ordinary layout. A repository shares one directory across
its worktrees by symlinking it, and every worktree carries the sameCLAUDE.local.md importing a file from it:
trunk/data/shared real directory @data/shared/corpus.md -> internal, loads, never prompts
wt/data/shared -> ../../trunk/data/shared @data/shared/corpus.md -> external, never loads, never prompts
The linked worktree's import is external but unrecognised as such, which is why
no dialog is ever offered for it. The approval is required and unreachable, in
every worktree at once, with no indication anything is wrong.
Adding a second import to a linked worktree, with a plain absolute path and no
symlink in it, makes the dialog appear immediately. Accepting it loads both
imports — including the symlinked one that could never prompt on its own. So the
symlinked import is a legitimate external include; it simply cannot ask.
What Should Happen?
Either behaviour would be fine on its own: prompt for the import, or don't treat
it as external. The defect is that the two disagree — it is refused as external
and cannot ask to be approved — which leaves a state no user action can resolve.
Resolving symlinks in the externality check is one way to close it; making the
refusal match whatever the prompt recognises is another.
Error Messages/Logs
No error, no warning, no dialog. The failure is entirely silent.
Steps to Reproduce
- In a git repository that already carries at least one commit. Leave both
files below untracked — if data/shared/corpus.md is committed, the
git worktree add in step 3 recreates that directory in the new worktree,
the ln -s then lands inside it as data/shared/shared, and the repro
silently tests nothing.
``bash``
mkdir -p data/shared
echo '# marker: imported content loaded' > data/shared/corpus.md
printf '@data/shared/corpus.md\n' > CLAUDE.local.md
- Start a session in the main worktree, run
/context.data/shared/corpus.md
is listed under Memory files. No dialog appears — correct, the path is
internal.
- Add a linked worktree reaching the same directory by symlink:
``bash``
git worktree add ../wt
cp CLAUDE.local.md ../wt/CLAUDE.local.md
mkdir -p ../wt/data
ln -s "../../$(basename "$PWD")/data/shared" ../wt/data/shared
The link is relative on purpose — that is the shape a repository sharing one
directory across several worktrees by symlink has. What matters is only that
it resolves outside the worktree.
- Start a session in
../wt, run/context.CLAUDE.local.mdis listed;
corpus.md is not. No dialog appears, on this or any later session.
- Add a second import with no symlink in its path:
``bash``
echo '# marker 2' > ~/probe.md
printf '@data/shared/corpus.md\n@%s/probe.md\n' "$HOME" > ../wt/CLAUDE.local.md
Start a session in ../wt. The dialog appears on first sight. Accept it, and
/context now lists both probe.md and corpus.md — including the
symlinked import that could never prompt for itself.
The approval is not tied to the probe. Add a second linked worktree that has
never had one and has had nothing done to it, and its symlinked import loads
from the same approval.
An InstructionsLoaded hook makes the load half of steps 2–5 observable
without /context, and works in a headless -p run because the events fire
before the login check. A followed import emits"load_reason":"include" with parent_file_path set to the importing file; a
suppressed one emits nothing at all, so the importing CLAUDE.local.md appears
alone. The dialog half is not observable this way — a -p run cannot render
one.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.241
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
VS Code integrated terminal
Additional Information
Ubuntu 24.04.4 LTS, kernel 6.8.0-137, git 2.43.0. Found on a repository with four
linked worktrees, all sharing one directory by symlink. The main worktree loads
the import and the linked ones do not, which is what hid it.
The load asymmetry reproduces identically on 2.1.241 and on 2.1.243, checked
with both versions installed and run against the same throwaway repository, so
it is not specific to one build. 2.1.243 has since been withdrawn; 2.1.241 is
the version reported above.
Two things a reader may otherwise try, both dead ends here:
- Setting
hasClaudeMdExternalIncludesApproved: trueby hand in~/.claude.json
has no effect, including when set on every ancestor path. The reason is the
project-state resolution described below, not a flag bug: the flag works when
it is written to the entry that is actually read, which under worktrees is not
the worktree's own path. That is why #76940's reporter sees it working on
Desktop and it does nothing here.
- The dialog is not being declined and re-suppressed. It does not appear at all,
across repeated sessions in a linked worktree.
Why hand-setting the flag has no effect: per-project state in ~/.claude.json
resolves to the main worktree, so an entry added for a linked worktree's own
path is never read. That behaviour is undocumented and is filed separately as
#89460.
Related, not duplicates:
- #76940 — Desktop silently ignores external includes and never prompts. Same
family. That reporter states the CLI does prompt, which is true here for a
non-symlinked path and false for a symlinked one.
- #43709 — external
@includesin the global~/.claude/CLAUDE.mdnever
prompting for approval. Nearest neighbour: same symptom, different cause — no
symlink and no worktree, the path is external outright. Closed for inactivity
and locked.
- #34437 — worktrees not sharing the main repo's project directory. Adjacent:
~/.claude/projects/ splits per worktree while ~/.claude.json does not.