[BUG] Path-based rules not applied when file is accessed via a symlinked path

Status Fixed / completed
Reported on v2.1.191
Maintainer reply None cached
Activity 4 comments · opened Jun 25, 2026 · closed Jun 30, 2026

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

  1. Start Claude Code in a project directory, e.g. ~/dev/myproject
  2. Create a path-based rule at .claude/rules/markdown.md with frontmatter glob **/*.md that 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 -->
```

  1. Create a symlink pointing to the project directory: sudo ln -s ~/dev/myproject /opt/myproject
  2. Ask Claude to write to a markdown file using the symlinked path: write a test line to /opt/myproject/test.md
  3. Observe: rule is not loaded — the marker comment is absent and Loaded .claude/rules/markdown.md does not appear in the output
  4. Ask Claude to write to the same file using the canonical path: write a test line to ~/dev/myproject/test.md
  5. Observe: rule is loaded — the marker comment is present and Loaded .claude/rules/markdown.md appears 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

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.

View original on GitHub ↗

4 Comments

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/67072
  2. https://github.com/anthropics/claude-code/issues/4743
  3. https://github.com/anthropics/claude-code/issues/25903

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

TaherJerbi · 2 months ago
Found 3 possible duplicate issues: 1. [[BUG] Symlink paths don't match permission patterns, causing repeated prompts #67072](https://github.com/anthropics/claude-code/issues/67072) 2. Inconsistent Permission Deny Rules for Symlinked/Mounted File Paths #4743 3. Symlinked rules files in ~/.claude/rules/ not loaded into context #25903

None of the bot-suggested duplicates are the same bug:

  • #67072 — symlink paths not matching permission patterns (repeated prompts). Different system entirely.
  • #4743 — deny rules inconsistent across mount points (WSL2). Closed, and again about permissions not rules.
  • #25903 — the rule file itself is a symlink in ~/.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 against paths: frontmatter, so the rule is silently skipped.

yurukusa · 2 months ago

Worth flagging the security edge of this, because it's not just cosmetic rule-loading: the same path-string matching is what permission deny 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:

realpath /opt/myproject/test.md   # -> ~/dev/myproject/test.md ; use that form

**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 PreToolUse hook and enforce against the canonical target. This closes the symlink bypass that the built-in deny rules currently miss:

PROTECTED="$HOME/dev/myproject/secrets"          # canonical dir to protect
target=$(jq -r '.tool_input.file_path // .tool_input.path // ""')
[ -z "$target" ] && exit 0
real=$(realpath -m -- "$target" 2>/dev/null)     # -m resolves symlinks even if the file doesn't exist yet
case "$real" in
  "$PROTECTED"|"$PROTECTED"/*)
    echo "Blocked: '$target' resolves to protected path '$real'." >&2
    exit 2 ;;                                     # exit 2 blocks the tool call
esac
exit 0

Wire it for Write|Edit in .claude/settings.json. Because it canonicalizes with realpath first, it fires whether the agent is handed /opt/myproject/secrets/x or ~/dev/myproject/secrets/x. For Bash commands you'd extract the path from tool_input.command instead, which is messier — which is exactly why fixing this in the matcher itself (canonicalize before glob) is the real solution.

TaherJerbi · 1 month ago

@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 😅