[BUG] Path-based rules not applied when file is accessed via a symlinked path
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When a file is accessed via a symlinked path, Claude does not load path-based rules from .claude/rules/ whose globs match the file — even though the symlink resolves to a file inside the project directory. The rule loads correctly when the same file is referenced by its canonical path. There are no error messages; the rule is silently skipped.
What Should Happen?
Claude should resolve symlinks before matching file paths against rule globs, so that path-based rules apply consistently whether the file is accessed via its canonical path or a symlink that resolves to the same file within the project.
Error Messages/Logs
No error output. The failure is silent — the rule is simply not loaded and the output line `Loaded .claude/rules/<rule>.md` does not appear.
Steps to Reproduce
- Start Claude Code in a project directory, e.g.
~/dev/myproject - Create a path-based rule at
.claude/rules/markdown.mdwith frontmatter glob**/*.mdthat instructs Claude to prepend a specific marker comment to any markdown file it writes:
```markdown
---
description: Rules for markdown files
paths:
- "**/*.md"
---
When writing or editing any markdown file, always include the following comment at the top of the file as an HTML comment:
<!-- RULE LOADED: markdown.md path-based rule was applied -->
```
- Create a symlink pointing to the project directory:
sudo ln -s ~/dev/myproject /opt/myproject - Ask Claude to write to a markdown file using the symlinked path:
write a test line to /opt/myproject/test.md - Observe: rule is not loaded — the marker comment is absent and
Loaded .claude/rules/markdown.mddoes not appear in the output - Ask Claude to write to the same file using the canonical path:
write a test line to ~/dev/myproject/test.md - Observe: rule is loaded — the marker comment is present and
Loaded .claude/rules/markdown.mdappears in the output
Key distinction: the file itself is inside the project directory in both cases. Claude Code is started from the canonical project path. The only difference is whether the path argument given to Claude is routed through a symlink.
Example:
Without symlink path (~/dev/symlink/test.md) — rule loads
❯ write a test line to the test file ~/dev/symlink/test.md
⏺ I'll read the file first, then add a test line.
⎿ Loaded .claude/rules/markdown.md
⏺ Update(test.md)
⎿ Added 3 lines, removed 1 line
+ <!-- This comment confirms that the path-based rule was successfully loaded and applied. -->
+ This is a test line written by Claude Code.
+ This is another test line written by Claude Code.
With symlink path (/opt/symlink/test.md) — rule does not load
❯ write a test line to the test file /opt/symlink/test.md
⏺ Let me read the file first before editing it.
(no "Loaded .claude/rules/markdown.md" line)
⏺ Update(/opt/symlink/test.md)
⎿ Added 1 line
+ This is a third test line written by Claude Code.
(no marker comment)
Claude Model
Sonnet (default)
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.191
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
Related issues
- #44456 — CLI discards logical symlink path, uses realpath
- #39594 — CLI resolves symlinks in working directory instead of preserving $PWD logical path
- #17732 — FEATURE: Preserve symlink path for CLAUDE.md parent directory lookup
This bug is distinct from the above: those issues involve Claude resolving symlinks too eagerly for the working directory. This bug is the inverse — Claude does not resolve symlinks when matching a file path argument against rule globs, causing rules to be silently skipped.
4 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
None of the bot-suggested duplicates are the same bug:
~/.claude/rules/. In this case the rule file is a normal file; it's the target file being edited whose path goes through a symlink.This bug: Claude is started from the canonical project path (
~/dev/symlink), the rule file is a regular file at.claude/rules/markdown.md, and the file being edited is inside the project. The only difference is that the path argument given to Claude (/opt/symlink/test.md) routes through a symlink. That path is never resolved before being matched againstpaths:frontmatter, so the rule is silently skipped.Worth flagging the security edge of this, because it's not just cosmetic rule-loading: the same path-string matching is what
permissiondeny rules use. If a deny rule is written against a canonical path (e.g. deny writes/reads under./secrets/**), a symlink that resolves into that directory can sidestep it the same way your.claude/rules/glob is skipped — silently, with no error. So "the rule didn't load" and "the guard didn't fire" are the same bug wearing two hats. That makes it worth keeping open rather than auto-closing as a pure duplicate.The proper fix is server-side (resolve symlinks to the canonical path before matching globs / deny patterns). Until that lands, two user-side mitigations:
1. For rule loading — reference files by their canonical path so the globs match:
**2. For anything you actually need enforced (the security case)** — don't rely on path-string matching at all; resolve the real path in a
PreToolUsehook and enforce against the canonical target. This closes the symlink bypass that the built-in deny rules currently miss:Wire it for
Write|Editin.claude/settings.json. Because it canonicalizes withrealpathfirst, it fires whether the agent is handed/opt/myproject/secrets/xor~/dev/myproject/secrets/x. ForBashcommands you'd extract the path fromtool_input.commandinstead, which is messier — which is exactly why fixing this in the matcher itself (canonicalize before glob) is the real solution.@yurukusa since this issue was closed, could you open an issue for the bug you mention in your comment and reference this one?
If they indeed have the same root cause then it's worth a shot 😅