Skip to content

fix: never crawl the home directory for a file index - #306

Merged
quickbeard merged 2 commits into
mainfrom
feat/home-crawl-guard
Aug 23, 2026
Merged

quickbeard merged 2 commits into
mainfrom
feat/home-crawl-guard

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Why

On macOS the desktop app asked for permission to Downloads, Photos, Desktop, and the Music library. The TCC database showed all four requested by ai.codev.desktop — Downloads granted and Photos denied one second apart, the signature of a single recursive walk of ~.

Root cause: materializing the file-search service walks everything under its location (fff at creation; ripgrep as an eagerly forked scan), and two paths rooted that location in the home directory:

  1. the project picker's bare-word search calls find.files({directory}) against its base, which is ~;
  2. any server request that names no directory falls back to process.cwd() (workspace-routing.ts), which the sidecar inherited as ~ from the main process's startup chdir(homedir()).

Reproduced end to end against a decoy home (sentinel files with pinned atimes): one directory-less find/file request made the server open files inside Downloads/ and inside Photos Library.photoslibrary/ — exactly the accesses macOS answers with those prompts.

What

Three changes, defense in depth:

  • core (filesystem/search.ts): new unindexableDirectory() — true for the home directory (Global.Path.home, honoring CODEV_TEST_HOME) and filesystem roots. The layer chooser routes such locations to a new shallowLayer: find answers from a single readDirectoryEntries of the location itself (direct children only — no protected folder is entered), with the same fuzzysort/empty-query/limit semantics as the indexed layers; glob/grep keep their on-demand ripgrep behavior, extracted into a shared ripgrepGlobGrep effect (ripgrepLayer spreads it — no behavior change there).
  • app (directory-picker-domain.ts): the picker's bare-word branch matches direct children via the existing file.list-based match() when its base is home or a filesystem root, instead of the recursive find.files — same protection against older/remote servers.
  • desktop (main/index.ts): startup chdir goes to an empty app-owned folder (userData/cwd, created after setPath("userData")) instead of homedir(), so directory-less fallbacks have nothing to crawl. Still not /, which the original chdir was avoiding for ripgrep's sake.

Browsing into a protected folder yourself (drilling into Downloads in the picker) still prompts, once — that access is user-chosen and shallow.

Test plan

  • Reproduction flipped: with the guard, the same decoy-home find/file request returns ["Music/","Pictures/","Desktop/","Downloads/"], finds nothing nested, and every sentinel atime is untouched.
  • Desktop, seen working: bun run dev boots, UI renders, and main + all helper/sidecar processes run with cwd …/ai.codev.desktop.dev/cwd (verified via lsof).
  • New tests: unindexableDirectory (home via CODEV_TEST_HOME, dot-segments, fs root), shallowLayer find (children only, no recursion, type filter) and on-demand glob; picker bare-word-over-home and over-root staying shallow.
  • Suites: core 1087 pass, app 697 pass, desktop 151 pass, opencode test/server 293 pass. bun typecheck in core, app, desktop.

DIVERGENCES.md gains one row covering all three upstream-file edits; CHANGELOG.md gains an Unreleased Privacy entry.

🤖 Generated with Claude Code

quickbeard and others added 2 commits August 23, 2026 11:13
On macOS the desktop app asked for access to Downloads, Photos, Desktop
and the Music library. The file-search index walks everything under its
location, and two paths rooted that location in the home directory:
the project picker's bare-word search runs against its `~` base, and
requests naming no directory fall back to process.cwd(), which the
sidecar inherited as `~` from the main process. The walk opens files
inside every protected folder, and macOS prompts for each (confirmed
in the TCC database; reproduced against a decoy home, where one
directory-less find request read sentinels inside Downloads and the
photos library bundle).

Three changes close it off:

- core: unindexableDirectory (home per Global.Path.home, or a
  filesystem root) routes the search service to a new shallowLayer —
  find answers from one readdir of the location itself, glob/grep keep
  their on-demand ripgrep behaviour (extracted into a shared
  ripgrepGlobGrep; ripgrepLayer is unchanged in behaviour).
- app: the picker's bare-word search matches direct children via the
  existing file.list path when its base is home or a root, instead of
  a recursive find.files — same answers against older servers too.
- desktop: chdir to an empty app-owned folder (userData/cwd) instead
  of homedir(), so the cwd fallback has nothing to crawl. Still not
  `/`, which is what the original chdir was avoiding for ripgrep.

With the guard in place the decoy-home request returns home's direct
children and no sentinel is touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 1e5b802 into main Aug 23, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the feat/home-crawl-guard branch August 23, 2026 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant