How ripgrep Handles Symbolic Links During Directory Traversal

ripgrep delegates filesystem walking to the ignore crate, which detects symbolic links via is_symlink(), follows them only when the -L/--follow flag is set, and prevents infinite recursion through ancestry-based cycle detection.

Searching through codebases that contain symbolic links requires careful handling to avoid infinite loops and unexpected traversal behavior. In the BurntSushi/ripgrep repository, all symlink-aware filesystem walking logic is encapsulated within the ignore crate, specifically within crates/ignore/src/walk.rs. The implementation respects user intent through explicit CLI flags while providing automatic safeguards against directory cycles.

Core Traversal Architecture

The filesystem traversal engine centers on the WalkBuilder struct defined in crates/ignore/src/walk.rs. This builder constructs directory walkers that yield DirEntry objects representing filesystem entries. Each entry is wrapped in a Work structure that carries metadata necessary for symlink evaluation. The separation of concerns allows ripgrep's core search engine to remain agnostic about symlink policies while the ignore crate handles the low-level traversal details.

Symlink identification occurs at the entry level through the is_symlink() method on Work instances (lines 72‑75). This method queries the underlying DirEntry for its file type:

fn is_symlink(&self) -> bool {
    self.dent.file_type().map_or(false, |ft| ft.is_symlink())
}

The file_type() method originates from crates/ignore/src/types.rs, which provides the low-level metadata abstraction used throughout the traversal pipeline.

Traversal behavior is governed by the follow_links: bool field within WalkBuilder (lines 489‑495). By default, this value is false, meaning ripgrep treats symbolic links as opaque objects rather than traversable paths. The CLI option -L or --follow toggles this field to true via the main entry point in src/main.rs, which invokes WalkBuilder::follow_links(true) when the flag is present.

When disabled (the default), directory symlinks are skipped during recursion, though file symlinks may still be matched as regular files if they appear as search results.

Decision Points During Traversal

Inside the worker loop (Worker::run at line 1753), ripgrep evaluates whether to descend into a directory using this conditional logic:

if self.follow_links && is_symlink {
    // attempt to follow it
}

This check ensures that symlink following never occurs accidentally. Only when both the user has enabled following and the current entry is identified as a symlink does the traversal logic attempt to enter the target directory.

Preventing Infinite Loops

Before descending into a symlinked directory, ripgrep invokes check_symlink_loop (lines 1892‑1912) to validate the ancestry chain. This function walks the hierarchy of ignore matchers to verify that following the symlink will not create a cycle back to an already-visited directory. If a loop is detected, the walker returns Error::Loop, halting traversal down that branch and preventing the infinite recursion that would otherwise occur in circular symlink structures.

Special Case Handling

The helper function walkdir_is_dir (lines 1000‑1014) implements a special exception for root entries. When a user explicitly passes a symlinked directory as a search path (e.g., rg "pattern" linked-dir/), ripgrep treats that symlink as a directory even without the -L flag. This avoids an extra stat call for non-root entries while honoring the user's explicit intent to search that specific path.

When a symlink points to a regular file rather than a directory, ripgrep handles it as a normal file entry during matching. However, if --follow is disabled and the symlink is not the root path, the symlink itself is ignored rather than traversed as a directory, maintaining consistent behavior with the "no follow" policy.

Practical Usage Examples

Default behavior ignores symlinks during directory traversal:


# Symlinked directories are skipped; only real directories are searched

rg "TODO" project/

Enable symlink following to traverse linked directories:


# Follow both file and directory symlinks

rg -L "TODO" project/

Root-level symlinks work without flags because you explicitly specified the path:


# linked-dir is a symlink, but ripgrep searches it because you named it directly

rg "TODO" linked-dir/

Summary

  • Default protection: Symlinks are not traversed unless -L/--follow is supplied, preventing accidental search scope expansion.
  • Cycle safety: The check_symlink_loop function detects and rejects circular symlink references before descending.
  • Explicit paths: Root-level symlinked directories are searchable without flags because the user explicitly requested that path.
  • Architecture: All logic is centralized in crates/ignore/src/walk.rs through the WalkBuilder and Worker implementations.

Frequently Asked Questions

Without -L, ripgrep identifies symlinks via is_symlink() and skips them during directory traversal. The symlink itself appears as an entry but its target is not entered, effectively pruning that branch from the search tree. This matches the behavior of find without the -follow option.

Yes. Before following any symlink, ripgrep calls check_symlink_loop (lines 1892‑1912) to walk the ancestry chain of the current path. If the symlink target points to a directory already present in the traversal stack, ripgrep returns an Error::Loop and skips that branch, preventing infinite recursion.

Why can I search a symlinked directory without -L if I specify it as the search path?

When you explicitly provide a symlinked directory as a command-line argument (e.g., rg "pattern" symlink/), ripgrep treats this as a root entry. The walkdir_is_dir helper (lines 1000‑1014) recognizes that you intentionally requested that specific path and follows the symlink once to treat it as a directory, regardless of the follow_links flag setting. This exception applies only to paths explicitly named by the user.

Following symlinks adds overhead due to the check_symlink_loop validation required for each symlinked directory encountered. However, unless your search space contains extensive symlink networks or circular references, the performance impact is typically negligible compared to the actual file I/O and regex matching operations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →