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
statunder~/Library,~/Desktop,~/Picturesfires 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
$HOMEproduces 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:
| Path | Tools | Behavior 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 |
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
| Setting | Effect | Root 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 Related
- PathJail Sandbox — the file-access boundary (separate from root guard)
- Configuration —
allow_paths,extra_roots, andpath_jailsettings - Troubleshooting — common path-related errors