From 1b8abb136d02373d214a990f99e371b990bd49f5 Mon Sep 17 00:00:00 2001 From: Dovah Date: Sun, 19 Jul 2026 22:26:56 -0400 Subject: [PATCH] Do not scan ~/.claude as project scope when launched from the home directory Launched with cwd = ~, the project-scope walk-up finds ~/.claude and labels it project scope while the same physical directory is also added as the user root - so every artifact in it is captured twice (observed live: a 6-artifact user inventory listed as 12, each once per scope). The user root is the true owner of ~/.claude; discover() now drops the project-scope alias, and an --add-dir resolving to ~/.claude is skipped by the same guard, mirroring the existing add-dir dedupe. Regression test: discover(start=home, home_dir=home) yields exactly one root for ~/.claude, labelled user, and the inventory captures each artifact once. Co-Authored-By: Claude Fable 5 --- src/capdisc/scope/inventory/roots.py | 13 ++++++++++++- tests/test_scope.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/capdisc/scope/inventory/roots.py b/src/capdisc/scope/inventory/roots.py index 4d4be5d..c8f6d65 100644 --- a/src/capdisc/scope/inventory/roots.py +++ b/src/capdisc/scope/inventory/roots.py @@ -153,7 +153,16 @@ def discover( kinds=_STANDALONE_KINDS, ) ) - roots += [ScanRoot(scope=ScopeKind.project, base=base) for base in _project_bases(start)] + # Launched from the home directory itself (cwd = ~), the walk-up finds `~/.claude` + # and would label it project scope while the same physical directory is also added + # as the user root below — double-capturing every artifact it holds. The user root + # is the true owner; drop the project-scope alias. + user_claude = (home_dir / ".claude").resolve() if home_dir is not None else None + roots += [ + ScanRoot(scope=ScopeKind.project, base=base) + for base in _project_bases(start) + if base.resolve() != user_claude + ] roots += [ ScanRoot(scope=ScopeKind.project, base=base, kinds=frozenset({ArtifactKind.skill})) for base in _nested_skill_bases(start) @@ -164,6 +173,8 @@ def discover( seen_project_bases = { root.base.resolve() for root in roots if root.scope == ScopeKind.project } + if user_claude is not None: + seen_project_bases.add(user_claude) for add in add_dirs: claude_dir = (add / ".claude").resolve() if claude_dir.is_dir() and claude_dir not in seen_project_bases: diff --git a/tests/test_scope.py b/tests/test_scope.py index f95348b..0407dec 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -24,6 +24,24 @@ def test_precedence_flips_by_kind(tmp_path: Path) -> None: assert _effective_scope(inv, ArtifactKind.agent, "foo") is ScopeKind.project # subagents invert +def test_home_launch_does_not_double_scan_user_claude(tmp_path: Path) -> None: + # Launched from the home directory itself (cwd = ~): `~/.claude` is the user root and + # must not also be captured as project scope, or every artifact in it lists twice. + home = tmp_path / "home" + standalone_set(home / ".claude") + roots = ScopeRoots.discover(start=home, home_dir=home) + + target = (home / ".claude").resolve() + labelled = [(root.scope, Path(root.base).resolve()) for root in roots.roots] + assert (ScopeKind.user, target) in labelled + assert (ScopeKind.project, target) not in labelled + + inv = ScopeInventory.scan(roots) + commands = [c for c in inv.artifacts if c.kind is ArtifactKind.command and c.name == "foo"] + assert len(commands) == 1 + assert commands[0].scope is ScopeKind.user + + def test_command_has_no_managed_scope(tmp_path: Path) -> None: repo, managed = tmp_path / "repo", tmp_path / "managed" (repo / ".git").mkdir(parents=True)