Quick Reference
Features Covered
| Feature | Module | Version |
|---|---|---|
| Skill CRUD | src/storage/skill-store.ts | v1.0 |
| Skill Index | src/storage/skill-index.ts | v1.0 |
| Skill Validation | src/validation/skill-validation.ts | v1.1 |
| Conflict Detection | src/conflicts/ | v1.2 |
| Skill Format | src/types/skill.ts | v1.0 |
Prerequisites
- Node.js 18+ installed
- gsd-skill-creator installed (
npm install) - A
.claude/skills/directory in your project or~/.claude/skills/for user-level skills - Basic familiarity with YAML frontmatter and Markdown
Configuration Reference
Storage Paths
- Project-level skills:
.claude/skills/{name}/SKILL.md - User-level skills:
~/.claude/skills/{name}/SKILL.md - Skill index cache:
.claude/skills/.skill-index.json - Progressive disclosure:
.claude/skills/{name}/references/ - Extracted scripts:
.claude/skills/{name}/scripts/
Name Constraints
- 1-64 characters
- Lowercase letters, numbers, hyphens only
- Must start and end with alphanumeric character
- No consecutive hyphens (
--) - Pattern:
/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/
Description Limits
- Maximum 1024 characters
- Activation-friendly patterns recommended
- Quality score computed from trigger phrases and capability statements
Conflict Detection Thresholds
- Default threshold: 0.85 cosine similarity
- Valid range: 0.5 to 0.95
- High severity: above 0.90
- Medium severity: threshold to 0.90
Allowed-Tools Format
- Array:
['Read', 'Grep'] - Space-delimited string:
'Read Grep' - Both formats accepted; normalized to array on write
Related Guides
Overview
Core Skill Management is the foundation of gsd-skill-creator. Every other feature in the system — learning, activation, conflict resolution, safety — depends on the ability to create, read, update, delete, and discover skills reliably. This guide covers the five modules that make that possible.
A skill in gsd-skill-creator is a Markdown file with YAML frontmatter stored inside a named subdirectory under .claude/skills/. The frontmatter contains metadata (name, description, triggers, learning data) while the Markdown body contains the instructions that Claude reads when the skill activates. The SkillStore manages the filesystem operations, the SkillIndex provides fast in-memory search, the validation layer enforces correctness, and the conflict detector prevents overlapping skills from causing unpredictable behavior.
Together these five modules ensure that skills are always well-formed, discoverable, and non-contradictory. If you are building tooling on top of gsd-skill-creator, or if you want to understand how skills flow from creation to activation, this is the place to start.
The lifecycle of a skill through these modules follows a clear path: the user defines skill content, the validation layer checks correctness (name, description, metadata), the SkillStore writes it to disk in the standardized format, the SkillIndex caches it for fast discovery, and the conflict detector checks it against existing skills for overlap. After creation, the skill is immediately available for activation through the Activation Pipeline, and over time it may evolve through the Learning & Refinement system.
Key Concepts
Think of gsd-skill-creator as a recipe book for an AI assistant. Each skill is like a recipe card: it has a title (the name), a short summary of what it is for (the description), and detailed step-by-step instructions (the body). The Skill Store is the filing cabinet where you keep those cards. The Skill Index is the index tab at the front of the cabinet that lets you quickly flip to the right card by keyword. The validation layer is the quality checker that makes sure every card has a proper title and follows the formatting rules before it goes in the cabinet. And the conflict detector is the editor who reads all the cards and says, "Wait, these two recipes both describe how to make chocolate cake — you should probably merge them or rename one."
Skills live in simple text files that you can read and edit with any editor. The metadata at the top of the file uses YAML, which is just a way to list properties like name: my-skill one per line. The instructions below the metadata are written in Markdown, a lightweight formatting language where # Heading makes a heading and - item makes a bullet point. There is nothing exotic here — it is all designed to be human-readable and version-controllable with Git.
The conflict detector works like a plagiarism checker: it reads the descriptions of all your recipe cards and uses math (specifically, it converts descriptions into number arrays called "embeddings" and measures how similar those arrays are) to find cards that describe very similar things. If two cards are more than 85% similar, it raises a flag so you can decide whether they should be combined into one card or kept separate with clearer distinctions.
How It Works
1. Skill CRUD (SkillStore)
The SkillStore class in src/storage/skill-store.ts is the primary interface for all filesystem operations on skills. It manages the subdirectory-based storage format where each skill lives in its own folder.
Storage Format
Each skill occupies a subdirectory under the configured skills directory (default: .claude/skills/ for project-level, ~/.claude/skills/ for user-level). The directory name is the skill name, and the main file is always SKILL.md. The standard layout is:
.claude/skills/
my-skill/
SKILL.md # Main skill file (frontmatter + body)
references/ # Optional: decomposed sections for large skills
architecture.md
examples.md
scripts/ # Optional: extracted shell scripts
deploy.sh
The SKILL.md file is always present. The references/ and scripts/ subdirectories are created automatically when a skill exceeds the progressive disclosure threshold (2000 words with multiple sections), allowing Claude to load the compact main file and fetch detailed sections on demand. This approach keeps the initial skill load fast while preserving all the detail for on-demand reading.
Create
The create() method validates the skill name against the official Claude Code specification, checks for reserved names, validates metadata structure, and writes the skill to disk. It automatically generates timestamps, sets the version to 1, and normalizes metadata into the official format (moving gsd-skill-creator extension fields into the metadata.extensions container).
The creation workflow follows a strict validation pipeline:
- Name validation via
validateSkillNameStrict()with auto-fix suggestions - Reserved name checking via
validateReservedName() - Name-metadata consistency check (skillName must match metadata.name)
- Metadata structure validation via
validateSkillMetadata() - Legacy format detection and migration logging
- Path safety assertion via
assertSafeSkillPath() - Directory creation and SKILL.md file write
- Budget validation with warning on exceeding character limits
const store = new SkillStore('.claude/skills');
const skill = await store.create('lint-on-save', {
name: 'lint-on-save',
description: 'Use when editing TypeScript files to run ESLint after save',
'allowed-tools': ['Bash', 'Read'],
metadata: {
extensions: {
'gsd-skill-creator': {
triggers: {
files: ['*.ts', '*.tsx'],
intents: ['lint', 'format', 'code quality'],
},
},
},
},
}, `# Lint on Save
When a TypeScript file is saved, run ESLint with auto-fix:
\`\`\`bash
npx eslint --fix {{file}}
\`\`\`
Report any remaining issues to the user.
`);
If the name is invalid, the method throws an error that includes a suggested correction. For example, passing "Lint On Save" produces an error suggesting "lint-on-save".
Create with Progressive Disclosure
The createWithDisclosure() method extends standard creation with automatic content decomposition. For large skills (over 2000 words with multiple sections), the ContentDecomposer splits the body into a compact SKILL.md and separate reference files. For smaller skills, it falls through to the standard create() path. After writing all files, the ReferenceLinker validates references for circular dependencies.
// Automatically decomposes if body is large enough
const skill = await store.createWithDisclosure('deployment-guide', metadata, largeBody);
// Creates:
// .claude/skills/deployment-guide/SKILL.md (compact summary)
// .claude/skills/deployment-guide/references/ (detailed sections)
// .claude/skills/deployment-guide/scripts/ (executable scripts)
Read
The read() method loads a skill from disk, parsing the YAML frontmatter with safe deserialization (no arbitrary code execution via the safeParseFrontmatter() wrapper) and validating it against the Zod schema. It returns a Skill object with metadata, body, and path fields. Every read operation goes through path safety checks to prevent traversal attacks, even on internal calls.
The read process follows a strict pipeline: first assertSafeName() validates the skill name, then assertSafeSkillPath() verifies the resolved path stays within the skills directory, then the file content is loaded and parsed. If the frontmatter is invalid or the Zod schema validation fails, a descriptive error is thrown with the specific validation failure.
const skill = await store.read('lint-on-save');
console.log(skill.metadata.name); // 'lint-on-save'
console.log(skill.metadata.description); // 'Use when editing TypeScript...'
console.log(skill.body); // '# Lint on Save\n\nWhen a...'
console.log(skill.path); // '.claude/skills/lint-on-save/SKILL.md'
Update
The update() method merges partial metadata updates with the existing skill, increments the version number, updates the updatedAt timestamp, validates the result, and writes back to disk. Extension data from both the existing skill and the update is merged using the getExtension() accessor pattern, ensuring correct handling regardless of whether legacy or new metadata format is used.
The method also checks the token budget after the update, emitting warnings at two levels:
- Error level: Content exceeds the budget ceiling — Claude Code may hide the skill entirely.
- Warning level: Content is approaching the budget — consider reducing size proactively.
const updated = await store.update('lint-on-save', {
description: 'Use when editing TypeScript or JavaScript files to run ESLint',
}, newBody);
console.log(updated.metadata.description); // Updated description
// Extension data preserved: version incremented, updatedAt refreshed
Delete
The delete() method removes the SKILL.md file and cleans up any references/ and scripts/ subdirectories, then removes the skill directory itself. Path safety checks run before any filesystem operation. The cleanup uses force: true to handle the common case where subdirectories do not exist (skills without progressive disclosure). Note that deletion is permanent — to preserve history, ensure the skill was committed to Git before deleting. The audit logger records all delete operations for forensic tracking.
await store.delete('lint-on-save');
// Removes: SKILL.md, references/ (if exists), scripts/ (if exists)
List, Exists, and Format Discovery
The list() method scans the skills directory for subdirectories containing SKILL.md and returns an array of skill names. The exists() method checks for a single skill by name. Both methods handle the case where the skills directory does not exist yet.
const names = await store.list();
// ['beautiful-commits', 'lint-on-save', 'test-runner']
const found = await store.exists('lint-on-save');
// true
The listWithFormat() method detects both current (subdirectory) and legacy (flat file) formats, returning format indicators for each skill. This powers migration workflows that convert old-format skills to the new subdirectory layout.
const withFormat = await store.listWithFormat();
// [
// { name: 'lint-on-save', format: 'current', path: '.claude/skills/lint-on-save/SKILL.md' },
// { name: 'old-skill', format: 'legacy', path: '.claude/skills/old-skill.md' },
// ]
const hasLegacy = await store.hasLegacySkills();
// true — migration recommended
2. Skill Index (SkillIndex)
The SkillIndex class in src/storage/skill-index.ts provides an in-memory cache of skill metadata for fast lookups without reading every skill file from disk on each query. It stores index data as a JSON file alongside the skills and uses file modification times (mtime) for cache invalidation.
Index Structure
Each index entry captures the essential fields needed for matching and filtering:
interface SkillIndexEntry {
name: string;
description: string;
enabled: boolean;
triggers?: {
intents?: string[]; // Regex or keyword patterns
files?: string[]; // Glob patterns for file matching
contexts?: string[]; // Context substring patterns
};
events?: {
emits?: string[]; // Events this skill produces
listens?: string[]; // Events this skill responds to
};
path: string;
mtime: number; // File modification time for cache invalidation
}
The index is stored at .claude/skills/.skill-index.json and rebuilt automatically when missing, corrupted, or stale. The index data format includes a version number and build timestamp for debugging and compatibility checks.
Loading and Refreshing
The index lifecycle has three modes:
- Load: Read the index from disk. If the file is missing or corrupt, automatically triggers a full rebuild.
- Rebuild: Discard the in-memory cache, re-read every skill from disk, and write a fresh index file. Used on first run or when the index is suspected to be corrupt.
- Refresh: Incrementally update the cache by checking each entry's mtime against the actual file. Only re-reads changed skills, removes deleted ones, and discovers new skills not yet in the index. This is the preferred mode for normal operation.
const index = new SkillIndex(store, '.claude/skills');
// First use: loads from disk or rebuilds if missing
await index.load();
// After modifying skills on disk (incremental):
await index.refresh();
// Force full rebuild:
await index.rebuild();
Search Operations
The index provides three search modes, each calling refresh() first to ensure up-to-date results:
- Text search —
search(query)performs case-insensitive substring matching against skill names and descriptions. Returns all skills where the query appears anywhere in the name or description text. - Trigger matching —
findByTrigger(intent?, file?, context?)returns enabled skills whose trigger patterns match the given criteria. Intent patterns support regex (compiled with case-insensitive flag), file patterns support glob syntax (converted to regex internally), and context patterns use substring matching. A skill matches if any one of its trigger conditions matches the provided criteria. - Enabled filter —
getEnabled()returns only skills whereenabledis true, filtering out disabled skills that should not activate.
// Text search
const results = await index.search('lint');
// Returns skills with "lint" in name or description
// Trigger matching — finds skills triggered by these conditions
const triggered = await index.findByTrigger(
'run linting', // intent: matches against regex patterns
'src/app.tsx', // file: matches against glob patterns
'GSD execute phase' // context: matches against substring patterns
);
// Get all enabled skills
const enabled = await index.getEnabled();
// Get every skill (enabled and disabled)
const all = await index.getAll();
Multi-Scope Listing
Skills exist at two scopes: project-level (inside the project's .claude/skills/) and user-level (inside ~/.claude/skills/). Project-level skills are version-controlled with the project and shared across the team. User-level skills are personal preferences that apply to all projects. When both scopes define a skill with the same name, the project-level version takes precedence during activation.
The standalone listAllScopes() function creates separate stores and indexes for user-level and project-level skills, loads both in parallel, and returns combined results with scope labels and conflict flags. When a skill with the same name exists at both scopes, both entries are marked with hasConflict: true, enabling the UI to warn about shadowing.
import { listAllScopes } from './skill-index.js';
const all = await listAllScopes();
// [
// { name: 'lint-on-save', scope: 'project', hasConflict: false, ... },
// { name: 'beautiful-commits', scope: 'user', hasConflict: true, ... },
// { name: 'beautiful-commits', scope: 'project', hasConflict: true, ... },
// ]
The conflict flag is informational only — it does not prevent loading. The activation pipeline handles scope resolution by preferring project-level skills when conflicts exist.
Cache Invalidation via mtime
The index uses file modification timestamps (mtime) for cache invalidation. During a refresh, the index compares the stored mtime for each entry against the actual file's current mtime. If they differ, the skill is re-read from disk and the index entry is updated. This approach is lightweight (a single stat() call per skill) and avoids the need to read and parse every SKILL.md file on every operation.
The limitation of mtime-based invalidation is that it relies on the filesystem updating timestamps accurately. In rare cases (filesystem bugs, network-mounted drives with clock skew, or manual touch without content change), the index may be stale or unnecessarily refreshed. The rebuild() method provides a fallback for these edge cases.
The index file itself includes a version number and builtAt timestamp for debugging cache issues.
3. Skill Validation
The validation layer in src/validation/skill-validation.ts enforces correctness at every stage of the skill lifecycle. It combines Zod schemas for structural validation with custom validators for name correctness, description quality, reserved name checking, and field classification. The module contains 15 distinct validators organized into five categories.
Name Validation
The OfficialSkillNameSchema enforces the official Claude Code naming specification using a chain of Zod validators with context-aware error messages:
- 1-64 characters in length
- Only lowercase letters, numbers, and hyphens
- Must start and end with an alphanumeric character (not a hyphen)
- No consecutive hyphens (
--)
When validation fails, every error message includes an auto-generated fix suggestion when possible. The suggestFixedName() function performs the transformation: lowercase, replace invalid characters with hyphens, collapse consecutive hyphens, trim hyphens from edges, and truncate to 64 characters.
import { validateSkillNameStrict } from './validation/skill-validation.js';
const result = validateSkillNameStrict('My Cool Skill!');
// {
// valid: false,
// errors: [
// 'Skill name must start with a lowercase letter or number (not hyphen).
// Did you mean: "my-cool-skill"?',
// 'Invalid characters in name. Only lowercase letters, numbers, and hyphens
// allowed. Did you mean: "my-cool-skill"?'
// ],
// suggestion: 'my-cool-skill'
// }
const good = validateSkillNameStrict('lint-on-save');
// { valid: true, errors: [] }
Description Quality
The validateDescriptionQuality() function checks whether a description contains activation-friendly patterns that help Claude match the skill to user intent. It scans for six regex pattern categories including "Use when...", "Activate when...", "For handling...", and mention-based patterns. Descriptions missing these patterns receive a warning (not an error) with concrete suggestions for improvement.
Why description quality matters: Claude Code uses skill descriptions as the primary signal for automatic activation. A description like "handles linting" gives Claude little to work with, while "Use when editing TypeScript files to run ESLint with auto-fix" contains explicit trigger phrases ("Use when editing TypeScript files"), a capability statement ("run ESLint"), and a specificity qualifier ("with auto-fix"). The quality validator encodes these patterns into six categories of activation-friendly text.
The enriched result includes a quality score, capability statement detection, and Use-when clause detection for comprehensive quality assessment:
import { validateDescriptionQuality } from './validation/skill-validation.js';
// Good description
const good = validateDescriptionQuality(
'Use when editing TypeScript files to run ESLint with auto-fix'
);
// {
// hasActivationTriggers: true,
// hasCapabilityStatement: true,
// hasUseWhenClause: true,
// qualityScore: 0.9,
// }
// Poor description
const poor = validateDescriptionQuality('Handles TypeScript linting');
// {
// hasActivationTriggers: false,
// qualityScore: 0.4,
// warning: 'Description may not activate reliably...',
// suggestions: [
// 'Add "Use when..." to specify when this skill should activate',
// 'Include specific keywords users might mention',
// 'Example: "Use when working with TypeScript projects"',
// ]
// }
Reserved Name Checking
The validateReservedName() function loads a curated list of reserved names (system commands, common tool names, Claude Code built-in commands) and checks whether a proposed skill name would conflict. The validator is lazy-loaded and cached for performance. If the name is reserved, the function returns the category, reason, a formatted error message, and a list of alternative name suggestions.
const result = await validateReservedName('help');
// {
// valid: false,
// reserved: true,
// category: 'claude-code-builtin',
// reason: 'Conflicts with Claude Code built-in command',
// error: 'Name "help" is reserved (claude-code-builtin)...',
// alternatives: ['help-guide', 'project-help', 'custom-help']
// }
Metadata Schema Validation
The SkillMetadataSchema validates the complete frontmatter structure using Zod, covering all official Claude Code fields and gsd-skill-creator extensions. It accepts both the legacy format (extension fields at root level) and the new format (extension fields under metadata.extensions.gsd-skill-creator), using .passthrough() to preserve unknown fields from other tools.
The AllowedToolsSchema provides dual-format support for the allowed-tools field, accepting both arrays (['Read', 'Grep']) and space-delimited strings ('Read Grep') and normalizing to an array on output. This dual-format acceptance means skills authored by hand (where a space-delimited string is more natural to type) and skills generated programmatically (where arrays are the canonical representation) are both accepted without conversion errors.
The known tool list used for validation includes: Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch, and NotebookEdit. Unknown tool names are accepted but may generate a warning — Claude Code may add new tools in future versions, and the .passthrough() approach ensures forward compatibility.
Field Classification
The classifyFields() function categorizes every frontmatter field into one of four buckets:
- Standard fields: Official Agent Skills spec fields (
name,description,license,compatibility,metadata,allowed-tools) — portable across all tools that implement the spec. - Claude Code extensions: Fields specific to Claude Code (
context,agent,model,hooks,disable-model-invocation,user-invocable,argument-hint) — portable across Claude Code implementations. - gsd-skill-creator extensions: Fields specific to gsd-skill-creator (
triggers,learning,enabled,version) — only meaningful when gsd-skill-creator is installed. - Unknown fields: Fields not recognized by any category — may come from other tools or be user-defined custom metadata.
This classification powers informational CLI output and helps users understand which fields are portable across tools. Skills intended for sharing should minimize gsd-skill-creator extension fields and maximize standard fields.
import { classifyFields } from './validation/skill-validation.js';
const classification = classifyFields({
name: 'my-skill',
description: 'A skill',
model: 'claude-sonnet-4-20250514',
context: 'fork',
'custom-field': 'value',
});
// {
// standard: ['name', 'description'],
// extensions: ['model', 'context'],
// unknown: ['custom-field'],
// gsdExtension: false
// }
4. Conflict Detection
The conflict detection system in src/conflicts/ identifies skill pairs with overlapping functionality before they cause unpredictable activation behavior. It uses embedding-based semantic similarity to compare skill descriptions, going beyond simple keyword matching to understand meaning.
Why Conflict Detection Matters
When two skills have overlapping descriptions, Claude Code may activate either one (or both) for the same user request. This produces unpredictable behavior: sometimes the user gets the output from skill A, sometimes from skill B, and the output may differ significantly. Conflict detection catches this before it happens by identifying skill pairs whose descriptions are semantically similar enough to cause activation ambiguity.
The problem is particularly acute in team environments where different people create skills independently. Two developers might each create a "run linting" skill with slightly different descriptions, and neither realizes the conflict until Claude starts producing inconsistent results. Proactive detection turns a runtime surprise into a creation-time warning.
How Detection Works
The ConflictDetector class computes pairwise cosine similarity between all skill description embeddings. The process follows these steps:
- Get the embedding service (HuggingFace transformer or TF-IDF heuristic fallback)
- Batch-embed all skill descriptions into numeric vectors
- Compute cosine similarity for every unique pair (n*(n-1)/2 comparisons)
- Flag pairs scoring above the configured threshold
- Extract overlapping domain-specific terms for each flagged pair
- Assign severity levels (high for 0.90+, medium for threshold to 0.90)
- Sort results by similarity score (highest first)
import { ConflictDetector } from './conflicts/conflict-detector.js';
const detector = new ConflictDetector({ threshold: 0.85 });
const result = await detector.detect([
{ name: 'lint-ts', description: 'Lint TypeScript files with ESLint' },
{ name: 'ts-linter', description: 'Run ESLint on TypeScript source code' },
{ name: 'test-runner', description: 'Execute vitest test suites' },
]);
// result = {
// conflicts: [
// {
// skillA: 'lint-ts',
// skillB: 'ts-linter',
// similarity: 0.92,
// severity: 'high',
// overlappingTerms: ['eslint', 'typescript', 'lint'],
// descriptionA: 'Lint TypeScript files with ESLint',
// descriptionB: 'Run ESLint on TypeScript source code',
// }
// ],
// skillCount: 3,
// pairsAnalyzed: 3,
// threshold: 0.85,
// analysisMethod: 'heuristic'
// }
Overlapping Term Extraction
For each conflict pair, the detector extracts overlapping domain-specific terms by tokenizing both descriptions, filtering out a comprehensive stop word list (150+ common English words), and computing the intersection. The stop words include common verbs, prepositions, articles, and pronouns — words that do not indicate domain overlap. This gives users concrete insight into why two skills were flagged.
Embedding Methods
The system supports two embedding methods, selected automatically:
- model: HuggingFace transformer model for high-accuracy semantic analysis. Best for nuanced similarity where skills use different words for the same concept.
- heuristic: TF-IDF fallback when the model is unavailable. Uses term frequency analysis to estimate similarity. Works well for skills with significant word overlap but may miss synonymy.
The analysisMethod field in the result indicates which method was used, so users can assess confidence accordingly.
Threshold Configuration
The threshold is clamped to the valid range of 0.5 to 0.95 with automatic console warnings when clamping occurs. The default of 0.85 provides a balanced tradeoff between precision and recall:
- 0.5-0.7 (loose): Flags many pairs, including skills with moderate topical overlap. Useful for auditing a large skill library where you want to be thorough.
- 0.7-0.85 (moderate): Catches skills with significant functional overlap while allowing topically related but functionally distinct skills to coexist.
- 0.85-0.95 (strict): Only flags near-duplicate descriptions. Best for established libraries where most overlap has already been resolved.
The clamping prevents misconfiguration — a threshold below 0.5 would flag nearly everything, and above 0.95 would miss nearly everything.
Conflict Resolution Support
The src/conflicts/ directory includes two additional modules beyond the detector that help resolve identified conflicts:
- ConflictFormatter (
conflict-formatter.ts) — Renders detection results as human-readable reports with severity badges, overlapping terms, and resolution suggestions. The formatter produces both terminal-friendly output (with ANSI color codes) and plain-text output for logging. - RewriteSuggester (
rewrite-suggester.ts) — Proposes description rewrites to reduce overlap between conflicting skills while preserving each skill's unique purpose. The suggestions aim to differentiate skills by emphasizing what makes each one distinct rather than what they share.
The typical conflict resolution workflow is: detect conflicts, review the report, then either merge the skills (if they are genuinely duplicates), rewrite their descriptions to differentiate them (if they serve different purposes but were described too similarly), or accept the overlap (if both skills are needed and their triggers differentiate them adequately).
5. Skill Format (SKILL.md)
The skill format defined in src/types/skill.ts follows the official Claude Code SKILL.md specification and extends it with gsd-skill-creator metadata for triggers and learning. The format is designed to be human-readable, version-controllable, and compatible with any tool that understands standard Markdown with YAML frontmatter.
Metadata Interface
The core SkillMetadata interface includes all official Claude Code fields at the root level:
interface SkillMetadata {
// Required
name: string; // max 64 chars, lowercase + hyphens only
description: string; // max 1024 chars, used for auto-triggering
// Official optional fields
'disable-model-invocation'?: boolean; // Prevent Claude from using the model
'user-invocable'?: boolean; // Allow /skill:name invocation
'allowed-tools'?: string[] | string; // Restrict available tools
'argument-hint'?: string; // Hint for user invocation arguments
model?: string; // Model override for execution
context?: 'fork'; // Fork context for isolation
agent?: string; // Agent reference
hooks?: Record<string, unknown>; // Lifecycle hooks configuration
license?: string; // SPDX license identifier
compatibility?: string; // Compatibility notes (max 500 chars)
// Extension container (new format)
metadata?: {
extensions?: {
'gsd-skill-creator'?: GsdSkillCreatorExtension;
[key: string]: unknown; // Preserves other tools' extensions
};
};
}
Extension Data
The GsdSkillCreatorExtension interface holds trigger definitions, learning metadata, versioning, and enable/disable state. In the new format, it lives under metadata.extensions.gsd-skill-creator. For backward compatibility, these fields are also accepted at the root level (legacy format), and the getExtension() accessor reads from either location transparently.
The following YAML example shows a complete skill file with all extension fields populated. In practice, most fields are optional and auto-generated — you only need to provide name, description, and optionally triggers when creating a new skill:
---
name: lint-on-save
description: "Use when editing TypeScript files to run ESLint after save"
allowed-tools:
- Bash
- Read
license: "MIT"
metadata:
extensions:
gsd-skill-creator:
enabled: true
version: 3
createdAt: "2025-06-15T10:00:00.000Z"
updatedAt: "2025-07-20T14:30:00.000Z"
triggers:
files:
- "*.ts"
- "*.tsx"
intents:
- lint
- format
contexts:
- "editing TypeScript"
threshold: 0.7
learning:
applicationCount: 12
feedbackScores: [5, 4, 5, 5]
lastRefined: "2025-07-20T14:30:00.000Z"
corrections:
- timestamp: "2025-07-18T10:00:00Z"
original: "npx eslint --fix ."
corrected: "npx eslint --fix --ext .ts,.tsx ."
context: "Needed to restrict to TypeScript files"
---
# Lint on Save
When a TypeScript file is saved, run ESLint with auto-fix...
Trigger System
The SkillTrigger interface defines three types of matching conditions plus a confidence threshold:
- intents: Regex or keyword patterns matched against the user's natural language request. Multiple intents use OR logic — matching any one intent is sufficient. Regex patterns are compiled with the case-insensitive flag (
i) for flexible matching. - files: Glob patterns matched against the file currently being edited. Supports standard glob syntax:
*.tsfor TypeScript files,src/**/*.tsxfor React components,tests/*.test.tsfor test files. Internally converted to regex for matching. - contexts: Substring patterns matched against the current workflow context (e.g., "in GSD planning phase", "during verification"). Simple contains-check: if the context string includes the pattern, the skill matches.
- threshold: Minimum confidence score (0-1) required for activation. Skills with lower match confidence are skipped. Default is 0.7 if not specified.
Learning Metadata
The SkillLearning interface tracks the skill's evolution over time. This data accumulates across sessions and feeds into the Learning & Refinement system:
- applicationCount: How many times the skill has been applied. Incremented on every activation. Used for popularity-based activation ranking.
- feedbackScores: Array of 1-5 user ratings collected over time. The average score provides a quality signal for activation priority.
- corrections: Array of
SkillCorrectionobjects, each containing the timestamp, original text, corrected text, and optional context. These are the primary input to the Refinement Engine's pattern analysis. - lastRefined: ISO 8601 timestamp of the most recent refinement, used for cooldown enforcement. Must be at least 7 days in the past before a new refinement can be applied.
Complete Skill Representation
The Skill interface ties metadata, body, and file path together:
interface Skill {
metadata: SkillMetadata;
body: string; // Markdown content (without frontmatter)
path: string; // Filesystem path to SKILL.md
}
This is the return type of all SkillStore read and create operations, and the input type for downstream systems like the activation pipeline and learning loop. The path field is always an absolute path to the SKILL.md file, enabling downstream systems to locate related resources (references, scripts) by path manipulation without needing to know the skills directory configuration.
Usage Examples
Example 1: Listing All Skills via CLI
npx skill-creator list
Expected output:
Skills (project: .claude/skills/)
=================================
beautiful-commits v3 enabled "Crafts professional git commit messages..."
lint-on-save v1 enabled "Use when editing TypeScript files..."
test-runner v2 enabled "Use when running tests to execute vitest..."
Skills (user: ~/.claude/skills/)
================================
git-workflow v1 enabled "Use when managing Git branches..."
Total: 4 skills (3 project, 1 user), 0 conflicts
Example 2: Creating a Skill via CLI
npx skill-creator create \
--name "test-runner" \
--description "Use when running tests to execute vitest with coverage" \
--allowed-tools "Bash Read Grep" \
--triggers-intents "test,run tests,vitest" \
--triggers-files "*.test.ts,*.spec.ts"
Expected output:
Created skill: test-runner
Path: .claude/skills/test-runner/SKILL.md
Version: 1
Budget: 423 / 12,000 chars (3.5%)
Description quality: good (activation triggers detected)
Example 3: Checking for Conflicts Before Creation
npx skill-creator conflicts
Expected output when conflicts exist:
Conflict Detection Report
========================
Skills analyzed: 8
Pairs compared: 28
Threshold: 0.85
Method: heuristic
Found 1 conflict:
HIGH lint-ts <-> ts-linter (similarity: 0.92)
Overlapping terms: eslint, typescript, lint
Consider merging these skills or differentiating descriptions.
No medium-severity conflicts found.
Example 4: Programmatic Skill Index Search
import { SkillStore } from 'gsd-skill-creator/storage/skill-store';
import { SkillIndex } from 'gsd-skill-creator/storage/skill-index';
const store = new SkillStore('.claude/skills');
const index = new SkillIndex(store, '.claude/skills');
// Find skills relevant to the current file
const matches = await index.findByTrigger(
undefined, // no intent filter
'src/components/Button.tsx', // file being edited
undefined // no context filter
);
console.log(`Found ${matches.length} skills for Button.tsx`);
for (const match of matches) {
console.log(` ${match.name}: ${match.description}`);
}
// Search by keyword
const lintSkills = await index.search('lint');
console.log(`Found ${lintSkills.length} skills matching "lint"`);
// Cross-scope listing
const { listAllScopes } = await import('gsd-skill-creator/storage/skill-index');
const allScopes = await listAllScopes();
const conflicts = allScopes.filter(s => s.hasConflict);
if (conflicts.length > 0) {
console.warn(`${conflicts.length} cross-scope name conflicts detected`);
}
Architecture Decisions
Subdirectory storage format: Each skill gets its own directory rather than a single flat file. This enables progressive disclosure (large skills can split content into references and scripts), atomic operations (you can lock at the directory level), and clean deletion (removing the directory removes all artifacts). The tradeoff is slightly more filesystem overhead, but the benefits for large skill libraries are substantial.
Gray-matter + Zod two-layer validation: Frontmatter is parsed with gray-matter (which uses js-yaml safeLoad internally) and then validated with Zod schemas. This two-layer approach catches both YAML syntax errors and semantic validation failures, with descriptive error messages at each stage. The use of .passthrough() on Zod schemas ensures forward compatibility — unknown fields from other tools are preserved, not rejected.
Dual-format metadata migration: The system supports both the legacy format (gsd-skill-creator fields at root level) and the new format (fields under metadata.extensions.gsd-skill-creator). The getExtension() accessor reads from either location transparently, and all write operations normalize to the new format. This provides zero-downtime migration for existing skill libraries — every read is format-agnostic, every write is format-canonical.
Embedding-based conflict detection: The conflict detector uses semantic embeddings rather than keyword overlap. This catches conflicts between skills that use different terminology for the same concept (e.g., "lint" vs. "static analysis") that keyword matching would miss. The fallback to TF-IDF heuristics ensures the system works even without access to ML models.
Extension Points
- Custom SkillStore path: Pass any directory to the constructor to manage skills in non-default locations.
- Progressive disclosure: Use
createWithDisclosure()for large skills; it automatically decomposes content into compact SKILL.md with reference files. - Index events: The index entry includes
events.emitsandevents.listensfields for event-driven skill activation patterns. - Schema extension: The
.passthrough()modifier on Zod schemas preserves unknown fields, so third-party tools can add their own metadata without breaking validation. - Custom conflict threshold: Pass a
ConflictConfigto the detector to tune sensitivity per project. - Custom validators: The
ReservedNameValidatoraccepts custom reserved name lists for project-specific exclusions.
Storage and Backup
Skills are plain Markdown files in Git-tracked directories. Standard Git backup strategies (remote mirrors, scheduled pushes) cover skill data. The index file (.skill-index.json) is a regenerable cache and does not need to be backed up — it rebuilds automatically on next access if missing. User-level skills in ~/.claude/skills/ live outside project Git repositories and need separate backup procedures.
For user-level skills, consider symlinking the directory to a dotfiles repository or backing up with a periodic cron job:
# Backup user-level skills to a dedicated repo
rsync -av ~/.claude/skills/ ~/dotfiles/claude-skills/
Monitoring
- Skill count:
ls -d .claude/skills/*/SKILL.md 2>/dev/null | wc -l - Index freshness: Compare
.skill-index.jsonmodification time against the newest skill file. - Legacy skills remaining: Check for
.mdfiles directly in.claude/skills/(not in subdirectories) — these indicate unmigrated legacy skills. - Budget usage: The CLI
statuscommand reports aggregate character usage against the token budget ceiling. - Cross-scope conflicts: Run
listAllScopes()periodically to check for user vs. project skill name collisions.
Maintenance Tasks
- Index rebuild: If the index becomes stale or corrupt, delete
.skill-index.jsonand it will rebuild on next access. - Legacy migration: Run the CLI migration command to convert flat-file skills to the subdirectory format. Use
hasLegacySkills()to check if migration is needed. - Disk usage: Large skill libraries with progressive disclosure generate additional files in
references/andscripts/. These are human-readable and safe to inspect. - Periodic conflict check: Schedule regular conflict detection runs to catch gradually-accumulating overlap as new skills are added.
# Check for legacy skills that need migration
find .claude/skills -maxdepth 1 -name '*.md' -not -name '.*' | head -5
# List all skills with their sizes
for dir in .claude/skills/*/; do
name=$(basename "$dir")
size=$(du -sh "$dir" 2>/dev/null | cut -f1)
echo "$name: $size"
done
# Verify index is recent (rebuild if older than skills)
stat -c '%Y %n' .claude/skills/.skill-index.json .claude/skills/*/SKILL.md | sort -rn | head -3
Troubleshooting
Skill creation fails with "Invalid skill name"
Cause: The name contains uppercase letters, special characters, starts/ends with a hyphen, or contains consecutive hyphens.
Solution: Follow the naming rules: lowercase letters, numbers, and single hyphens only. Check the error message for an auto-suggested fix. The suggestion function handles common mistakes like spaces (replaced with hyphens), uppercase (lowercased), and trailing hyphens (trimmed).
Skill creation fails with "Name is reserved"
Cause: The chosen name conflicts with a Claude Code built-in command or a system command.
Solution: Use one of the alternative names suggested in the error message. Common reserved names include help, status, init, and config. Prefix with your project name for disambiguation: myapp-status instead of status.
Skill creation fails with "exceeds character budget"
Cause: The skill content (frontmatter + body) exceeds the per-skill character budget enforced by Claude Code.
Solution: Use createWithDisclosure() instead of create() to automatically decompose large skills into compact main files with reference documents. Alternatively, shorten the body content or split into multiple focused skills.
Index search returns stale results
Cause: The .skill-index.json cache has not been refreshed since skills were modified directly on disk (outside of SkillStore operations).
Solution: Call index.refresh() or index.rebuild() to re-scan the skills directory. You can also delete .skill-index.json and it will rebuild automatically on next access.
Conflict detection reports no conflicts despite similar skills
Cause: The similarity threshold may be too high, or the embedding method (heuristic vs. model) may not capture the semantic overlap.
Solution: Lower the threshold (e.g., from 0.85 to 0.75) and re-run. Check the analysisMethod field — if it shows "heuristic", the TF-IDF fallback may miss nuanced similarity that a transformer model would catch. Consider using different wording to make descriptions more explicitly similar if you expect them to conflict.
PathTraversalError when reading or creating skills
Cause: The skill name contains path traversal sequences (../), path separators (/, \), null bytes, or the resolved path escapes the configured skills directory.
Solution: Use only valid skill names (lowercase, numbers, hyphens). This error is a defense-in-depth safety measure. If you encounter it during normal usage, verify that the skills directory configuration points to the correct absolute path.

