Plugin Interfaces and Base Classes in dotnet/skills

The dotnet/skills repository does not define a traditional C# interface or base class for plugins; instead, it uses a metadata-driven architecture centered around the PluginDiscovery static class and the PluginInfo record.

Unlike conventional plugin systems that rely on inheritance or interface implementation, the dotnet/skills repository identifies plugins through convention-based metadata. A valid plugin is simply a directory containing a plugin.json descriptor and one or more skill definitions, discovered and validated by the PluginDiscovery class located in eng/skill-validator/src/Shared/PluginDiscovery.cs.

The Metadata-Driven Plugin Model

The architecture abandons traditional object-oriented plugin patterns in favor of file-system conventions. The validator treats any folder containing a properly formatted plugin.json as a plugin, parsing its contents into strongly-typed records rather than instantiating interface implementations.

This approach eliminates the need for compiled dependencies between the validator and individual plugins, allowing skills to be defined declaratively through markdown and JSON configuration.

Core Plugin Components

PluginInfo Record

The PluginInfo record acts as the canonical representation of a plugin's metadata. Created by PluginDiscovery.ParsePluginJson, this record encapsulates data from the plugin.json file, including the plugin's name, version, description, and lists of skill and agent file paths.

SkillInfo Record

The SkillInfo record represents an individual skill within a plugin. It stores the display name, description, and the file path to the skill's SKILL.md implementation. The validation pipeline uses this type to track specific skills as they are processed alongside their parent plugin context.

PluginDiscovery Static Class

The PluginDiscovery static class serves as the sole entry point for plugin location and validation. It provides methods to traverse directory structures, parse JSON metadata, and ensure safe file access within plugin boundaries. Located at eng/skill-validator/src/Shared/PluginDiscovery.cs, this class enforces the repository's security model by validating that all file operations remain within the plugin's root directory.

Working with Plugin Discovery

Locating a Plugin from a Skill Path

To find the parent plugin context for any given skill file, use the FindPluginContext method:

using SkillValidator.Shared;

// Start with a path to a specific skill
string someSkillPath = "/path/to/plugins/dotnet-test/skills/writing-mstest-tests/SKILL.md";

var skill = new SkillInfo("Writing MSTest tests", "Adds MSTest skeletons", someSkillPath);

// Discover the containing plugin and its metadata
var pluginContext = PluginDiscovery.FindPluginContext(skill);
if (pluginContext is not null)
{
    var (root, info) = pluginContext.Value;
    Console.WriteLine($"Plugin root: {root}");
    Console.WriteLine($"Plugin name: {info.Name}");
    Console.WriteLine($"Contains {info.SkillPaths.Count} skill(s)");
}

This method traverses the directory hierarchy until it locates a plugin.json file, returning both the plugin's root path and the populated PluginInfo record.

Validating Safe File Paths

The repository implements path traversal protection through TryGetSafeSubdirectory:

string pluginRoot = "/path/to/plugins/dotnet-test";
string userPath = "../outside/file.txt";

if (PluginDiscovery.TryGetSafeSubdirectory(pluginRoot, userPath,
        out var safePath, out var error))
{
    // safePath is guaranteed to stay inside the plugin directory
    Console.WriteLine($"Safe path: {safePath}");
}
else
{
    Console.WriteLine($"Error: {error}");
}

This validation ensures that relative paths in plugin.json cannot escape the plugin's root directory, preventing directory traversal attacks.

Parsing plugin.json Metadata

Direct parsing of plugin descriptors is handled by ParsePluginJson:

string jsonPath = Path.Combine(pluginRoot, "plugin.json");
PluginInfo? info = PluginDiscovery.ParsePluginJson(jsonPath);

if (info != null)
{
    Console.WriteLine($"Loaded plugin '{info.Name}' version {info.Version}");
}

The method returns null if the JSON is malformed or missing required fields, allowing the validator to handle incomplete plugin definitions gracefully.

Summary

  • No inheritance required: dotnet/skills uses convention-based discovery rather than C# interfaces or base classes for plugin extensibility.

  • PluginInfo is the core contract: This record, generated by PluginDiscovery.ParsePluginJson, represents the complete plugin specification parsed from plugin.json.

  • Security by design: The PluginDiscovery class enforces containment through TryGetSafeSubdirectory, ensuring plugins cannot access files outside their root directory.

  • Skill granularity: Individual capabilities are tracked as SkillInfo records, each pointing to a specific SKILL.md file within the plugin structure.

Frequently Asked Questions

Is there a base class or interface that plugins must implement?

No. According to the dotnet/skills source code, plugins do not implement any C# interface or inherit from a base class. The system recognizes plugins solely by the presence of a plugin.json file in the directory root, which PluginDiscovery parses into a PluginInfo record.

How does the validator discover available plugins?

The validator uses the static PluginDiscovery class to scan for plugin.json files. When processing a specific skill path, it calls FindPluginContext to traverse parent directories until it locates the plugin root and its associated metadata descriptor.

What files constitute a valid plugin structure?

A valid plugin requires a plugin.json file at its root containing at minimum the plugin name, version, and an array of skill paths. Each referenced skill must have a corresponding SKILL.md file in the skills/ subdirectory. The PluginDiscovery logic in eng/skill-validator/src/Shared/PluginDiscovery.cs validates these relationships during the discovery phase.

How are skills represented if there's no plugin interface?

Skills are materialized as SkillInfo records containing the skill's display name, description, and file path. The validator creates these records when processing a plugin's SkillPaths collection from the PluginInfo object, treating each SKILL.md file as a discrete, self-contained capability.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →