Bundle Commands

Deep Dive

Markdown-based command discovery with frontmatter parsing

Bundle commands turn plain Markdown files into interactive slash commands. Each .md file contains YAML frontmatter with metadata (name, description, enabled state) and a body that becomes the prompt template. The discovery engine scans configured directories, parses every Markdown file, and produces typed ClaudeBundleCommandSpec objects for registration.

📚 What is this & why does it matter?

Bundle Commands is the mechanism that lets plugin authors define new slash commands using nothing more than Markdown files. Instead of writing TypeScript registration code, a plugin author creates a .md file with a YAML frontmatter header and a prompt template body. The discovery engine handles scanning, parsing, and registration automatically.

💡 Real-world analogy

Think of a restaurant menu binder. Each laminated page (Markdown file) has a header strip at the top with the dish name, price, and dietary flags (frontmatter), and the rest of the page is the recipe (prompt template). A new waiter (the command discovery engine) can fan through the binder, read each header strip, and add every dish to the POS system — without needing to understand how to cook anything.

Contents

Overview

Primary Role

Discover and parse bundle commands from Markdown files with frontmatter metadata.

Main Inputs

Plugin root directory, Claude bundle manifest, config for enable state.

Key Decisions

Which directories to scan, how to parse frontmatter (name, description, enabled), how to strip frontmatter from prompt template.

Outputs

ClaudeBundleCommandSpec[] — array of command specs with pluginId, name, description, promptTemplate.

flowchart LR
  Root["Plugin Root"] --> Manifest["Read Bundle Manifest"]
  Manifest --> Roots["resolveClaudeCommandRootDirs()"]
  Roots --> Scan["listMarkdownFilesRecursive()"]
  Scan --> Parse["parseFrontmatterBlock()"]
  Parse --> Strip["stripFrontmatter()"]
  Strip --> Spec["ClaudeBundleCommandSpec"]

The pipeline flows left-to-right: read the bundle manifest to find command root directories, recursively discover .md files, parse YAML frontmatter from each, strip the frontmatter to isolate the prompt template, and assemble typed command specs.

How It Works

Step 1 — Resolve Command Root Directories

The Claude bundle manifest may declare explicit command root directories. If none are declared, the engine falls back to a conventional commands/ directory relative to the plugin root.

function resolveClaudeCommandRootDirs(
  rootDir: string,
  manifest: ClaudeBundleManifest
): string[] {
  if (manifest.commandRoots?.length) {
    return manifest.commandRoots.map(r => resolve(rootDir, r));
  }
  return [resolve(rootDir, "commands")];
}

Step 2 — Recursive Markdown Discovery

listMarkdownFilesRecursive() walks each command root directory, collecting every file ending in .md. Non-markdown files and hidden directories (starting with .) are skipped.

Step 3 — Frontmatter Parsing

Each Markdown file may begin with a YAML frontmatter block delimited by --- lines. The parser extracts name, description, and enabled fields. The enabled field uses parseFrontmatterBool() which accepts true, "true", "yes", 1 as truthy values.

Step 4 — Strip Frontmatter

stripFrontmatter() removes the YAML block (including delimiters) to produce the clean prompt template body that will be sent to the AI model at command invocation time.

Step 5 — Assemble Command Specs

Each parsed file becomes a ClaudeBundleCommandSpec with the pluginId, rawName (derived from filename or frontmatter), description, promptTemplate, and sourceFilePath for debugging.

Behavior Sequence

sequenceDiagram
  participant Disc as Command Discovery
  participant FS as File System
  participant FM as Frontmatter Parser
  Disc->>FS: readClaudeBundleManifest(rootDir)
  FS-->>Disc: raw manifest
  Disc->>Disc: resolveClaudeCommandRootDirs()
  loop Each command root dir
    Disc->>FS: listMarkdownFilesRecursive(dir)
    FS-->>Disc: .md file paths
    loop Each .md file
      Disc->>FS: readFileSync(path)
      FS-->>Disc: content
      Disc->>FM: parseFrontmatterBlock(content)
      FM-->>Disc: name, description, enabled
      Disc->>Disc: stripFrontmatter(content)
      Disc->>Disc: Build ClaudeBundleCommandSpec
    end
  end

The sequence diagram shows the two nested loops: an outer loop over command root directories, and an inner loop over each discovered Markdown file. The frontmatter parser is invoked for each file, and the stripped body becomes the prompt template.

Annotated Code Walkthrough

Step through the key utility functions that power frontmatter handling:

stripFrontmatter() removes the YAML block between the opening and closing --- delimiters, returning only the prompt template body:

export function stripFrontmatter(content: string): string {
  const lines = content.split("\n");
  if (lines[0]?.trim() !== "---") {
    return content; // No frontmatter — entire content is the template
  }

  let closingIdx = -1;
  for (let i = 1; i < lines.length; i++) {
    if (lines[i].trim() === "---") {
      closingIdx = i;
      break;
    }
  }

  if (closingIdx === -1) {
    return content; // Unclosed frontmatter — treat whole file as template
  }

  return lines.slice(closingIdx + 1).join("\n").trimStart();
}
Graceful fallback: If the file starts with --- but never has a closing delimiter, the entire content is returned as the template rather than throwing an error. This ensures a broken frontmatter block never silently swallows the prompt.

parseFrontmatterBool() normalizes various truthy/falsy representations into a strict boolean, used for the enabled field:

export function parseFrontmatterBool(
  value: unknown,
  defaultValue: boolean = true
): boolean {
  if (value === undefined || value === null) return defaultValue;
  if (typeof value === "boolean") return value;
  if (typeof value === "number") return value !== 0;
  if (typeof value === "string") {
    const lower = value.toLowerCase().trim();
    if (["true", "yes", "1", "on"].includes(lower)) return true;
    if (["false", "no", "0", "off"].includes(lower)) return false;
  }
  return defaultValue;
}
Default-true convention: When the enabled field is omitted entirely, commands default to enabled. Plugin authors only need to explicitly set enabled: false to disable a command.

parseFrontmatterBlock() ties it all together — extracts YAML lines, parses key-value pairs, and returns a typed metadata object:

export function parseFrontmatterBlock(content: string): {
  name?: string;
  description?: string;
  enabled: boolean;
} {
  const lines = content.split("\n");
  if (lines[0]?.trim() !== "---") {
    return { enabled: true }; // No frontmatter
  }

  const yamlLines: string[] = [];
  for (let i = 1; i < lines.length; i++) {
    if (lines[i].trim() === "---") break;
    yamlLines.push(lines[i]);
  }

  const fields = parseSimpleYaml(yamlLines.join("\n"));
  return {
    name: fields.name as string | undefined,
    description: fields.description as string | undefined,
    enabled: parseFrontmatterBool(fields.enabled),
  };
}

The main discovery function assembles specs from all files, filtering out disabled commands:

export function discoverBundleCommands(
  rootDir: string,
  manifest: ClaudeBundleManifest,
  pluginId: string
): ClaudeBundleCommandSpec[] {
  const roots = resolveClaudeCommandRootDirs(rootDir, manifest);
  const specs: ClaudeBundleCommandSpec[] = [];

  for (const dir of roots) {
    const mdFiles = listMarkdownFilesRecursive(dir);
    for (const filePath of mdFiles) {
      const content = readFileSync(filePath, "utf-8");
      const meta = parseFrontmatterBlock(content);
      if (!meta.enabled) continue; // Skip disabled commands

      specs.push({
        pluginId,
        rawName: meta.name ?? basename(filePath, ".md"),
        description: meta.description,
        promptTemplate: stripFrontmatter(content),
        sourceFilePath: filePath,
      });
    }
  }

  return specs;
}

Edge Cases & Error Handling

ScenarioBehaviorLocation
No frontmatter (file starts without ---)Entire file content becomes the prompt template; name derived from filenameparseFrontmatterBlock()
Frontmatter without closing ---Treated as no frontmatter — whole file is the template (graceful fallback)stripFrontmatter()
enabled: false commandsSkipped during discovery; not included in the returned spec arraydiscoverBundleCommands()
Empty command directoriesNo error; listMarkdownFilesRecursive() returns an empty array, loop body never executesdiscoverBundleCommands()
Non-markdown files in command dirsIgnored by the recursive file lister; only .md extensions are collectedlistMarkdownFilesRecursive()

See also:

Directory Map

Source files for bundle command discovery
src/plugins/
├── bundle-commands.ts             # Main entry: discoverBundleCommands()
├── bundle-commands-types.ts       # ClaudeBundleCommandSpec type definition
├── bundle-commands-frontmatter.ts # parseFrontmatterBlock(), stripFrontmatter()
├── bundle-commands-bool.ts        # parseFrontmatterBool() utility
└── bundle-commands-fs.ts          # listMarkdownFilesRecursive(), resolveClaudeCommandRootDirs()