fix(sync): stop probing macOS protected folders during discovery - #1366
fix(sync): stop probing macOS protected folders during discovery#1366wesm wants to merge 8 commits into
Conversation
Local project-identity discovery resolved every session's recorded working directory and read Git metadata from it, and the source-project probe stat-ed the same path. On macOS both reach into locations guarded by a TCC consent prompt, so a first sync raised a prompt for every guarded folder any session had ever run in. The desktop app requests no file-access entitlement; the prompts came from this passive access. Discovery now skips working directories under Desktop, Documents, Downloads, Movies, Music, Pictures, Library/CloudStorage, Library/Mobile Documents, and Dropbox. Sessions there keep path-only project identity and lose only Git remote, worktree, and branch detail. The new scan_protected_paths config option opts back in for users who keep code in those folders and accept the prompt. The gate lives on the engine and defaults to closed, so an engine built without the option, including the one that drives the startup identity backfill, cannot prompt. Closes #1364
roborev: Combined Review (
|
Review of the previous commit found two gaps. First, project extraction itself probes the recorded working directory: findGitRepoRoot stats every ancestor, reads .git file contents, lists sibling directories, and execs git, all before the engine's identity gates run, so parsing a session recorded under Documents still raised the consent prompt. Extraction now consults the same protected-path policy and falls back to the path basename for refused cwds. The guard is package-level because parsers run deep inside per-format code; NewEngine enables it when scan_protected_paths is set and never disables it. Second, the protected-path check compared lexically, so a working directory that reaches a protected folder only through a symlink passed the gate and the subsequent Stat or EvalSymlinks followed the link in. ResolvesIntoProtectedUserDataPath resolves one component at a time, checking each candidate lexically before touching it with Lstat, so answering the question never enters a protected location. Unresolvable links count as protected; home is also compared in symlink-resolved form so a home behind a linked ancestor still matches.
roborev: Combined Review (
|
The symlink-aware protected-path resolver Lstats each path component, and in the identity-cache gate it runs before the automount rejections inside NormalizeRootPath and discoverLocalGitIdentity. A locally attributed session with a /home/... cwd would therefore wake automountd on every one-minute cache expiry, the CPU storm those rejections exist to prevent. The resolver now refuses automounter namespaces at every resolution step, so both a literal /home/... input and a symlink hopping into the namespace mid-walk stop before any Lstat. Automount paths are reported unprotected: nothing there is user data, and downstream identity capture already rejects them itself.
roborev: Combined Review (
|
Review of the previous commits found the protected-path policy compressed two distinct hazards into one boolean and vetted only the cwd. The resolver now classifies a path as safe, protected user data, or automounter namespace. The automount class stays refused under the scan_protected_paths opt-in — consenting to consent prompts is not consenting to waking automountd — and a symlink hopping into /home is refused where the lexical checks cannot see it. The parser guard keeps one nuance: literal automount cwds already pass isForeignOSPath's resolved-autofs probe before the guard runs, so only symlink-discovered namespace paths are refused there. Git discovery also vets what gitfile contents point at. A linked worktree in an unguarded directory can name a gitdir or common directory inside a protected folder; identity capture and the parser previously read commondir, config, or HEAD there, and the parser could escalate to exec git against the same target. Both now abort at the worktree with path-only results when a target is refused. Enabling scan_protected_paths applies to sessions parsed afterward; docs now state that agentsview sync --full reparses existing sessions.
roborev: Combined Review (
|
Review of the previous commit found two remaining probe leaks. First, gitFileTargetsProbeable reused the cwd guard, whose automount allowance exists because isForeignOSPath vets literal cwds with the resolved-autofs probe before the guard runs. Gitfile targets never get that vetting, so a gitdir under /home reached readCommonDir and woke automountd. Targets now use their own guard that refuses automount namespaces outright. Second, both discovery paths vetted directories but statted and read the .git entry itself before classifying it. A .git symlink into a protected folder — a real pre-gitfile redirection pattern — was followed by the type probe, and the engine then read HEAD and config through it. Both paths now vet the exact .git path first; classification follows links, so the symlink case is refused without touching the target. Ancestor stats in the git-root walkers stay unvetted by choice: every read is now behind a vet, per-level classification would cost a quadratic Lstat walk on the hot parse path, and stat-only metadata access is not an established TCC trigger.
roborev: Combined Review (
|
Review of the previous commit found three remaining probe paths, and Windows CI failed on tests that drive the darwin classifier with POSIX fixtures. Missing-cwd sibling recovery read sibling gitfiles and their commondir targets without the gitfile-target vetting the upward walk applies, and verified deleted worktrees by listing a .git/worktrees directory derived from those targets. Sibling .git entries now go through the same Lstat-first typing and target vetting, so a refused sibling is skipped instead of recovering the protected main repository's name. Classification resolved the home directory with EvalSymlinks before any automount check, so a home under /home, or linked through /net, woke automountd on every call. Home resolution now walks component-by- component, aborts on any automounter candidate, and is memoized per process since home never changes. Both git-root walkers statted .git entries with a following stat before any vet. They now Lstat first and follow only symlinks whose target passes the guard; a refused link marks a repo boundary without a conservative result, so the parser cannot escalate to exec git against the same target. The four tests that exercise the darwin classifier's component walk with symlinks or literal /home paths now skip on Windows, where those fixtures are not absolute paths; production Windows behavior is unchanged because the classifier is inert off darwin.
roborev: Combined Review (
|
The lint CI job failed on nilaway: findLocalGitRoot mixed Lstat and Stat results in one flow, and statGitEntry could return a nil info with a nil error. The engine walker now types the entry in a helper where every dereference sits under its own error check, and statGitEntry signals a refused symlink with a sentinel error so info is non-nil exactly when err is nil. This worktree also had no prek hooks installed, which is how the failing commit got pushed; hooks are now installed so lint gates commits again. Review of the previous commit found two remaining gaps, both fixed. The ancestor boundary check in missing-cwd sibling recovery statted dir/.git with a following stat before any vet; it now types the entry through statGitEntry so a refused symlink counts as a boundary without being followed. And the exact metadata-file paths - HEAD, config, and commondir - are now vetted before reading: they sit inside vetted directories, but as symlinks they can lead into a protected folder, and reading through one would raise the prompt every directory-level vet already prevented.
roborev: Combined Review (
|
Review of the previous commit found that missing-cwd sibling recovery typed each sibling's .git entry before any vet. When the first existing ancestor is the home directory, the siblings include Documents and the other guarded folders, so typing them Lstats inside a guarded folder - and a guarded sibling holding a real .git directory flowed into deletedChildIsWorktree, whose ReadDir of the worktrees list is exactly the enumeration macOS gates behind a consent prompt. Each sibling's .git path is now vetted before statGitEntry touches it. For guarded siblings the lexical check answers without any filesystem access, so recovery from a deleted direct child of home skips Documents entirely instead of probing it.
roborev: Combined Review (
|
Closes #1364. On first open, the macOS app asked for access to Documents, Downloads, and Dropbox. The bundle requests no file-access entitlement; the prompts came from the Go sidecar passively probing session working directories during sync.
Cause
gitexec..git,config, andHEADfor every local session; a source-availability check stats each cwd.Fix
export.ClassifyLocalPathProbeclassifies a path as safe, protected user data, or automount namespace. It resolves one component at a time, checking each candidate lexically beforeLstat, so the check itself never enters a guarded folder or wakes automountd. Symlinks are followed; home is compared raw and resolved (resolution is automount-safe and memoized).gitdir,commondir), and.gitentries that are symlinks. Exact metadata-file paths (HEAD,config,commondir) are vetted before each read.~/Desktop,~/Documents,~/Downloads,~/Movies,~/Music,~/Pictures,~/Library/CloudStorage,~/Library/Mobile Documents,~/Dropbox.scan_protected_paths = trueopts back in, accepting one prompt per folder. Automount namespaces stay refused regardless — the opt-in is not permission to wake automountd.isForeignOSPathvets them with a resolved-autofs probe; gitfile targets get no such vetting and are refused outright.Limits
Library/CloudStoragewould still prompt.~/Dropboxdirectory (not a File Provider domain) loses Git detail it did not have to; the opt-in recovers it.scan_protected_pathsapplies to sessions parsed afterward;agentsview sync --fullreparses existing ones (documented).Where to look
internal/export/project_identity.go— classifier, safe home resolutioninternal/sync/engine.go—mayProbeLocalPath, gitfile-target vets, walkerinternal/parser/project.go— cwd and gitfile-target guards,statGitEntry, sibling recovery🤖 Generated with Claude Code