Core Concepts

Root Guard

How LeanCTX prevents accidental scanning of broad or privacy-protected directories, the exact refusal rules, and workarounds.

LeanCTX prevents AI agents from scanning overly broad directories — your home folder, cloud-sync roots, or privacy-protected macOS directories. This page documents the exact rules, the two code paths that enforce them, and how to work around refusals for legitimate use cases.

Why the Root Guard Exists

The MCP server process is often spawned by editors with cwd == $HOME. A tool call whose path defaults to "." would then walk the entire home directory:

  • On macOS, every stat under ~/Library, ~/Desktop, ~/Pictures fires a TCC privacy prompt.
  • On Windows, scanning cloud-sync folders forces on-demand providers to hydrate every placeholder file (downloading gigabytes).
  • On all platforms, indexing $HOME produces a multi-million-entry index that is useless for context engineering.

Two Enforcement Paths

The root guard is enforced by two code paths with different observable behavior:

PathToolsBehavior on refusal
Walk tools ctx_search, ctx_tree, ctx_glob Returns an explicit ERROR: refusing to scan '<path>' — it resolves to a broad or privacy-protected directory
Index builders Graph index, BM25 index, semantic search Returns an empty index (0 files, 0 edges) with only a debug!-level log — indistinguishable from a genuinely empty directory at the tool-call level
Silent empty index: If ctx_compose or semantic search returns zero results for a directory you know contains files, check whether the root guard refused the scan. Run lean-ctx ls <path> — the walk-tool path will surface the actionable error.

Refusal Rules

A directory is refused when any of the following conditions hold:

Rule 1: Filesystem root

/, \, or any drive root on Windows.

Rule 2: Home directory

The user's $HOME itself (e.g. /Users/alice, C:\Users\alice).

Rule 3: macOS TCC-protected directories

When the process is a TCC-standalone identity (LaunchAgent daemon, auto-updater), any path under ~/Documents, ~/Desktop, or ~/Downloads is refused without any stat probes. Editor-spawned MCP servers inherit the IDE's TCC grant and are exempt.

Rule 4: Blocked home subdirectories

Paths at or under any of these home subdirectories are refused unless a project marker exists in their ancestry (below the blocked dir) or the directory has 2+ marker-bearing child directories:

Desktop       Documents     Downloads     Pictures
Music         Videos        Movies        Library
.local        .cache        .config       snap
Applications  OneDrive      Dropbox       Google Drive

Rule 5: Direct home children without markers

A directory that is a direct child of $HOME (e.g. ~/random-folder) is refused if it has no project marker and no 2+ marker-bearing child directories. This catches bare ~/code or ~/projects directories when they lack a git repo or build file at their own level — but a multi-repo workspace parent (e.g. ~/code containing ~/code/repo-a/.git and ~/code/repo-b/.git) passes.

Rule 6: Breadth heuristic (>50 subdirectories, no markers)

A directory with no project markers, no .NET project files, and more than 50 immediate subdirectories is refused. This catches root-like directories outside of home (e.g. /usr/local, /opt).

The 12 recognized breadth markers are:

.git            Cargo.toml       package.json      go.mod
pyproject.toml  setup.py         Makefile          CMakeLists.txt
pnpm-workspace.yaml              .projectile       BUILD.bazel
go.work

Additionally, *.csproj, *.sln, *.fsproj, and *.vbproj files satisfy the breadth check.

Workarounds

When the root guard refuses a directory you legitimately need to scan:

Option 1: Pass a specific subdirectory

# Instead of:
ctx_tree(path="~/Documents")              # ✗ refused

# Pass the actual project:
ctx_tree(path="~/Documents/my-project")   # ✓ has .git marker

Option 2: Pass a marker-bearing ancestor

If your file is at ~/Documents/company/project/src/lib.rs and ~/Documents/company/project/.git exists, any path from the project directory downward is accepted.

Option 3: Create a project marker

# Make a directory scannable by adding a marker:
touch ~/my-data-dir/.projectile
# Now ctx_tree(path="~/my-data-dir") works

Option 4: Multi-repo workspace

If a directory contains 2+ child directories with project markers, it is always accepted (the "multi-repo parent" escape):

~/code/
├── repo-a/.git     # marker
├── repo-b/.git     # marker
└── repo-c/.git     # marker
# ctx_tree(path="~/code") ✓ — multi-repo parent

What Does NOT Bypass the Root Guard

SettingEffectRoot guard impact
LEAN_CTX_ALLOW_PATH Widens PathJail (file-access sandbox) None — root guard is separate
allow_paths in config Same as above, persistent None
extra_roots in config Widens PathJail + session roots None
path_jail = false Disables PathJail entirely None — root guard is independent

The root guard and PathJail are independent safety layers. PathJail restricts which files an agent can access; the root guard restricts which directories can be scanned (walked/indexed). Widening PathJail does not affect the root guard's decisions.

Diagnosing Refusals

# Walk tools give an explicit error:
$ lean-ctx ls ~/Documents
ERROR: refusing to scan '~/Documents' — it resolves to a broad or
privacy-protected directory (/Users/alice/Documents).
Pass a specific project directory as 'path'.

# Index builders are silent — check with debug logging:
$ RUST_LOG=lean_ctx::core::graph_index=debug lean-ctx compose ~/Documents
[graph_index: refusing to scan /Users/alice/Documents — inside
home/Documents without project markers]

# Or use the walk tool to test a path:
$ lean-ctx ls ~/Documents/my-project    # if this works, the path is safe
Support this project