Symlinked files in .claude/rules/ are not auto-loaded (contradicts docs)

Status Open
Reported on v2.1.238
Maintainer reply None cached
Activity 7 comments · opened Aug 20, 2026

Version

Claude Code 2.1.238 (claude --version).

Docs claim (code.claude.com/docs/en/memory, quoted verbatim)

"The .claude/rules/ directory supports symlinks, so you can maintain a shared set of rules and link them into multiple projects. Symlinks are resolved and loaded normally."

Observed behavior

A symlink inside .claude/rules/ pointing to a file outside the project directory is not
auto-loaded into context at session start. A plain (non-symlinked) file in the same location loads
correctly. Verified in both a genuine interactive session (claude --dangerously-skip-permissions)
and headless print mode (claude -p), with a neutral, non-leading prompt in both cases.

Repro steps

mkdir -p sandbox/.claude/rules
mkdir -p canonical-source
echo -e '# canonical\nMARKER: ALPHA' > canonical-source/rule.md
echo -e '# sandbox CLAUDE.md\nMARKER: BETA' > sandbox/CLAUDE.md
ln -s "$(pwd)/canonical-source/rule.md" sandbox/.claude/rules/rule.md
cd sandbox
claude -p "List every instruction/rules file you can see loaded, with exact paths and content."
# Expected per docs: both CLAUDE.md and the symlinked rule.md appear as loaded context.
# Observed: only CLAUDE.md is auto-loaded. The symlinked file is not mentioned unless the
# model is separately asked to read the filesystem, at which point it finds the file exists,
# the symlink resolves correctly (`readlink` works), and its content is exactly what was written.

Control

Replacing the symlink with a plain, non-symlinked file of identical content in the same
.claude/rules/ location loads correctly (auto-injected, same priority tier as CLAUDE.md,
matches the docs' "Rules without paths frontmatter are loaded at launch with the same priority as
.claude/CLAUDE.md"). This isolates the gap to symlinks specifically, not .claude/rules/
loading in general.

Why this matters

The docs present cross-project rule symlinks as the recommended way to share instructions across
multiple projects without the token cost of @path imports (which load the imported file's full
content into context regardless). If symlinks silently don't load, a team relying on this pattern
would have zero indication their shared rules aren't reaching any project — the symlink resolves
fine at the OS level (readlink/ls -la show it correctly), so nothing looks broken from the
filesystem side.

Context this was found in

Investigating whether .claude/rules/ symlinks could de-duplicate boilerplate instructions shared
across multiple CLAUDE.md files in a multi-project setup, using symlinks as the proposed mechanism
per the docs. Found via a deliberate empirical precedence-conflict test in a throwaway sandbox
before relying on the mechanism anywhere real — not found by accident.

View original on GitHub ↗

7 Comments

taylorthurlow · 9 days ago

I'm experiencing the same issue. This worked previously, some point in the last few weeks I believe is when it stopped.

Max-Schubert · 9 days ago

Same for me on version 2.1.237
It seems the restriction to the same directory also applies for these symlinks as well which would definitely be an issue in the docs:
https://code.claude.com/docs/en/memory#share-rules-across-projects-with-symlinks

<img width="975" height="449" alt="Image" src="https://github.com/user-attachments/assets/43fd43c6-fef4-4d30-9d87-23cb01fea235" />

Seledrex · 5 days ago
"The .claude/rules/ directory supports symlinks, so you can maintain a shared set of rules and link them into multiple projects. Symlinks are resolved and loaded normally."

Our organization followed the recommend approach in the docs stated above to share common rules across many repos. This is a major bug for us, and we would love to see it resolved as soon as possible 🙏

nicholasshirley · 4 days ago

This just bit me. Rules have been silently being ignore for 2 weeks. 2.1.223 is the last working version that I have locally that behaves as it's described to.

Investigation by Claude:

Bisected further and read the loader in the 2.1.246 bundle — here's the mechanism.

Still broken on 2.1.243, 2.1.245 and 2.1.246 (same test as above, headless: a random token planted in
the rule file that the model can only print if the file was loaded). On 2.1.246 specifically:

  • Dropped: .claude/rules itself a symlink to outside the project; a symlinked subdirectory inside a

real .claude/rules; a symlinked file inside a real .claude/rules.

  • Still loaded: a real .claude/rules (control); symlinks under ~/.claude/rules (user scope); and —

inconsistently — a whole-directory .claude -> <outside> symlink whose contents are real files.

Mechanism:

  • The project-scope rules walk runs with includeExternal = the project's

hasClaudeMdExternalIncludesApproved flag from ~/.claude.json — the same flag the
external-@-import approval dialog sets.

  • With it false, a rules directory that lstats as a symlink and resolves outside the working directory

returns nothing, and a symlinked entry inside a real rules directory that resolves outside is skipped.
Nothing is logged or shown.

  • The user-scope walk (~/.claude/rules) passes includeExternal: true unconditionally, which is why

those symlinks keep working.

  • The approval dialog is only raised when a CLAUDE.md @-import resolves outside the project. The rules

walk never raises it, so a rules symlink has no route to becoming approved — except as a side effect of
also having an external @-import (reading the code, approval would unblock the rules walk too; not
verified empirically).

  • The whole-directory .claude symlink slips through because the check lstats only the rules entry,

not its parents.

There is no CHANGELOG entry for this between 2.1.223 (last known good here) and 2.1.237 (first reported
bad above); the only rules-symlink line anywhere is 2.1.239's claudeMdExcludes fix.

If the external gate is intentional — a committed symlink in a cloned repo could pull files from outside
the checkout into context — then what would make it livable is: route rules symlinks through the same
approval dialog as @-imports, print a startup notice whenever a rules entry is skipped, and update the
docs. Otherwise, restoring the documented behavior for project-scope symlinks.

ARivottiC · 3 days ago

The problem was introduced in 2.1.232.

I detected it with an InstructionsLoaded hook that appends one line per instruction file the harness loads.

Each release is installed by version:

claude install 2.1.236

The same prompt runs on every release, in a fresh session:

read @.notes/other.md. Don't do nothing about it. Now check the plugin session
dir for instructions-loaded.log and print the content in context. Then check
/project/rules/rule.md and update @.notes/doc.md with your findings

When it is not working, the log holds only the session-start line:

2026-08-27T07:38:53Z	session_start	User	/linked/claude.md		-

I tested from 2.1.236 downwards and it is working on 2.1.231:

2026-08-27T08:11:39Z	session_start	User	/linked/claude.md		-
2026-08-27T08:11:39Z	session_start	Project	/linked/rule1.md		-
2026-08-27T08:11:39Z	session_start	Project	/linked/rule2.md		-
2026-08-27T08:11:39Z	session_start	Project	/linked/rule3.md		-
2026-08-27T08:11:40Z	path_glob_match	Project	/linked/rule.md	.notes	/project/.notes/doc.md
lcristin · 2 days ago

Confirming @nicholasshirley mechanism and @ARivottiC bisection independently, plus an additional data point:

Windows repro: same behavior reproduced on Windows: intra-repo symlink loads, cross-repo symlink silently skipped. Not just a Linux/macOS issue.

Workaround, confirmed working: triggering the external-import approval dialog on the target project flips hasClaudeMdExternalIncludesApproved to true for that project entry in ~/.claude.json, which immediately unlocks the previously-skipped rule symlinks. No other change needed. Concretely: add a temporary @/absolute/path/outside/project.md import to a project-scope memory file, open a session, accept the approval dialog, remove the line.

This confirms the fix target precisely: whatever changed in 2.1.232, decoupling includeExternal for the project-scope rules walk from hasClaudeMdExternalIncludesApproved (or making the approval dialog actually fire for rules, instead of silently skipping) resolves it without touching the user-scope path, which is unaffected.

ARivottiC · 1 day ago

The hasClaudeMdExternalIncludesApproved workaround does not cover path-scoped rules.

I applied it as @lcristin describes: a @/absolute/path/outside/project.md import in the project CLAUDE.md, approval dialog accepted. Every rule in my .claude/rules/ is a symlink to a shared directory outside the project, and the approval did take effect — the rules that carry no paths: frontmatter now load again, where before none of them did.

The rules with paths: frontmatter still never load. Same hook and same prompt as my earlier comment, on 2.1.251, with the approval in place:

2026-08-29T04:13:03Z	session_start	Project	/linked/rule1.md		-
2026-08-29T04:13:03Z	session_start	User	/linked/claude.md		-
2026-08-29T04:13:03Z	session_start	Project	/linked/rule2.md		-
2026-08-29T04:13:03Z	include	Project	/linked/rule3.md	.other	-
2026-08-29T04:13:03Z	session_start	Project	/project/.claude/CLAUDE.md		-
2026-08-29T04:13:03Z	session_start	Project	/linked/rule4.md		-

/linked/rule.md is scoped paths: [".notes/**"]. The prompt reads two files under .notes/ and no path_glob_match line appears for either of them.

/linked/rule3.md is path-scoped too, to .other. It is in the session only because it is the file the CLAUDE.md @-import points at, and it loads without anything under .other being read. So an approved external @-import loads a path-scoped rule unconditionally, while the glob walk that should load one on demand still fires nothing.

Without the workaround, the link problem is still not fixed in 2.1.251.