Skip to content

fix(sync): stop probing macOS protected folders during discovery - #1366

Open
wesm wants to merge 8 commits into
mainfrom
kenn-forge/issue-1364-macos-app-requests-access-to-documents-downloads-and-dropbox
Open

fix(sync): stop probing macOS protected folders during discovery#1366
wesm wants to merge 8 commits into
mainfrom
kenn-forge/issue-1364-macos-app-requests-access-to-documents-downloads-and-dropbox

Conversation

@wesm

@wesm wesm commented Aug 8, 2026

Copy link
Copy Markdown
Member

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

  • Parsing extracts project names by walking each session's recorded cwd for a git root: ancestor stats, gitfile reads, git exec.
  • Identity capture reads .git, config, and HEAD for every local session; a source-availability check stats each cwd.
  • First sync touches every historical session at once, so each guarded folder any session ever ran in produced a consent prompt, attributed to AgentsView.

Fix

  • export.ClassifyLocalPathProbe classifies a path as safe, protected user data, or automount namespace. It resolves one component at a time, checking each candidate lexically before Lstat, 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).
  • Every passive probe consults it: identity capture, the source-availability stat, the parser git-root walk, sibling-based missing-cwd recovery, gitfile targets (gitdir, commondir), and .git entries that are symlinks. Exact metadata-file paths (HEAD, config, commondir) are vetted before each read.
  • Guarded folders: ~/Desktop, ~/Documents, ~/Downloads, ~/Movies, ~/Music, ~/Pictures, ~/Library/CloudStorage, ~/Library/Mobile Documents, ~/Dropbox.
  • Sessions there keep path-only identity: still ingested, listed, and searchable, but named from the path with no Git remote, worktree relationship, or branch.
  • scan_protected_paths = true opts back in, accepting one prompt per folder. Automount namespaces stay refused regardless — the opt-in is not permission to wake automountd.
  • One asymmetry: literal automount cwds stay probeable in the parser because isForeignOSPath vets them with a resolved-autofs probe; gitfile targets get no such vetting and are refused outright.

Limits

  • The folder list is static; a cloud provider planting a top-of-home folder outside Library/CloudStorage would still prompt.
  • A legacy real ~/Dropbox directory (not a File Provider domain) loses Git detail it did not have to; the opt-in recovers it.
  • Enabling scan_protected_paths applies to sessions parsed afterward; agentsview sync --full reparses existing ones (documented).

Where to look

  • internal/export/project_identity.go — classifier, safe home resolution
  • internal/sync/engine.gomayProbeLocalPath, gitfile-target vets, walker
  • internal/parser/project.go — cwd and gitfile-target guards, statGitEntry, sibling recovery

🤖 Generated with Claude Code

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-ci

roborev-ci Bot commented Aug 8, 2026

Copy link
Copy Markdown

roborev: Combined Review (4417413)

High-severity issue remains: protected-path gating occurs too late, so macOS consent prompts can still be triggered during parsing.

High

  • internal/parser/project.go:130 — The gate runs after parsing, but parsers including Codex and Claude call ExtractProjectFromCwd*, which invokes findGitRepoRoot and stats the recorded working directory. Sessions under Documents, Downloads, or cloud folders can therefore still trigger the macOS consent prompts this change intends to prevent.
    • Fix: Apply the protected-path policy during parser project extraction, bypassing all filesystem-backed Git-root discovery for protected CWDs unless scan_protected_paths is enabled.

Medium

  • internal/export/project_identity.go:844 — Protected-path detection checks only the lexical path. An unprotected-looking CWD symlinked into ~/Documents or ~/Library/CloudStorage passes the check; subsequent os.Stat or EvalSymlinks can follow it into the protected location and trigger a consent prompt.
    • Fix: Resolve symlink components without entering protected targets, checking each resulting path against protected roots before any Stat, EvalSymlinks, or Git metadata access.

Reviewers: 2 done | Synthesis: codex, 13s | Total: 4m55s

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-ci

roborev-ci Bot commented Aug 8, 2026

Copy link
Copy Markdown

roborev: Combined Review (28e2256)

One medium-severity issue remains in the macOS path-probing safeguards.

Medium

  • internal/sync/engine.go:12528mayProbeLocalPath runs before the existing automount safeguards and invokes a resolver that calls Lstat on every path component. Paths under /home, /net, or /Network/Servers can wake automountd on every identity-cache miss, reintroducing the CPU storm these safeguards were designed to prevent.
    • Fix: Reject IsAutomountNamespacePath paths before calling ResolvesIntoProtectedUserDataPath, regardless of the protected-path opt-in.

Reviewers: 2 done | Synthesis: codex, 10s | Total: 4m32s

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-ci

roborev-ci Bot commented Aug 9, 2026

Copy link
Copy Markdown

roborev: Combined Review (b158fe3)

Review verdict: Three medium-severity issues remain in protected-path handling and opt-in refresh behavior.

Medium

  • internal/export/project_identity.go:912 — Automount namespaces return false, which callers interpret as permission to probe. A cwd symlinked into /home, /net, or /Network/Servers may therefore be followed by Stat/EvalSymlinks, while scan_protected_paths = true bypasses the resolver entirely, potentially recreating the automountd CPU storm. Represent automount paths as independently unsafe and block them regardless of the protected-path opt-in, ideally using a shared tri-state or safe-probe predicate.

  • internal/parser/project.go:170, internal/sync/engine.go:12527 — The policy validates only the cwd. An unprotected linked worktree can contain a .git file targeting a protected directory, after which Git metadata readers access that location and may trigger a consent prompt. Apply the protected-path policy to every resolved gitdir and commondir before statting or reading it, and abort Git discovery when either target is protected.

  • internal/sync/engine.go:566 — Enabling scan_protected_paths neither invalidates persisted skip state nor requeues the completed identity backfill. Previously ingested, unchanged sessions can consequently remain path-only rather than regaining full identity on the next sync. Persist policy state and refresh affected sessions when it changes, or explicitly require and initiate a full resync.


Reviewers: 2 done | Synthesis: codex, 15s | Total: 6m51s

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-ci

roborev-ci Bot commented Aug 9, 2026

Copy link
Copy Markdown

roborev: Combined Review (83de48b)

Medium-severity issues remain in the filesystem-probing safeguards.

Medium

  • internal/parser/project.go:59defaultProbeGitRootForCwd permits lexical automount paths. Although valid for a cwd vetted by isForeignOSPath, gitFileTargetsProbeable also applies it to unvetted gitfile targets. Targets under /home or /net can therefore reach readCommonDir and repeatedly wake automountd.

    Fix: Use a stricter gitfile-target predicate that rejects LocalPathProbeAutomountNamespace, or perform the first-level autofs probe before allowing the target. Add coverage for an automount gitfile target.

  • internal/parser/project.go:599; internal/sync/engine.go:12741 — Both Git-root walkers call os.Stat on .git before classifying that exact path. A safe cwd with .git symlinked into ~/Documents can still trigger a protected-folder access prompt. The sync path likewise validates parent directories instead of the exact config, HEAD, and commondir paths before reading them.

    Fix: Classify each exact .git and metadata path before any stat or read, passing the probe callback into the root walker and checkout/config helpers. Add coverage for symlinked .git entries and metadata-file targets.


Reviewers: 2 done | Synthesis: codex, 14s | Total: 7m48s

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-ci

roborev-ci Bot commented Aug 9, 2026

Copy link
Copy Markdown

roborev: Combined Review (d4b5f9a)

Medium-severity path-probing gaps remain in the macOS protected-folder safeguards.

Medium

  • internal/parser/project.go:633, internal/sync/engine.go:12763 — The .git guards execute only after root walkers call os.Stat, which follows symlinks. A .git symlink into a protected folder can therefore trigger access and a consent prompt before rejection. Vet candidates before any following stat/read, or use Lstat to classify symlinks first and pass the probe policy into findLocalGitRoot.

  • internal/parser/project.go:719 — Missing-CWD recovery via repoRootFromSiblings directly reads sibling gitfiles and their commondir targets without applying gitFileTargetsProbeable. Reuse the guarded helper so .git, gitdir, and commondir targets are checked before access.

  • internal/export/project_identity.go:900ClassifyLocalPathProbe calls filepath.EvalSymlinks(home) before checking automount namespaces. Homes under /home or symlinked through /net can wake automountd on every call, recreating the CPU-storm risk. Check automount-backed paths component-by-component before resolving symlinks, and cache the safely resolved home.


Reviewers: 2 done | Synthesis: codex, 15s | Total: 9m43s

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-ci

roborev-ci Bot commented Aug 9, 2026

Copy link
Copy Markdown

roborev: Combined Review (1135e4a)

Code review found two medium-severity gaps in protected-folder gating.

Medium

  • internal/parser/project.go:711 — Missing-CWD recovery calls osStat on dir/.git before the guarded sibling scan. A .git symlink targeting Documents or cloud storage can still trigger a protected-folder prompt. Use statGitEntry, treat a refused target as a repository boundary without following it, and add coverage for a deleted CWD whose existing ancestor has a protected .git symlink.

  • internal/sync/engine.go:12803, internal/parser/project.go:931 — Although the Git directory is vetted, reads of commondir, HEAD, and config may follow individual file symlinks into protected folders. Classify each metadata-file path before opening it and thread the probe callback into the relevant readers.


Reviewers: 2 done | Synthesis: codex, 11s | Total: 8m16s

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-ci

roborev-ci Bot commented Aug 9, 2026

Copy link
Copy Markdown

roborev: Combined Review (bb5addf)

Medium-severity path-probing issue remains despite otherwise improved macOS protected-path gating.

Medium

  • internal/parser/project.go:742 — Missing-CWD recovery stats .git beneath every sibling before applying the protected-path policy. If the first existing ancestor is the user’s home directory, this can probe paths such as ~/Documents/.git and trigger the consent prompts the change is intended to prevent.

    Suggested fix: Vet each sibling gitPath with probeGitfileTarget before calling statGitEntry, and add coverage for recovery from a deleted direct child of the home directory.


Reviewers: 2 done | Synthesis: codex, 10s | Total: 10m48s

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-ci

roborev-ci Bot commented Aug 9, 2026

Copy link
Copy Markdown

roborev: Combined Review (63ea2d7)

Medium-severity path-classification gap remains.

Medium

  • internal/export/project_identity.go:923 — Protected-path classification misses macOS’s /System/Volumes/Data APFS firmlink namespace. Paths such as /System/Volumes/Data/Users/me/Documents/repo can reach the same TCC-protected location as ~/Documents but are classified as safe; equivalent automount paths are also missed. Lexically canonicalize the /System/Volumes/Data prefix before protected-directory and automount checks, without probing the path, and add coverage for the physical namespace.

Reviewers: 2 done | Synthesis: codex, 11s | Total: 12m7s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

macos app requests access to Documents, Downloads and Dropbox

1 participant