# Understanding the Custom Rules and SKILL.md System in ThePrimeagen's 99 Neovim AI Agent

> Master ThePrimeagen's 99 Neovim AI Agent by understanding its custom rules and SKILL.md system. Learn how to define reusable AI prompts in markdown for dynamic triggering.

- Repository: [ThePrimeagen/99](https://github.com/theprimeagen/99)
- Tags: deep-dive
- Published: 2026-02-16

---

**The SKILL.md system in 99 is a file-based custom rules engine that lets you define reusable AI prompts as markdown files, which are dynamically loaded and triggered via `#` completions inside Neovim.**

The **99** repository by ThePrimeagen implements a lightweight AI assistant that lives entirely within Neovim. At the heart of its extensibility lies the custom rules system, which uses specially named [`SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/SKILL.md) files to inject domain-specific knowledge into AI prompts. Understanding how this SKILL.md system works is essential for customizing the agent's behavior to match your specific codebase or workflow.

## What is the SKILL.md System?

The SKILL.md system is a convention-based approach to prompt engineering built into 99. Instead of typing repetitive instructions into the AI chat, you write reusable rules as markdown files named [`SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/SKILL.md) and store them in designated directories. When you type `#` followed by the rule name in a prompt, 99 injects the contents of that markdown file into the request context sent to the AI provider.

This system is implemented in [`lua/99/extensions/agents/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/extensions/agents/init.lua), where the **agents** extension scans configured directories, parses [`SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/SKILL.md) files, and registers them as completion items.

## How Custom Rules Work in 99

### The Agents Extension Architecture

The agents extension serves as the bridge between your filesystem and the AI prompt builder. When 99 initializes via `_99.setup(opts)`, the extension reads the `completion.custom_rules` configuration and recursively scans each directory for [`SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/SKILL.md) files.

The core logic uses `helpers.ls` to enumerate files and builds a lookup table (`by_name`) that maps rule names to their file contents. This table is then exposed through a `completion_provider` function compatible with `nvim-cmp` or any LSP-compatible completion engine.

### Loading Rules from Disk

When you trigger a completion with `#`, the provider queries the `by_name` table and returns matching rules as completion items. Upon selecting a rule, 99 reads the file contents and appends them to the `RequestContext` before sending the request to the AI provider.

The prompt assembly happens in [`lua/99/ops/make-prompt.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/make-prompt.lua), which stitches together:
1. Auto-injected markdown files (`md_files` from config)
2. The selected custom rule content
3. The user's free-form text input

## Configuring Custom Rules in Your Setup

To enable custom rules, configure the `completion.custom_rules` path in your 99 setup. Here is a complete Lazy.nvim configuration example:

```lua
-- init.lua
return {
  "ThePrimeagen/99",
  config = function()
    local a99 = require("99")
    a99.setup({
      completion = {
        source = "cmp",        -- Use nvim-cmp for completions
        custom_rules = {       -- Directories containing SKILL.md files
          vim.fn.stdpath("config") .. "/99-rules/",
          "scratch/custom_rules/",
        },
        files = {              -- Optional: enable @-file completion
          enabled = true,
        },
      },
      md_files = { "AGENT.md" },   -- Auto-inject into every prompt
    })
  end,
}

```

Each directory listed in `custom_rules` is scanned recursively. 99 identifies valid rules by the filename [`SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/SKILL.md) and extracts the rule name from the parent directory or a header within the file.

## Using SKILL.md Rules in Prompts

Once configured, invoke custom rules by typing `#` followed by the rule name in any 99 operation. For example, if you have a rule at [`99-rules/rust/SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/99-rules/rust/SKILL.md), you can reference it as `#rust`:

```vim
:'<,'>lua require('99').visual({ prompt = "#rust Refactor this to use iterators" })

```

The `#rust` trigger causes 99 to:
1. Locate the `rust` rule in the `by_name` lookup table
2. Read the contents of [`99-rules/rust/SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/99-rules/rust/SKILL.md)
3. Prepend the rule content to your prompt before sending to the AI provider

This works across all operations: `visual`, `search`, and `tutorial`.

## File Structure and Location

Understanding where 99 stores its logic helps when debugging or extending the SKILL.md system:

| File | Purpose |
|------|---------|
| [`lua/99/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/init.lua) | Core state (`_99_State`), public API, and request tracking |
| [`lua/99/extensions/agents/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/extensions/agents/init.lua) | Custom rule loading and `#` completion provider |
| [`lua/99/extensions/files/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/extensions/files/init.lua) | `@` file completion provider |
| [`lua/99/ops/make-prompt.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/make-prompt.lua) | Prompt assembly logic that injects SKILL.md content |
| [`lua/99/request-context.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/request-context.lua) | Context object holding buffer, cursor, and temp file info |

The agents extension specifically relies on `helpers.ls` (likely in `lua/99/util/` or similar) to scan directories for [`SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/SKILL.md) files during initialization.

## Summary

- **SKILL.md files** are markdown-based custom rules stored in directories configured via `completion.custom_rules`.
- The **agents extension** ([`lua/99/extensions/agents/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/extensions/agents/init.lua)) scans these directories at startup and provides `#` triggered completions.
- When you type `#rulename` in a prompt, 99 injects the contents of that rule's [`SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/SKILL.md) file into the **RequestContext** before sending to the AI provider.
- Rules work across all 99 operations including `visual`, `search`, and `tutorial`.
- The system integrates with `nvim-cmp` via the `completion_provider` interface, offering fuzzy matching against loaded rule names.

## Frequently Asked Questions

### What format should a SKILL.md file use?

A [`SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/SKILL.md) file should be valid markdown containing instructions, code examples, or context you want the AI to consider. The first line can contain a heading (e.g., `# Python Optimization`) which the agents extension may use for display purposes, but the entire file content is injected verbatim into the prompt. There is no required frontmatter or YAML metadata.

### How does 99 resolve conflicting rule names?

If multiple directories in `completion.custom_rules` contain [`SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/SKILL.md) files with identical parent directory names (e.g., [`rules/vim/SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/rules/vim/SKILL.md) and [`backup/vim/SKILL.md`](https://github.com/ThePrimeagen/99/blob/main/backup/vim/SKILL.md)), the agents extension loads both but the `by_name` lookup table will contain the last one scanned. To avoid collisions, use unique directory names or organize rules under a single root directory with descriptive subfolders like `vim-advanced` or `vim-basics`.

### Can I use SKILL.md rules with any AI provider?

Yes. The SKILL.md system is provider-agnostic. The agents extension loads rules and injects them into the `RequestContext` before the prompt is sent to whichever provider is configured (OpenCode, Claude Code, Cursor, or Kiro). The provider implementation in [`lua/99/providers.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/providers.lua) receives the fully assembled prompt containing the rule content, regardless of the backend CLI being used.

### Where should I store my custom rules?

Store custom rules in a directory outside of the 99 plugin directory to prevent loss during updates. Common locations include:
- `~/.config/nvim/99-rules/` for Neovim-specific rules
- Project-specific rules in a `.99/` or `prompts/` directory within your repository
- Shared team rules in a separate git repository cloned to `~/code/99-rules/`

Add these paths to the `completion.custom_rules` array in your `_99.setup()` configuration.