NAME
Langertha::Raider::Context::Plugins - Internal resolver of the installed and enabled Claude Code plugins
VERSION
version 0.504
SYNOPSIS
# Internal to Langertha-Raider -- no API promise.
my $plugins = Langertha::Raider::Context::Plugins->new(
cwd => $root,
enabled => $settings->enabled_plugin_map,
);
for my $p (@{ $plugins->plugins }) {
my @dirs = $plugins->skill_dirs($p); # each dir holds a SKILL.md
my @agents = $plugins->agent_dirs($p); # agent dirs and *.md files
my @commands = $plugins->command_dirs($p); # command dirs and *.md files
}
DESCRIPTION
Internal module. Its interface may change without notice.
The Claude Code plugins whose skills raider offers (ADR 0019 §4): every plugin that is installed and enabled. The logic follows the briefing hook (briefing-preload: claude_plugins, plugin_skill_dirs), which mirrors Claude Code:
plugins/installed_plugins.json in "claude_home" is the index (version 2:
{ plugins => { 'name@market' => [ { scope, installPath, projectPath?, version, ... }, ... ] } }), not the cache directory -- the cache keeps versions that are no longer installed.A key that "enabled" does not map to true is skipped: an installed marketplace plugin is off until
enabledPluginsturns it on.An install with a
projectPathcounts only when "cwd" is that directory or below it. Project installs come first, the deeperprojectPathfirst; user installs last; ties by key, then install path.The plugin name is
nameof .claude-plugin/plugin.json in the install path, else the key before@.
A missing or invalid installed_plugins.json means no plugins. Paths are compared lexically (.. resolved, symlinks not), as the reference does.
cwd
The directory raider runs for; compared with each install's projectPath. Required.
claude_home
The user's Claude Code directory, by default "claude_home" in Langertha::Raider::Home. undef (no home): no plugins.
enabled
The merged enabledPlugins, { 'name@market' => 0|1 } -- usually "enabled_plugin_map" in Langertha::Raider::Context::Settings. Required.
plugins
for my $p (@{ $plugins->plugins }) {
say $p->{name}.' ('.$p->{key}.', '.$p->{scope}.') at '.$p->{root};
}
The installed and enabled plugins in the order described above, each
{
name => 'superpowers', # manifest name, else key before '@'
key => 'superpowers@claude-plugins-official',
root => '/abs/install/path',
manifest => { ... }, # .claude-plugin/plugin.json, {} when absent/invalid
scope => 'project' | 'user', # 'project' when the install has a projectPath
}
A key installed more than once (user and project) appears once: its first install in this order (the deepest project install that counts, else the user install), as Claude Code loads one install per plugin. The others are dropped.
skill_dirs
my @dirs = $plugins->skill_dirs($plugin); # absolute paths as strings
The skill directories Claude Code loads from one entry of "plugins": the default skills/<name>/ scan, then each path of the manifest's skills (a string or a list), resolved against the plugin root. Each path is either a skill directory itself (it holds SKILL.md) or a directory of skill directories, scanned sorted by name. A path that leaves the plugin root is skipped. A directory reached twice is returned once (the reference yields it twice). Every returned directory holds a SKILL.md.
data_dir
my $dir = $plugins->data_dir($plugin); # .../plugins/data/briefing-getty
The plugin's persistent data directory, ${CLAUDE_PLUGIN_DATA} of its hooks: plugins/data/<id> in "claude_home", where the id is the plugin's key (name@market) with every character other than a letter, digit, _ or - replaced by - -- Claude Code's rule. A string; the directory is not created here. undef without a "claude_home".
hooks
my ( $groups, $errors ) = $plugins->hooks($plugin);
for my $g (@{ $groups->{PreToolUse} // [] }) { ... $g->{matcher}, $g->{hooks}, $g->{source} ... }
The hooks one entry of "plugins" declares, in the shape of "hooks" in Langertha::Raider::Context::Settings, with source set to plugin:<name>. Claude Code merges two places, in this order:
hooks/hooks.json in the plugin root, the event map under a top-level
hookskeythe manifest's
hooks: a path to such a file (relative to the root), an inline event map, or a list of both
A missing default file is no error. A file that is not valid JSON, has no hooks object, or a path that leaves the plugin root contributes nothing (a file named twice counts once) and is reported in $errors (strings naming the plugin and the file); never fatal.
agent_dirs
my @paths = $plugins->agent_dirs($plugin); # absolute paths as strings
Where Claude Code finds the agents of one entry of "plugins" (ADR 0020 §1): the plugin's agents/, then each path of the manifest's agents (a string or a list), resolved against the plugin root. Each returned path exists and is either a directory, to be walked for *.md (Langertha::Raider::Context::Agents), or one *.md file. A path that leaves the plugin root is skipped; a path reached twice is returned once.
command_dirs
my @paths = $plugins->command_dirs($plugin); # absolute paths as strings
Where Claude Code finds the commands of one entry of "plugins" (ADR 0030 §1), as "agent_dirs" for agents: the plugin's commands/, then each path of the manifest's commands (a string or a list; the custom paths add to commands/, they do not replace it), resolved against the plugin root. Each returned path exists and is either a directory, searched recursively for *.md (Langertha::Raider::Context::Skills), or one *.md file. A path that leaves the plugin root is skipped; a path reached twice is returned once. A manifest commands of another shape (an object) adds nothing.
mcp_servers
my ( $servers, $errors ) = $plugins->mcp_servers($plugin);
for my $s (@$servers) { ... $s->{name}, $s->{entry}, $s->{source} ... }
The MCP servers one entry of "plugins" declares (ADR 0029 §2), as Claude Code reads them, in this order:
.mcp.json in the plugin root: the map under
mcpServers, or the whole object when it has no such keythe manifest's
mcpServers: a path to such a file (relative to the root), an inline map, or a list of both
A server named twice: the later declaration wins. $servers is sorted by name, each { name, entry, source } with the entry as written and source plugin:<name> (FILE) (plugin.json for an inline map). A missing default file is no error. A file the manifest names that does not exist or is not a JSON object, a path that leaves the plugin root, or a mcpServers that is not a map contributes nothing (a file named twice counts once) and is reported in $errors; never fatal. Entries are not checked here.
SEE ALSO
SUPPORT
Issues
Please report bugs and feature requests at https://github.com/Getty/langertha-raider/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.