ADR-133: Plugin Way Discovery¶
ARCHIVED — 2026-08-13. No longer part of the active architecture set. Kept for history and so existing references still resolve.
Why: Never implemented. Four months after proposal, no plugin way-path resolution exists; the only 'claude plugin list' call in the tree counts skills for the context-cost warning in hooks/ways/macro.sh.
Nothing below this line has been edited.
Provenance. This ADR was originally drafted on 2026-04-25 as ADR-129 on the exploratory
feat/plugin-way-discoverybranch (PR #76). That branch was discarded — it had drifted ~53 commits behind main and conflicted across the refactored ways-cli internals — but the design reasoning was sound and worth keeping. It is re-filed here as ADR-133 because the original ADR-129 number was reassigned on main to "instance suffix and heartbeat liveness." Status is Proposed: the design is captured and its load-bearing assumption (claude plugin list --json) is verified current, but it is not implemented. The Implementation section names specific code (candidates.rs,cmd/corpus.rs, theSessionStarthook chain) as it stood in April 2026; those internals have since been refactored, so treat that section as indicative of approach, not literal touch points.
Context¶
Way discovery is currently hardcoded to two filesystem locations:
- Project-local:
$PROJECT/.claude/ways/ - Global:
~/.claude/hooks/ways/
Claude Code plugins can ship ways/ directories inside their install paths (matching the project-local convention, demonstrated by the x@tracer-plugins plugin which contains .claude/ways/fruity/way.md). However, the ways system has no mechanism to discover or scan these. Plugin-shipped ways are invisible.
Claude Code provides a stable CLI interface for querying plugin state:
Returns an array of installed plugins, each with:
- id — plugin identifier (name@marketplace)
- installPath — absolute path to the installed plugin on disk
- enabled — whether the plugin is currently active
- scope — "user" (global) or "project" (scoped to a specific project)
- projectPath — (project-scoped only) which project the plugin belongs to
- version — installed version
- installedAt / lastUpdated — timestamps
This is sufficient to resolve which plugins are active and where their files live.
Design constraints¶
- No per-invocation subprocess:
ways scanruns on every prompt and tool use. Shelling out toclaude plugin list --jsonon each invocation adds unacceptable latency. - Don't couple to internal file formats: Reading
installed_plugins.jsonandsettings.jsondirectly is faster but couples to Claude Code's internal storage format, which may change without notice. - Use the official CLI:
claude plugin list --jsonis the stable public interface for plugin state. - Enabled means enabled: The
enabledfield already reflects whether a plugin is active. No additional scope filtering is needed — ifenabledistrue, the plugin participates. - Version deduplication: If the same plugin ID appears with multiple versions, use the latest (by
lastUpdatedtimestamp). The CLI already resolves to the active version, but defensive dedup protects against edge cases.
Decision¶
Hybrid approach: resolve once, scan many¶
At session start, resolve enabled plugin way-paths via claude plugin list --json and write them to a session-scoped manifest. The ways binary reads this manifest during scans, adding plugin directories to the candidate collection alongside project-local and global ways.
Session-start resolution¶
A new step in the SessionStart hook chain (after ways init, before ways corpus --if-stale):
- Run
claude plugin list --json - Filter to
enabled == true - For each, check if
$installPath/ways/exists on disk - Deduplicate by plugin name: if multiple versions, keep the one with the latest
lastUpdated - Write the list of way-paths to
$SESSION_DIR/plugin-ways.json
The manifest format:
[
{
"id": "x@tracer-plugins",
"path": "/Users/tracer/.claude/plugins/cache/tracer-plugins/x/1.0.0/ways"
}
]
Candidate collection¶
collect_candidates() gains a third source, inserted between project-local and global:
1. $PROJECT/.claude/ways/ — project-local (highest priority)
2. $PLUGIN/ways/ — per enabled plugin (middle priority)
3. ~/.claude/hooks/ways/ — global (lowest priority)
The ways binary reads the session manifest (plugin-ways.json) and calls collect_from_dir() on each path. The existing WalkDir-based scanning, frontmatter parsing, domain filtering, and scope gating apply identically to plugin-sourced ways.
ID namespacing¶
Plugin way IDs are prefixed with the plugin identifier to prevent collisions:
plugin:x@tracer-plugins/fruity (from plugin)
softwaredev/code/security (from global)
softwaredev/code/testing (from project-local)
Same-ID ways across sources share a session marker (project-local overrides plugin overrides global). Plugin ways cannot shadow global ways unless they use the same domain/path structure intentionally.
Corpus integration¶
ways corpus --if-stale must include plugin way directories so that semantic (embedding) matching works for plugin-shipped ways. The corpus generation reads the same session manifest to discover additional scan roots.
Macro trust¶
Plugin macros (macro.sh files inside plugin ways) are third-party code. They follow the same trust model as project-local macros: disabled by default, enabled per-plugin via ~/.claude/trusted-plugin-macros (or extending the existing trusted-project-macros mechanism).
Consequences¶
Benefits¶
- Plugins can ship ways alongside skills and hooks — a single plugin can provide guidance, tools, and workflows
- Plugin authors can use the full way authoring surface: frontmatter, semantic matching, macros, check curves, scope gating
- No coupling to Claude Code's internal plugin storage format — uses the stable CLI interface
- Session-start resolution means zero per-scan overhead from plugin discovery
- Existing way precedence model extends naturally (project > plugin > global)
Costs¶
- Session-start adds one
claude plugin list --jsonsubprocess call (~100-200ms) - Session manifest is a new file to manage (create on start, stale if plugins change mid-session)
- Corpus regeneration may take slightly longer with additional plugin way directories
- Plugin way authors must understand the ID namespacing scheme
Risks¶
- Mid-session plugin changes: If a user installs/removes/toggles a plugin during a session, the manifest is stale until the next session or compaction. Acceptable — plugin changes are rare and a session restart is natural.
- Manifest missing: If the session manifest doesn't exist (e.g., older ways binary, failed resolution),
collect_candidates()falls back to the current two-source behavior. No breakage. - Plugin path instability: Plugin install paths include version strings that change on update. The session manifest captures the path at resolution time, so this is fine within a session. Cross-session, the next start re-resolves.
Way file path convention¶
Each way lives in its own directory, named to match the way file. This enables sibling files (.check.md, macro.sh) alongside the way definition.
| Scope | Full path |
|---|---|
| Global | ~/.claude/hooks/ways/{domain}/{way}/{way}.md |
| Project-local | $PROJECT/.claude/ways/{domain}/{way}/{way}.md |
| Plugin | $PLUGIN_INSTALL_PATH/ways/{domain}/{way}/{way}.md |
Plugins use ways/ at the plugin install root. Global ways use hooks/ways/ under ~/.claude/ because they sit alongside other hook types.
Implementation¶
Indicative as of the April 2026 ways-cli structure; verify against current code before building.
Touch points¶
- New hook script:
hooks/ways/resolve-plugins.sh— runsclaude plugin list --json, filters, writes manifest settings.json: Addresolve-plugins.shtoSessionStarthooks (afterways init)candidates.rs:collect_candidates()reads session manifest and adds plugin dirscandidates.rs:collect_checks()— same addition for check filescmd/corpus.rs: Corpus generation reads manifest for additional scan roots- Way ID derivation: Prefix plugin-sourced IDs with
plugin:{id}/ - Macro trust: New trust file or extend existing mechanism for plugin macros