We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide25 min read

Agent Skills, the open SKILL.md standard for agents

Agent Skills is an open format for packaging knowledge for AI agents. The SKILL.md file, progressive disclosure, tool support, and common mistakes.

Agent Skills, the open SKILL.md standard for agents

Agent Skills is an open format for packaging procedural knowledge for AI agents. A single skill is a directory holding a SKILL.md file describing how to carry out a particular task, and the agent loads that description only when it is needed. Anthropic developed the format and released it as an open standard, subsequently adopted by dozens of competing tools.

Where the standard came from and why it won

The specification was published on 18 December 2025, and the pace of adoption was unusual even for this industry. Within months the same SKILL.md file in the same directory was being read by tools from companies competing with each other: Claude Code, VS Code with Copilot, OpenAI's Codex, Cursor, Google's Gemini CLI, JetBrains' Junie, Amazon's Kiro, and goose.

The reason for that agreement is more pragmatic than idealistic. Each of those tools had its own way of accepting instructions from a user, so a team working across three different agents maintained three versions of the same knowledge. A shared format removes that without handing control to anyone, since it is an ordinary directory holding a text file rather than a service you connect to.

The standard is stewarded by the Agentic AI Foundation at the Linux Foundation, the same body that received the Model Context Protocol. For a team weighing an investment in writing skills that matters: the format does not depend on one company's priorities.

What a skill looks like

A skill is a directory in which only one file is mandatory.

Code
TEXT
my-skill/
β”œβ”€β”€ SKILL.md          # required: metadata and instructions
β”œβ”€β”€ scripts/          # optional: executable code
β”œβ”€β”€ references/       # optional: documentation
└── assets/           # optional: templates and resources

The file itself opens with metadata, where the minimum is a name and a description, followed by plain text instructions.

Code
Markdown
---
name: weekly-report
description: Generates a weekly report from support tickets. Use when someone asks for a summary of the week or a ticket breakdown.
---

# Weekly report

1. Fetch the last seven days of tickets using `scripts/fetch.py`.
2. Group them by the categories in `references/categories.md`.
3. Assemble the report using the `assets/template.md` layout.
4. Do not add conclusions the data does not support.

The description field matters more than it looks, and I return to it under common mistakes. It is the only thing the agent sees at startup, so how it is phrased decides whether the skill gets used at all.

Progressive disclosure, the heart of the idea

The mechanism the whole format rests on is called progressive disclosure and works in three stages.

At startup the agent loads only the name and description of each available skill, a few dozen words apiece. That is enough to know when a skill might apply, and it costs so little that you can keep dozens on hand.

When a task matches a description, the agent loads the file's full contents into context. Only at that point do you pay for longer instructions, and only when they are needed.

In the third stage the agent follows those instructions, reaching for bundled scripts and supporting files as required. A ten thousand word reference document costs nothing until an instruction tells the agent to open it.

That construction solves a problem which previously had no good answer. Dumping a team's entire knowledge into one instructions file works with one page and stops working at twenty, because all of it enters context on every task and crowds out what actually matters. Skills invert that: the knowledge can be arbitrarily large, since only the matching part loads.

Skills against MCP, two different layers

This distinction generates the most confusion, so it is worth stating plainly.

The Model Context Protocol gives an agent capabilities: access to a database, a repository, a browser. It answers what the agent can do. Skills give an agent knowledge: how to perform a specific task in your organisation, in what order, what to avoid. They answer how to do it well.

It follows that they do not compete but complement each other, and most often appear together. A skill describing a deployment process uses an MCP server providing access to the deployment system, and without it would be instructions without tools. Conversely, an MCP server alone provides access with no knowledge of how to use it according to your rules. There is more on the protocol itself in the MCP toolkit writeup.

A practical selection rule: if the agent lacks access to something, you need an MCP server. If the agent has access but does things contrary to your process, you need a skill.

What are Agent Skills?

Agent Skills are patterns, tools, and capabilities that allow AI agents to perform specialized tasks beyond text generation. When a language model (LLM) has access to skills/tools, it becomes a true agent - it can interact with the external world: scrape websites, execute database queries, manipulate files, send emails, and integrate with any API.

The concept of skills/tools is the foundation of modern AI systems and appears under different names depending on the platform:

  • Tool Use (Anthropic Claude)
  • Function Calling (OpenAI)
  • Tools (LangChain)
  • MCP (Model Context Protocol - Anthropic)
  • Actions (GPTs)
  • Skills (Custom agents)

Evolution from chatbots to agents

Traditional chatbots could only generate text based on a prompt. Modern AI agents can:

  1. Analyze - Understand the user's task
  2. Plan - Decide which tools to use
  3. Execute - Call the appropriate skills/tools
  4. Iterate - Use results for further actions
  5. Report - Present the final result

This "ReAct" (Reasoning + Acting) model allows agents to solve complex, multi-step problems.

System architecture with skills

Code
TEXT
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      User Request                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     AI Agent (LLM)                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  System Prompt + Context + Conversation History    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Available Tools/Skills (descriptions + schemas)   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό (Tool Call)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Skills Registry                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚Web Scraperβ”‚ β”‚ Database β”‚ β”‚ FileSystemβ”‚ β”‚   API    β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό (Tool Result)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                AI Agent (continues reasoning)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Final Response                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why are Skills important?

1. Breaking LLM limitations

Language models on their own:

  • Have no access to the internet in real time
  • Cannot execute code
  • Do not know current data (knowledge cutoff)
  • Cannot modify external systems

Skills solve these limitations by giving the LLM "hands" to act in the world.

2. Specialization and modularity

Instead of trying to build a single "super-model" that can do everything, skills allow you to:

  • Add specialized capabilities in a modular way
  • Update individual functions without changing the entire system
  • Combine different skills depending on needs
  • Test each skill independently

3. Security and control

Skills act as a controlled interface between AI and the world:

  • You can limit what the agent can do
  • You can log and audit all actions
  • You can add rate limiting and sandboxing
  • You can require approval for dangerous operations

4. Reducing hallucinations

When an LLM has access to real data through skills:

  • It does not have to "guess" information
  • It can verify facts before responding
  • Responses are based on current data
  • Lower risk of confabulation

Anatomy of a Skill

Basic skill structure

Code
TypeScript
interface Skill {
  // Identifier used by LLM for invocation
  name: string

  // Description for LLM - what this skill does
  description: string

  // Parameter schema (JSON Schema or Zod)
  inputSchema: JSONSchema | ZodSchema

  // Optional output schema
  outputSchema?: JSONSchema | ZodSchema

  // Main execution logic
  execute: (params: unknown) => Promise<unknown>

  // Optional metadata
  metadata?: {
    category?: string
    requiresAuth?: boolean
    rateLimit?: number
    timeout?: number
  }
}

Complete skill example

Code
TypeScript
import { z } from 'zod'

// Parameter schema with Zod
const weatherInputSchema = z.object({
  city: z.string().describe('City name'),
  units: z.enum(['metric', 'imperial']).default('metric').describe('Temperature units'),
  lang: z.string().default('en').describe('Response language')
})

// Output schema
const weatherOutputSchema = z.object({
  temperature: z.number(),
  description: z.string(),
  humidity: z.number(),
  wind_speed: z.number()
})

// Skill definition
export const weatherSkill: Skill = {
  name: 'get_weather',
  description: `Fetches the current weather for a given city.
    Use this tool when the user asks about the weather, temperature,
    whether it will rain, or what atmospheric conditions are like.`,

  inputSchema: weatherInputSchema,
  outputSchema: weatherOutputSchema,

  metadata: {
    category: 'information',
    requiresAuth: true,  // Requires API key
    rateLimit: 60,       // Max 60 requests/minute
    timeout: 5000        // 5 second timeout
  },

  async execute(params) {
    const { city, units, lang } = weatherInputSchema.parse(params)

    const apiKey = process.env.OPENWEATHER_API_KEY
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&lang=${lang}&appid=${apiKey}`

    const response = await fetch(url)

    if (!response.ok) {
      throw new Error(`Weather API error: ${response.status}`)
    }

    const data = await response.json()

    return weatherOutputSchema.parse({
      temperature: data.main.temp,
      description: data.weather[0].description,
      humidity: data.main.humidity,
      wind_speed: data.wind.speed
    })
  }
}

Popular skill categories

1. Web & Internet Skills

Code
TypeScript
export const webScraperSkill = {
  name: 'web_scrape',
  description: 'Fetches and parses content from a web page',

  inputSchema: z.object({
    url: z.string().url(),
    selector: z.string().optional().describe('CSS selector for specific elements'),
    format: z.enum(['text', 'html', 'markdown']).default('text')
  }),

  async execute({ url, selector, format }) {
    const response = await fetch(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; Bot/1.0)'
      }
    })

    const html = await response.text()

    const dom = new JSDOM(html)
    const document = dom.window.document

    let content: string

    if (selector) {
      const elements = document.querySelectorAll(selector)
      content = Array.from(elements).map(el => el.textContent).join('\n')
    } else {
      const article = document.querySelector('article, main, .content')
      content = article?.textContent || document.body.textContent || ''
    }

    if (format === 'markdown') {
      return turndownService.turndown(content)
    }

    return content.trim()
  }
}

export const searchSkill = {
  name: 'web_search',
  description: 'Searches the internet using DuckDuckGo/Serper',

  inputSchema: z.object({
    query: z.string(),
    num_results: z.number().min(1).max(10).default(5)
  }),

  async execute({ query, num_results }) {
    const response = await fetch('https://google.serper.dev/search', {
      method: 'POST',
      headers: {
        'X-API-KEY': process.env.SERPER_API_KEY!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ q: query, num: num_results })
    })

    const data = await response.json()

    return data.organic.map((result: any) => ({
      title: result.title,
      link: result.link,
      snippet: result.snippet
    }))
  }
}

2. Database Skills

Code
TypeScript
export const databaseQuerySkill = {
  name: 'database_query',
  description: `Executes a safe SQL query against the database.
    ONLY SELECT queries are allowed. Use parameters for values.`,

  inputSchema: z.object({
    query: z.string().describe('SQL SELECT query'),
    params: z.array(z.any()).default([]).describe('Query parameters')
  }),

  async execute({ query, params }) {
    const normalizedQuery = query.trim().toLowerCase()
    if (!normalizedQuery.startsWith('select')) {
      throw new Error('Only SELECT queries are allowed')
    }

    const forbidden = ['drop', 'delete', 'update', 'insert', 'alter', 'create', 'truncate']
    for (const word of forbidden) {
      if (normalizedQuery.includes(word)) {
        throw new Error(`Forbidden keyword: ${word}`)
      }
    }

    const result = await db.query(query, params)

    return result.rows.slice(0, 100)
  }
}

export const prismaSkill = {
  name: 'prisma_query',
  description: 'Executes a type-safe query through Prisma ORM',

  inputSchema: z.object({
    model: z.enum(['User', 'Post', 'Comment', 'Product']),
    operation: z.enum(['findMany', 'findFirst', 'count']),
    where: z.record(z.any()).optional(),
    select: z.array(z.string()).optional(),
    take: z.number().max(100).optional(),
    skip: z.number().optional(),
    orderBy: z.record(z.enum(['asc', 'desc'])).optional()
  }),

  async execute({ model, operation, where, select, take, skip, orderBy }) {
    const prismaModel = prisma[model.toLowerCase() as keyof typeof prisma]

    const query: any = {
      where,
      take: take || 50,
      skip
    }

    if (select) {
      query.select = Object.fromEntries(select.map(s => [s, true]))
    }

    if (orderBy) {
      query.orderBy = orderBy
    }

    // @ts-ignore - dynamic model access
    return await prismaModel[operation](query)
  }
}

3. File System Skills

Code
TypeScript
export const fileSystemSkill = {
  name: 'filesystem',
  description: 'File system operations in a sandboxed directory',

  inputSchema: z.object({
    action: z.enum(['read', 'write', 'list', 'exists', 'mkdir', 'delete']),
    path: z.string(),
    content: z.string().optional()
  }),

  metadata: {
    sandboxPath: '/workspace',
    maxFileSize: 10 * 1024 * 1024
  },

  async execute({ action, path, content }) {
    const sandboxPath = '/workspace'
    const fullPath = join(sandboxPath, path)
    const normalizedPath = normalize(fullPath)

    if (!normalizedPath.startsWith(sandboxPath)) {
      throw new Error('Path traversal attack detected')
    }

    switch (action) {
      case 'read':
        const fileContent = await fs.readFile(normalizedPath, 'utf-8')
        return { content: fileContent.slice(0, 100000) }

      case 'write':
        if (!content) throw new Error('Content required for write')
        if (content.length > 10 * 1024 * 1024) throw new Error('File too large')
        await fs.writeFile(normalizedPath, content)
        return { success: true, path: normalizedPath }

      case 'list':
        const entries = await fs.readdir(normalizedPath, { withFileTypes: true })
        return entries.map(e => ({
          name: e.name,
          type: e.isDirectory() ? 'directory' : 'file'
        }))

      case 'exists':
        try {
          await fs.access(normalizedPath)
          return { exists: true }
        } catch {
          return { exists: false }
        }

      case 'mkdir':
        await fs.mkdir(normalizedPath, { recursive: true })
        return { success: true }

      case 'delete':
        await fs.unlink(normalizedPath)
        return { success: true }
    }
  }
}

4. Code Execution Skills

Code
TypeScript
export const codeExecutionSkill = {
  name: 'execute_code',
  description: 'Executes JavaScript code in a secure sandbox',

  inputSchema: z.object({
    code: z.string(),
    language: z.enum(['javascript', 'typescript']).default('javascript'),
    timeout: z.number().max(30000).default(5000)
  }),

  async execute({ code, language, timeout }) {
    const vm = new NodeVM({
      timeout,
      sandbox: {
        console: {
          log: (...args: any[]) => logs.push(args.join(' ')),
          error: (...args: any[]) => errors.push(args.join(' '))
        }
      },
      require: {
        external: false,
        builtin: ['util', 'path']
      }
    })

    const logs: string[] = []
    const errors: string[] = []

    try {
      let executableCode = code
      if (language === 'typescript') {
        const result = ts.transpileModule(code, {
          compilerOptions: { module: ts.ModuleKind.CommonJS }
        })
        executableCode = result.outputText
      }

      const result = vm.run(executableCode)

      return {
        success: true,
        result,
        logs,
        errors
      }
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
        logs,
        errors
      }
    }
  }
}

export const pythonSkill = {
  name: 'execute_python',
  description: 'Executes Python code',

  inputSchema: z.object({
    code: z.string(),
    timeout: z.number().max(60000).default(10000)
  }),

  async execute({ code, timeout }) {
    return new Promise((resolve, reject) => {
      const python = spawn('python3', ['-c', code], {
        timeout,
        env: {
          ...process.env,
          PYTHONDONTWRITEBYTECODE: '1'
        }
      })

      let stdout = ''
      let stderr = ''

      python.stdout.on('data', (data) => { stdout += data })
      python.stderr.on('data', (data) => { stderr += data })

      python.on('close', (exitCode) => {
        resolve({
          success: exitCode === 0,
          stdout: stdout.trim(),
          stderr: stderr.trim(),
          exitCode
        })
      })

      python.on('error', (err) => {
        reject(new Error(`Python execution failed: ${err.message}`))
      })
    })
  }
}

5. API Integration Skills

Code
TypeScript
export const apiCallerSkill = {
  name: 'api_call',
  description: 'Executes an HTTP request to an external API',

  inputSchema: z.object({
    url: z.string().url(),
    method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('GET'),
    headers: z.record(z.string()).optional(),
    body: z.any().optional(),
    timeout: z.number().max(30000).default(10000)
  }),

  async execute({ url, method, headers, body, timeout }) {
    const allowedDomains = [
      'api.openai.com',
      'api.anthropic.com',
      'api.github.com',
      'api.stripe.com'
    ]

    const urlObj = new URL(url)
    if (!allowedDomains.some(d => urlObj.hostname.endsWith(d))) {
      throw new Error(`Domain not allowed: ${urlObj.hostname}`)
    }

    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), timeout)

    try {
      const response = await fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...headers
        },
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal
      })

      const contentType = response.headers.get('content-type')
      const data = contentType?.includes('application/json')
        ? await response.json()
        : await response.text()

      return {
        status: response.status,
        statusText: response.statusText,
        data
      }
    } finally {
      clearTimeout(timeoutId)
    }
  }
}

export const githubSkill = {
  name: 'github',
  description: 'Interaction with GitHub API - repos, issues, PRs',

  inputSchema: z.object({
    action: z.enum([
      'get_repo',
      'list_issues',
      'create_issue',
      'get_pr',
      'list_prs',
      'get_file'
    ]),
    owner: z.string(),
    repo: z.string(),
    params: z.record(z.any()).optional()
  }),

  async execute({ action, owner, repo, params }) {
    const octokit = new Octokit({
      auth: process.env.GITHUB_TOKEN
    })

    switch (action) {
      case 'get_repo':
        return await octokit.repos.get({ owner, repo })

      case 'list_issues':
        return await octokit.issues.listForRepo({
          owner,
          repo,
          state: params?.state || 'open',
          per_page: params?.limit || 10
        })

      case 'create_issue':
        return await octokit.issues.create({
          owner,
          repo,
          title: params?.title,
          body: params?.body,
          labels: params?.labels
        })

      case 'list_prs':
        return await octokit.pulls.list({
          owner,
          repo,
          state: params?.state || 'open',
          per_page: params?.limit || 10
        })

      case 'get_file':
        const content = await octokit.repos.getContent({
          owner,
          repo,
          path: params?.path
        })
        if ('content' in content.data) {
          return {
            ...content.data,
            decoded: Buffer.from(content.data.content, 'base64').toString()
          }
        }
        return content.data
    }
  }
}

6. Communication Skills

Code
TypeScript
export const emailSkill = {
  name: 'send_email',
  description: 'Sends an email via Resend/SendGrid',

  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string().max(200),
    body: z.string().max(10000),
    html: z.boolean().default(false)
  }),

  metadata: {
    requiresApproval: true,
    rateLimit: 10
  },

  async execute({ to, subject, body, html }) {
    const resend = new Resend(process.env.RESEND_API_KEY)

    const result = await resend.emails.send({
      from: 'assistant@example.com',
      to,
      subject,
      [html ? 'html' : 'text']: body
    })

    return { success: true, id: result.id }
  }
}

export const slackSkill = {
  name: 'slack_message',
  description: 'Sends a message on Slack',

  inputSchema: z.object({
    channel: z.string(),
    message: z.string(),
    thread_ts: z.string().optional()
  }),

  async execute({ channel, message, thread_ts }) {
    const slack = new WebClient(process.env.SLACK_BOT_TOKEN)

    const result = await slack.chat.postMessage({
      channel,
      text: message,
      thread_ts
    })

    return {
      success: true,
      ts: result.ts,
      channel: result.channel
    }
  }
}

Skills Registry

Registry implementation

Code
TypeScript
import { Skill } from './types'

class SkillsRegistry {
  private skills: Map<string, Skill> = new Map()
  private categories: Map<string, Skill[]> = new Map()

  register(skill: Skill) {
    this.validateSkill(skill)

    this.skills.set(skill.name, skill)

    const category = skill.metadata?.category || 'general'
    if (!this.categories.has(category)) {
      this.categories.set(category, [])
    }
    this.categories.get(category)!.push(skill)

    console.log(`Registered skill: ${skill.name}`)
  }

  private validateSkill(skill: Skill) {
    if (!skill.name || typeof skill.name !== 'string') {
      throw new Error('Skill must have a name')
    }
    if (!skill.description || typeof skill.description !== 'string') {
      throw new Error('Skill must have a description')
    }
    if (typeof skill.execute !== 'function') {
      throw new Error('Skill must have an execute function')
    }
    if (this.skills.has(skill.name)) {
      throw new Error(`Skill ${skill.name} already registered`)
    }
  }

  get(name: string): Skill | undefined {
    return this.skills.get(name)
  }

  getAll(): Skill[] {
    return Array.from(this.skills.values())
  }

  getByCategory(category: string): Skill[] {
    return this.categories.get(category) || []
  }

  toToolDefinitions() {
    return this.getAll().map(skill => ({
      name: skill.name,
      description: skill.description,
      input_schema: this.zodToJsonSchema(skill.inputSchema)
    }))
  }

  private zodToJsonSchema(schema: z.ZodSchema) {
    return zodToJsonSchema(schema)
  }

  async execute(name: string, params: unknown) {
    const skill = this.get(name)
    if (!skill) {
      throw new Error(`Skill not found: ${name}`)
    }

    if (skill.metadata?.rateLimit) {
      await this.checkRateLimit(name, skill.metadata.rateLimit)
    }

    const timeout = skill.metadata?.timeout || 30000
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error(`Skill ${name} timed out`)), timeout)
    })

    try {
      const result = await Promise.race([
        skill.execute(params),
        timeoutPromise
      ])

      this.logExecution(name, params, result, true)

      return result
    } catch (error) {
      this.logExecution(name, params, error, false)
      throw error
    }
  }

  private async checkRateLimit(skillName: string, limit: number) {
    const key = `ratelimit:${skillName}`
    const count = await redis.incr(key)

    if (count === 1) {
      await redis.expire(key, 60)
    }

    if (count > limit) {
      throw new Error(`Rate limit exceeded for skill: ${skillName}`)
    }
  }

  private logExecution(name: string, params: unknown, result: unknown, success: boolean) {
    console.log(JSON.stringify({
      timestamp: new Date().toISOString(),
      skill: name,
      params: this.sanitizeForLog(params),
      success,
      result: success ? this.sanitizeForLog(result) : result
    }))
  }

  private sanitizeForLog(data: unknown): unknown {
    if (typeof data === 'object' && data !== null) {
      const sanitized = { ...data as object }
      const sensitiveKeys = ['password', 'token', 'apiKey', 'secret']
      for (const key of sensitiveKeys) {
        if (key in sanitized) {
          (sanitized as any)[key] = '[REDACTED]'
        }
      }
      return sanitized
    }
    return data
  }
}

export const skillsRegistry = new SkillsRegistry()

import { webScraperSkill } from './skills/web-scraper'
import { databaseQuerySkill } from './skills/database'
import { fileSystemSkill } from './skills/filesystem'
import { weatherSkill } from './skills/weather'
import { githubSkill } from './skills/github'

skillsRegistry.register(webScraperSkill)
skillsRegistry.register(databaseQuerySkill)
skillsRegistry.register(fileSystemSkill)
skillsRegistry.register(weatherSkill)
skillsRegistry.register(githubSkill)

Integration with Claude

Tool Use API

Code
TypeScript
import Anthropic from '@anthropic-ai/sdk'
import { skillsRegistry } from './skills/registry'

const anthropic = new Anthropic()

async function runAgentWithTools(userMessage: string) {
  const tools = skillsRegistry.toToolDefinitions()

  let response = await anthropic.messages.create({
    model: 'claude-opus-5',
    max_tokens: 4096,
    system: `You are a helpful assistant with access to tools.
             Use tools when you need current information
             or to perform an action. Respond in English.`,
    tools,
    messages: [{ role: 'user', content: userMessage }]
  })

  while (response.stop_reason === 'tool_use') {
    const toolUseBlocks = response.content.filter(
      block => block.type === 'tool_use'
    )

    const toolResults = await Promise.all(
      toolUseBlocks.map(async (block) => {
        if (block.type !== 'tool_use') return null

        try {
          const result = await skillsRegistry.execute(block.name, block.input)
          return {
            type: 'tool_result' as const,
            tool_use_id: block.id,
            content: JSON.stringify(result)
          }
        } catch (error) {
          return {
            type: 'tool_result' as const,
            tool_use_id: block.id,
            content: JSON.stringify({
              error: error instanceof Error ? error.message : 'Unknown error'
            }),
            is_error: true
          }
        }
      })
    )

    response = await anthropic.messages.create({
      model: 'claude-opus-5',
      max_tokens: 4096,
      system: `You are a helpful assistant with access to tools.`,
      tools,
      messages: [
        { role: 'user', content: userMessage },
        { role: 'assistant', content: response.content },
        { role: 'user', content: toolResults.filter(Boolean) as any }
      ]
    })
  }

  const textBlock = response.content.find(block => block.type === 'text')
  return textBlock?.type === 'text' ? textBlock.text : ''
}

const answer = await runAgentWithTools(
  'What is the weather in Warsaw and how many open issues do I have on the example/test repo?'
)

MCP (Model Context Protocol)

MCP is a newer standard from Anthropic for tool integration:

Code
TypeScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

const server = new Server(
  { name: 'my-skills-server', version: '1.0.0' },
  { capabilities: { tools: {} } }
)

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'get_weather',
        description: 'Fetches the current weather for a city',
        inputSchema: {
          type: 'object',
          properties: {
            city: { type: 'string', description: 'City name' }
          },
          required: ['city']
        }
      }
    ]
  }
})

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params

  switch (name) {
    case 'get_weather':
      const weather = await getWeather(args.city)
      return {
        content: [{ type: 'text', text: JSON.stringify(weather) }]
      }

    default:
      throw new Error(`Unknown tool: ${name}`)
  }
})

const transport = new StdioServerTransport()
await server.connect(transport)

Best practices

1. Security first

Code
TypeScript
async execute(params) {
  const validated = inputSchema.parse(params)

  validated.query = sanitizeHtml(validated.query)

  if (!userHasPermission('skill:execute')) {
    throw new Error('Permission denied')
  }

  // ...
}

2. Error handling

Code
TypeScript
async execute(params) {
  try {
    const result = await someOperation()
    return { success: true, data: result }
  } catch (error) {
    console.error('Skill error:', error)

    return {
      success: false,
      error: error instanceof Error
        ? error.message
        : 'An error occurred'
    }
  }
}

3. Idempotency

Code
TypeScript
async execute({ action, data, idempotencyKey }) {
  const existing = await cache.get(`idempotent:${idempotencyKey}`)
  if (existing) {
    return existing
  }

  const result = await performAction(data)

  await cache.set(`idempotent:${idempotencyKey}`, result, 3600)

  return result
}

4. Logging & monitoring

Code
TypeScript
const executeWithLogging = async (skill: Skill, params: unknown) => {
  const startTime = Date.now()
  const requestId = crypto.randomUUID()

  console.log(JSON.stringify({
    event: 'skill_start',
    requestId,
    skill: skill.name,
    params: sanitize(params),
    timestamp: new Date().toISOString()
  }))

  try {
    const result = await skill.execute(params)

    console.log(JSON.stringify({
      event: 'skill_success',
      requestId,
      skill: skill.name,
      duration: Date.now() - startTime,
      resultSize: JSON.stringify(result).length
    }))

    return result
  } catch (error) {
    console.log(JSON.stringify({
      event: 'skill_error',
      requestId,
      skill: skill.name,
      duration: Date.now() - startTime,
      error: error instanceof Error ? error.message : 'Unknown'
    }))

    throw error
  }
}

5. Rate limiting & throttling

Code
TypeScript
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1m'),
})

async function executeWithRateLimit(skillName: string, userId: string) {
  const { success, limit, remaining, reset } = await ratelimit.limit(
    `${skillName}:${userId}`
  )

  if (!success) {
    throw new Error(`Rate limit exceeded. Try again in ${reset}ms`)
  }

  // Execute skill...
}

Testing skills

Code
TypeScript
import { describe, it, expect, vi } from 'vitest'
import { weatherSkill } from './skills/weather'

describe('weatherSkill', () => {
  it('should return weather data for valid city', async () => {
    vi.spyOn(global, 'fetch').mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        main: { temp: 20, humidity: 65 },
        weather: [{ description: 'sunny' }],
        wind: { speed: 5 }
      })
    } as Response)

    const result = await weatherSkill.execute({ city: 'Warsaw' })

    expect(result).toEqual({
      temperature: 20,
      description: 'sunny',
      humidity: 65,
      wind_speed: 5
    })
  })

  it('should throw error for invalid city', async () => {
    vi.spyOn(global, 'fetch').mockResolvedValueOnce({
      ok: false,
      status: 404
    } as Response)

    await expect(
      weatherSkill.execute({ city: 'NonExistentCity12345' })
    ).rejects.toThrow('Weather API error: 404')
  })

  it('should validate input schema', async () => {
    await expect(
      weatherSkill.execute({ city: 123 })
    ).rejects.toThrow()
  })
})

Pricing

ComponentCost
Skill developmentDeveloper time
Claude API (tool use)Standard API rates
MCP Server hostingDepends on infrastructure
Third-party APIsDepends on provider

Skills are code - there are no additional fees for using them. The costs are:

  • API calls to LLM (Claude, OpenAI)
  • Infrastructure hosting (servers, databases)
  • Third-party services (weather API, GitHub, etc.)

FAQ - frequently asked questions

How many skills can an agent have?

The format itself sets no limit, and progressive disclosure means only names and descriptions load at startup. The limits come from elsewhere:

  • every description occupies fixed space in the context on every request, so several dozen skills is already noticeable overhead;
  • the more similar the descriptions, the more often the agent reaches for the wrong one;
  • individual products impose their own caps, for instance an agent in Claude Managed Agents accepts at most twenty skills.

Two things are worth separating here, because they get conflated constantly. Skill count is not tool count. For very large tool sets there is a separate search mechanism that pulls tool definitions in on demand instead of holding them all in context. The practical limit is therefore qualitative: a dozen or so cleanly separated skills work better than a hundred overlapping ones.

How to choose between Tool Use and MCP?

  • Tool Use API: Simpler, direct integration, less setup
  • MCP: Standard protocol, better tooling, reusable servers

For simple use cases, use Tool Use. For complex systems with many integrations, consider MCP.

Can skills call other skills?

Yes, but with caution. A better approach:

  1. The LLM decides on the sequence of skills
  2. Or use an "orchestrator" skill that coordinates others

How to handle long-running operations?

Code
TypeScript
async execute(params) {
  const jobId = await startLongJob(params)

  return {
    status: 'started',
    jobId,
    checkStatusWith: 'check_job_status'
  }
}

How to secure sensitive skills?

  1. Approval workflow - Some skills require user confirmation
  2. Scoped permissions - Different users have access to different skills
  3. Audit logging - Log all invocations
  4. Rate limiting - Limit frequency

Can I use skills with different LLMs?

Yes, and that is the point of the standard. The same directory holding a SKILL.md file is read today by tools from different vendors, so a skill written once works regardless of which agent you happen to be using.

How to write good skills

Experience with this format comes down to a handful of rules absent from the specification that decide whether a skill gets used or gathers dust.

Write the description for retrieval, not for documentation. The agent sees only the name and description at startup, so that is the only moment it decides to use the skill. A description saying what the skill is performs worse than one saying when to use it. A sentence like "Use when someone asks for a summary of the week or a ticket breakdown" lands better than "A reporting tool".

Write instructions as a procedure, not as prose. A numbered list of steps naming specific files and commands works markedly better than a paragraph explaining the idea. The agent does not need historical context, only an order of operations.

Record prohibitions too, not just instructions. "Do not modify files in the migrations directory" is often more valuable than three sentences about what to do, because it blocks the most common mistake. A skill is the right home for the tribal knowledge usually passed on verbally during someone's first week.

Keep one skill to one task. It is tempting to build a single large skill describing an entire development process, but its description then turns generic and the agent cannot tell when to load it. Five narrow skills with precise descriptions outperform one broad one.

Version them alongside the code. A skill describing a process that changed six months ago is worse than no skill, because the agent will follow an outdated procedure with full confidence. Keeping the directory in the project repository means a process change and an instruction change travel in one pull request.

A final rule concerns where your skills come from. Catalogues collecting ready made entries already exist, Skills.sh among them, and installing several dozen at once is tempting. That usually makes things worse, since descriptions start overlapping and the agent picks the wrong one. One person collections circulate alongside the registries, Everything Claude Code among them, where over a hundred skills arrive together with hooks, commands, and working rules. Such a set reads like somebody else's editor configuration: worth mining for ideas, not worth adopting whole, because it reflects its author's habits and a hundred odd similar descriptions make it harder for the agent to pick the right one. Treat somebody else's skills as a starting point to read and adapt rather than a dependency to install and forget. The ones that work best are written for a specific team and a specific repository, because that is exactly the knowledge a model lacks.

Common mistakes

The first is a description from which the use case cannot be recognised. If the agent never reaches for your skill, nine times out of ten the problem lies in the description rather than the instructions.

The second is cramming everything into the main file. The format has three levels precisely so that long material can live in supporting files. The main file should fit on a screen, with details moving into the documentation directory the agent opens when needed.

The third is confusing a skill with a tool. A skill grants the agent no new technical capability: if it lacks database access, no instruction changes that. Access comes from the tool layer; the skill says how to use it.

The fourth is treating bundled scripts as trusted code. A skill pulled from an external catalogue can carry scripts the agent will execute with your permissions. That is the same class of risk as installing a dependency from an unknown source and deserves the same caution.

The fifth is writing skills for things better expressed in code. If a procedure can be captured in a script run by one command, do that and let the skill merely say when to run it. Instructions describing twenty steps a computer would perform deterministically are an invitation to errors.

The full format specification lives at agentskills.io, and work on the standard happens in the project repository.