Add Claude Code plugin command discovery
What problem(s) was I solving?
Problem: CodeLayer users cannot see or use slash commands from Claude Code marketplace plugins in the CodeLayer UI.
Claude Code marketplace plugins install their commands to a different location (~/.config/claude-code/plugins/{plugin-name}/commands/), and their metadata is stored in separate JSON files (installed_plugins.json and settings.json). The daemon had no logic to discover these plugin commands.
Impact: Users installing plugins like "Superpowers" or other marketplace plugins would not see those commands available in CodeLayer, creating a disconnected experience between Claude Code and CodeLayer.
What user-facing changes did I ship?
CodeLayer will now display slash commands from installed Claude Code plugins:
- Plugin commands appear in the slash command list with
source: "plugin" - Commands are namespaced as
/plugin-name:command-name(e.g.,/superpowers:brainstorm) - Only enabled plugins' commands appear (respects user's Claude Code settings)
- Nested plugin commands use colon separators (e.g.,
/plugin-name:config:show) - Fuzzy search works across plugin commands
- Commands seamlessly integrate with existing local/global commands
No breaking changes - existing local and global commands work exactly as before.
How I implemented it
Architecture Decision: Isolated Plugin Discovery
I chose to encapsulate all plugin-specific logic in isolated functions to make the plugin system maintainable and adaptable to future changes by Anthropic:
New File: hld/api/handlers/plugins.go (122 lines)
discoverPluginCommands(configDir string)- Main discovery orchestrationscanPluginCommandsDir(dir, pluginName string)- Scans individual plugin commands- Type definitions for plugin metadata structures
Implementation Details
1. OpenAPI Schema Extension (hld/api/openapi.yaml)
- Added
"plugin"to theSlashCommand.sourceenum - Regenerated Go types →
SlashCommandSourcePluginconstant
2. Plugin Discovery Logic (hld/api/handlers/plugins.go)
Discovery Flow:
1. Read $CLAUDE_CONFIG_DIR/plugins/installed_plugins.json
- Contains plugin metadata: version, install path, git commit
2. Read $CLAUDE_CONFIG_DIR/settings.json
- Contains enabledPlugins map (plugin-id → true/false)
3. For each enabled plugin:
- Extract plugin name from "plugin-name@marketplace" ID
- Scan {installPath}/commands/ for .md files
- Create namespaced command: /plugin-name:command-name
4. Return list of plugin commands with source="plugin"
if CLAUDE_CONFIG_DIR is discovery will use ~/.claude
3. Error Handling (Graceful Degradation)
- Missing
installed_plugins.json→ return empty list (no plugins installed) - Missing
settings.json→ treat all plugins as enabled (default behavior) - Malformed JSON → log warning, return empty list
- Missing plugin commands directory → skip that plugin
- All errors logged but don't break the entire API request
4. Integration (hld/api/handlers/sessions.go)
- Added plugin discovery after global command discovery (line 1699-1721)
- Plugin commands merged into same deduplication map as local/global
- Maintains existing precedence: local → global → plugin (no conflicts due to namespacing)
5. Comprehensive Testing
Unit Tests (hld/api/handlers/plugins_test.go - 12 tests):
- No installed plugins file
- Malformed
installed_plugins.json - No settings file (default enabled behavior)
- Disabled plugin filtering
- Enabled plugin discovery
- Multiple plugins
- Non-existent directories
- Empty directories
- Single command
- Nested commands (colon separator)
- Non-markdown file filtering
- Multiple commands per plugin
Integration Tests (hld/daemon/daemon_slash_commands_integration_test.go):
TestPluginCommandsIntegrationwith 3 subtests:- Enabled/disabled plugin filtering via settings.json
- Default behavior without settings.json (all enabled)
- Graceful handling of missing installed_plugins.json
Key Design Decisions
1. Namespace Format: /plugin-name:command
- Plugin name is in the command name itself
- Source field remains simple enum: "plugin" (not "plugin:name")
- Easy to parse:
command.name.split(':')[0].slice(1)gives plugin name
2. Config Directory Detection
- Uses existing
CLAUDE_CONFIG_DIRenvironment variable logic - Falls back to
~/.claude(macOS/Linux standard) - Leverages existing
expandTilde()helper function
3. Isolation Pattern
- All plugin logic in separate
plugins.gofile - Can be updated independently if Anthropic changes plugin format
- No pollution of main session handler logic
How to verify it
- [x] I have ensured
make check testpasses
Verification Results:
✓ Format check passed
✓ Vet check passed
✓ Lint check passed
✓ Unit tests passed (424 tests)
✓ Unit tests with race detection passed (424 tests)
✓ Integration tests passed (50 tests)
✓ Integration tests with race detection passed (50 tests)
Test Coverage:
- 12 new unit tests for plugin discovery (all passing)
- 3 new integration test scenarios (all passing)
- 0 failing tests
- 0 lint issues
Manual Testing (optional but recommended):
With real Claude Code plugins installed:
# Start daemon
cd hld && make build && ./hld
# Query plugin commands
curl "http://127.0.0.1:5188/api/v1/slash-commands?working_dir=$(pwd)" | \
jq '.data[] | select(.source == "plugin")'
# Test fuzzy search
curl "http://127.0.0.1:5188/api/v1/slash-commands?working_dir=$(pwd)&query=brain" | \
jq '.data[] | select(.source == "plugin")'
Description for the changelog
Added: Claude Code marketplace plugin commands now appear in CodeLayer's slash command list. Plugin commands are discovered from ~/.config/claude-code/plugins/ and respect enabled/disabled state from Claude Code settings. Commands are namespaced as /plugin-name:command-name.
A picture of a cute animal (not mandatory but encouraged)
Technical Notes
Files Modified:
hld/api/openapi.yaml- Added "plugin" to source enumhld/api/server.gen.go- Auto-generated constanthld/api/handlers/sessions.go- Added plugin discovery call (24 lines)hld/daemon/daemon_slash_commands_integration_test.go- Added integration test (274 lines)hld/go.mod/hld/go.sum- Added test mock dependencies
Files Created:
hld/api/handlers/plugins.go- Plugin discovery implementation (122 lines)hld/api/handlers/plugins_test.go- Comprehensive unit tests (250+ lines)
Implementation Plan: thoughts/shared/plans/2025-01-08-plugin-command-discovery.md
Research Document: docs/research/2025-11-04-codelayer-slash-command-loading.md
Total Changes: +635 lines, -156 lines across 6 modified + 2 new files
[!IMPORTANT] Adds discovery and display of Claude Code marketplace plugin commands in CodeLayer UI, with new logic in
plugins.goand comprehensive testing.
- Behavior:
- Adds discovery of slash commands from Claude Code marketplace plugins in
hld/api/handlers/plugins.go.- Commands are namespaced as
/plugin-name:command-nameand appear withsource: "plugin".- Only enabled plugins' commands are shown, respecting
settings.json.- Supports nested commands with colon separators.
- Integrates with existing local/global commands without breaking changes.
- Implementation:
- New file
plugins.gofor plugin command discovery logic.- Updates
openapi.yamlto add "plugin" toSlashCommand.sourceenum.- Modifies
sessions.goto include plugin command discovery.- Testing:
- Adds unit tests in
plugins_test.gofor various scenarios including missing files and malformed JSON.- Adds integration tests in
daemon_slash_commands_integration_test.gofor plugin command discovery.- Misc:
- Updates
go.modandgo.sumfor new dependencies.This description was created by
for c3542968d8d014c879f8aa6e1d4909e3b3f7e45a. You can customize this summary. It will automatically update as commits are pushed.
Thanks for the PR @crdant !
Plugins are still considered to be in beta by anthropic right now so I am hesitant to merge something that depends on what the Claude code team considers an unstable API
Will discuss internally and let you know where we wind up on this
Plugins are still considered to be in beta by anthropic right now so I am hesitant to merge something that depends on what the Claude code team considers an unstable API
Makes a lot of sense.
This PR and my last one have a common theme. hld is in the loop for autocompleting slash commands, so as soon as I type / and don't see a command I expect I'm back to the terminal until I have time to create a PR, get it approved, and install a new release.
This whack-a-mole game isn't the best thing for me as a user or for your team as creators. Perhaps there's a better approach I can't see to stay aligned so the / makes the same recommendations either in or out of CodeLayer.
Appreciate both sides of this convo, and thanks chuck for the PR!
@K-Mistele is this something we could drop in an opt-in “experimental features” thing?
I would guess Claude will continue to launch beta features that ppl wanna use and we should weigh the tradeoffs of not supporting them
@K-Mistele @dexhorthy if the experimental feature model makes sense, I can imagine I wouldn't be too hard for me to wrap it and contribute the whole thing.