Skip hidden folders when discovering plugins in a folder - #229
Conversation
Choosing a plugin repository in Settings -> Plugins -> Load from folder lists every Claude Code worktree under .claude/worktrees as its own candidate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matthew Boston <mboston@squareup.com>
Folder discovery already skipped .git; it now skips every dot-prefixed directory below the selected folder. Claude Code worktrees under .claude/worktrees are full checkouts with their own built plugin, so each one appeared as a separate candidate and enough of them exceeded the 32-plugin preview limit. Selecting a hidden folder directly still works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matthew Boston <mboston@squareup.com>
|
@codex review |
wesbillman
left a comment
There was a problem hiding this comment.
Pinky, commenting on Wes's behalf.
No blocking findings at 7d37a4173bcea2f0d514e6b86cfe9134995f00c9. Reviewed the full two-file diff and traced Settings → native folder picker → prepare_folder. The filter applies only to descendants, preserves root/dist discovery and direct hidden-folder selection, and leaves capability containment and Git acquisition unchanged.
Hosted CI is green: Rust logs include the new regression and the complete plugin-manager suite (11 unit + 13 integration tests), plus formatting/Clippy. CI tested merge 3bd83a6 against base 5015556a.
Remaining validation gap: actual desktop picker workflow is untested; Windows native validation was skipped. No local suites or native app launch performed. This is a review comment, not approval.
…rs-support * origin/main: feat: show owner-view agent memories in profiles (#231) Add opt-in Canvas-backed channel Todos (#222) Skip hidden folders when discovering plugins in a folder (#229) feat: add owned local agent actions to profiles (#190) feat(channels): move session creation into the context menu (#209) Add Goose as an agent harness option (#214) feat: preview channel agent activity in profiles (#187) Use context-aware identity names with human-first priority (#167) feat: add managed agents to channels from profiles (#196) Signed-off-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz> # Conflicts: # src/features/relay/outbox.ts
Why
Load from folder on a plugin repository with Claude Code worktrees lists each worktree under
.claude/worktrees/<name>, and itsdist, as a separate plugin. Picking the right one means scrolling past every other checkout. With 16 or more worktrees the preview exceeds the 32-plugin limit and the import fails.What
Folder discovery now skips every dot-prefixed folder below the selected folder; before, it skipped only
.git.node_modulesandtargetare still skipped. Selecting a hidden folder itself, or a worktree inside one, still lists its plugins. Load from Git is unchanged: worktrees are not committed, so Git imports never listed them.Alternatives considered
Results are for each rule on its own, run against buzz-plugin-pr-beacon (a plugin at the root, a built copy in
dist, three worktrees under.claude/worktrees, each with its owndist).dist.git,.claude)dist.agents,.cache, …) would need a code changedist.gitis a file (linked worktrees, submodules)dist.gitignoreand global excludes with git's rulesdist, and a freshly scaffolded plugin lists nothing.gitignorein the selected folder, never skipping a folder that holdsmanifest.jsondistgit check-ignoreGitignore-aware discovery: why git's own rules hide the plugin
buzzodz plugin newwrites a.gitignoreofnode_modules/anddist/, and its build writesplugin.jsonly todist/. Under git's rules a freshly scaffolded, built plugin lists 0 plugins and 1 warning (the rootmanifest.jsonhas noplugin.jsnext to it).!dist/manifest.jsonline cannot bring it back.dist/plugin.jsto a committed rootplugin.js. The scaffold does not do that.manifest.json, and never skip the selected folder. The docs would have to describe it as gitignore-like, not as git's behavior.dist/*plus!dist/manifest.json), because the folderdistdoes not matchdist/*.Plugin counts per rule on real plugin repositories
Each rule on its own, without this PR's dot-prefix rule:
manifest.jsondist, 3 worktrees, 3 worktreedist)dist, 1 worktree)dist, 1 worktree)examples/plugins.gitignorelists.claude/. A repository that does not ignore.claude/would list its worktrees again under C..claude/to learn whether plugins sit below it, which brings the worktrees back.dist/, which has to stay visible, andnode_modules/, which is already skipped. Gitignore matching would skip nothing extra there.User's global git excludes
~/.config/git/ignorelists.claude/worktrees/, so on that machine global excludes alone would hide the worktrees. A machine without that line would still list them, and a folder that is not a Git repository has no ignore rules at all. The dot-prefix rule is still needed either way.GIT_CONFIG_GLOBAL=/dev/null. Honoring global excludes in folder import would make the same repository list different plugins depending on the import source and on the machine.ignorecrate findscore.excludesFilewith a regular expression over~/.gitconfig, falling back to~/.config/git/config. It does not follow[include]or[includeIf], and does not check that the key sits under[core].cap-stddirectory that folder discovery is confined to today.Ignore rules from folders above the selection
.gitignorefiles and.git/info/excludesit outside the selected folder'scap-stddirectory.info/excludefrom the main repository's git folder, not the worktree's. Reaching it means following two pointer files that the imported repository controls (the worktree's.gitfile, thencommondir). That breaks the containment rule indocs/plugin-architecture.md.buzz-plugin-pr-beacon/distorbuzz-plugin-pr-beacon/.claude/worktreeswould hide everything in the folder the user picked, because the repository's.gitignoreignores both.Libraries for in-process gitignore matching
None of these crates are in
Cargo.locktoday.ignore0.4.25 withGitignoreBuilder::add_lineignore,globset,bstr,crossbeam-deque,crossbeam-epochadd()opens files withstd::fsand bypassescap-std;add_linetakes text already read throughread_source_file.WalkBuilderwalks withstd::fsandwalkdir, so it cannot replace thecap-stdwalk. Rule precedence across nested.gitignorefiles lives in private modules, soprepare_folderwould carry rule state for each pending folder itself.globsetwith hand-written.gitignoreparsing**, leading-/anchoring, trailing-/folder rules, negation, BOM and case handling.gix-ignore,gix-glob,gix-config[include]and[includeIf]not verified.Any in-process version also has to handle:
.gitignore:read_source_filerejects symlinks and would fail the whole scan, while git skips the file.core.ignorecase.Estimated at 100–200 production lines plus fixtures; not measured.
Running git to evaluate ignore rules
core.fsmonitorcommand set in the imported repository's.git/configruns during a plaingit check-ignore.-c core.fsmonitor=falseor--no-indexeach stopped it, but git still parses the untrusted config (include.path,core.excludesFile). Blocking keys one at a time would have to keep up with every future git config key that runs a command.GIT_DIRat an empty scratch repository avoids reading the imported repository's config, but then parent.gitignorefiles andinfo/excludeare not read either, which leaves the same in-folder rules the in-process options give.git check-ignorefails outside a Git repository, even with--no-index, so folders that are not repositories would need a second code path.git check-ignore --stdinprocess took 85 ms.Skipping subfolders that are Git checkouts
.githides every plugin when someone selects a parent folder holding several cloned plugin repositories. Rejected..gitis a regular file. Linked worktrees and submodules have a.gitfile; independent clones have a.gitfolder, so a folder of clones is still scanned. It costs onecap-stdstat per folder, about 5–10 lines.git statusalso stops at any nested repository without looking inside.Not in this PR
.git-file check above, if a worktree layout without a dot prefix shows up.manifest.jsonexception, reading only.gitignorefiles inside the selected folder, if ignored build folders start hitting the 20,000-entry or 60-second limits.manifest.jsonfailed to load, so this needs a new field and new UI.Test plan
dist, plus plugins under.claude/worktrees/featureand.cache/built, lists only the root anddist. Selecting.claudeitself lists the worktree plugins. The test fails on main, passes with the fix, and fails if the check is narrowed to a fixed list of folder names.distwere listed; after, only the root anddist.🤖 Generated with Claude Code