# Plugin Interfaces and Base Classes in dotnet/skills

> Discover how dotnet/skills implements plugins using a metadata-driven approach with PluginDiscovery and PluginInfo instead of traditional interfaces or base classes.

- Repository: [.NET Platform/skills](https://github.com/dotnet/skills)
- Tags: api-reference
- Published: 2026-05-22

---

**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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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:

```csharp
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`](https://github.com/dotnet/skills/blob/main/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`:

```csharp
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`](https://github.com/dotnet/skills/blob/main/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`:

```csharp
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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/SKILL.md) file in the `skills/` subdirectory. The `PluginDiscovery` logic in [`eng/skill-validator/src/Shared/PluginDiscovery.cs`](https://github.com/dotnet/skills/blob/main/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`](https://github.com/dotnet/skills/blob/main/SKILL.md) file as a discrete, self-contained capability.