Skip to content

Skip hidden folders when discovering plugins in a folder - #229

Merged
wesbillman merged 2 commits into
block:mainfrom
bostonaholic:bostonaholic--folder-import-hidden-dirs
Sep 24, 2026
Merged

wesbillman merged 2 commits into
block:mainfrom
bostonaholic:bostonaholic--folder-import-hidden-dirs

Conversation

@bostonaholic

@bostonaholic bostonaholic commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Why

Load from folder on a plugin repository with Claude Code worktrees lists each worktree under .claude/worktrees/<name>, and its dist, 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_modules and target are 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 own dist).

Option Lists Verdict
Skip dot-prefixed folders below the selection root, dist This PR
Skip a fixed list of names (.git, .claude) root, dist Rejected: every other tool folder (.agents, .cache, …) would need a code change
Skip any subfolder that is its own Git checkout root, dist Rejected: hides every plugin when selecting a folder of cloned plugin repositories
Skip subfolders whose .git is a file (linked worktrees, submodules) root, dist Possible follow-up
Honor .gitignore and global excludes with git's rules root Rejected: hides dist, and a freshly scaffolded plugin lists nothing
Honor .gitignore in the selected folder, never skipping a folder that holds manifest.json root, dist Possible follow-up; same result as this PR on every repository checked
Ask git check-ignore root Rejected: runs commands from the imported repository's config
Gitignore-aware discovery: why git's own rules hide the plugin
  • buzzodz plugin new writes a .gitignore of node_modules/ and dist/, and its build writes plugin.js only to dist/. Under git's rules a freshly scaffolded, built plugin lists 0 plugins and 1 warning (the root manifest.json has no plugin.js next to it).
  • Git never re-includes a file below an ignored folder, so a !dist/manifest.json line cannot bring it back.
  • buzz-plugin-pr-beacon still lists its root only because its build script copies dist/plugin.js to a committed root plugin.js. The scaffold does not do that.
  • A usable version needs two departures from git: never skip a folder that directly holds manifest.json, and never skip the selected folder. The docs would have to describe it as gitignore-like, not as git's behavior.
  • Matching folders only, not files, avoids the file-negation cases (dist/* plus !dist/manifest.json), because the folder dist does not match dist/*.
Plugin counts per rule on real plugin repositories

Each rule on its own, without this PR's dot-prefix rule:

  • A: this PR, skip dot-prefixed folders
  • B: git's rules, skip every ignored folder
  • C: skip an ignored folder unless it directly holds manifest.json
  • D: use ignore rules only to skip folders that hold no plugin anywhere below them
Repository Plugin folders found by a full walk A B C D
buzz-plugin-pr-beacon 8 (root, dist, 3 worktrees, 3 worktree dist) 2 1 2 5
buzz-plugin-browser 3 (root, dist, 1 worktree) 2 1 2 3
buzz-plugin-handoff-board 3 (root, dist, 1 worktree) 2 1 2 3
buzz-app examples/plugins 4 (none ignored) 4 4 4 4
  • C matches this PR in all three plugin repositories only because each one's .gitignore lists .claude/. A repository that does not ignore .claude/ would list its worktrees again under C.
  • D has to walk into .claude/ to learn whether plugins sit below it, which brings the worktrees back.
  • In the three plugin repositories the only ignored folders without a dot prefix are dist/, which has to stay visible, and node_modules/, which is already skipped. Gitignore matching would skip nothing extra there.
User's global git excludes
  • The author's ~/.config/git/ignore lists .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.
  • Load from Git runs git with 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.
  • The ignore crate finds core.excludesFile with 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].
  • The file is the user's own config, so reading it is low risk. It is still a read outside the cap-std directory that folder discovery is confined to today.
Ignore rules from folders above the selection
  • Parent .gitignore files and .git/info/exclude sit outside the selected folder's cap-std directory.
  • For a linked worktree, git reads info/exclude from 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 .git file, then commondir). That breaks the containment rule in docs/plugin-architecture.md.
  • Inheriting parent rules also breaks direct selection: picking buzz-plugin-pr-beacon/dist or buzz-plugin-pr-beacon/.claude/worktrees would hide everything in the folder the user picked, because the repository's .gitignore ignores both.
Libraries for in-process gitignore matching

None of these crates are in Cargo.lock today.

Option New crates Notes
ignore 0.4.25 with GitignoreBuilder::add_line 5: ignore, globset, bstr, crossbeam-deque, crossbeam-epoch add() opens files with std::fs and bypasses cap-std; add_line takes text already read through read_source_file. WalkBuilder walks with std::fs and walkdir, so it cannot replace the cap-std walk. Rule precedence across nested .gitignore files lives in private modules, so prepare_folder would carry rule state for each pending folder itself.
globset with hand-written .gitignore parsing 2 Has to reimplement **, leading-/ anchoring, trailing-/ folder rules, negation, BOM and case handling.
gix-ignore, gix-glob, gix-config about 32 Handling of [include] and [includeIf] not verified.

Any in-process version also has to handle:

  • A symlinked .gitignore: read_source_file rejects symlinks and would fail the whole scan, while git skips the file.
  • A size limit well below the 8 MiB source-file limit, so a huge pattern file cannot slow the scan.
  • The 60-second deadline, which is checked only when a folder is taken off the queue.
  • macOS core.ignorecase.

Estimated at 100–200 production lines plus fixtures; not measured.

Running git to evaluate ignore rules
  • Reproduced on git 2.55: a core.fsmonitor command set in the imported repository's .git/config runs during a plain git check-ignore.
  • -c core.fsmonitor=false or --no-index each 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.
  • Pointing GIT_DIR at an empty scratch repository avoids reading the imported repository's config, but then parent .gitignore files and info/exclude are not read either, which leaves the same in-folder rules the in-process options give.
  • git check-ignore fails outside a Git repository, even with --no-index, so folders that are not repositories would need a second code path.
  • Folder import does not need git installed today; this would add that requirement.
  • Speed is not the issue: 1,000 paths through one git check-ignore --stdin process took 85 ms.
Skipping subfolders that are Git checkouts
  • Skipping any subfolder that contains .git hides every plugin when someone selects a parent folder holding several cloned plugin repositories. Rejected.
  • Narrower version: skip a subfolder whose .git is a regular file. Linked worktrees and submodules have a .git file; independent clones have a .git folder, so a folder of clones is still scanned. It costs one cap-std stat per folder, about 5–10 lines. git status also stops at any nested repository without looking inside.
  • Downside: it hides plugins inside submodule checkouts.
  • Not needed while worktrees live in dot-prefixed folders. It becomes a follow-up if a tool puts worktrees in a folder without a dot prefix.

Not in this PR

  • The .git-file check above, if a worktree layout without a dot prefix shows up.
  • Gitignore matching with the manifest.json exception, reading only .gitignore files inside the selected folder, if ignored build folders start hitting the 20,000-entry or 60-second limits.
  • A preview note that hidden folders were skipped. The preview's "Folders skipped" list covers only folders whose manifest.json failed to load, so this needs a new field and new UI.

Test plan

  • Added a regression test: a folder with a plugin at its root and in dist, plus plugins under .claude/worktrees/feature and .cache/built, lists only the root and dist. Selecting .claude itself 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.
  • Ran folder discovery against a real plugin repository with Claude Code worktrees: before the fix each worktree and its dist were listed; after, only the root and dist.
  • Plugin manager crate tests, Clippy and rustfmt pass.
  • Browser cases added or removed: none.
  • Not captured: screenshots. The change is in native folder discovery behind the system folder picker, and the browser test harness mocks that backend, so a browser screenshot would show fixture data rather than this change. Checking in the desktop app is still to do.

🤖 Generated with Claude Code

bostonaholic and others added 2 commits September 24, 2026 12:00
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>
@bostonaholic

Copy link
Copy Markdown
Contributor Author

@codex review

@bostonaholic
bostonaholic marked this pull request as ready for review September 24, 2026 19:00
@bostonaholic
bostonaholic requested review from a team, comp615 and wesbillman as code owners September 24, 2026 19:00

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thank you!

@wesbillman
wesbillman merged commit a05a6f6 into block:main Sep 24, 2026
12 checks passed
morgmart pushed a commit that referenced this pull request Sep 24, 2026
…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
@bostonaholic
bostonaholic deleted the bostonaholic--folder-import-hidden-dirs branch September 24, 2026 21:06
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.

2 participants