Skill

Package a capability once, reuse it across agents.

What it is

Skills are reusable agent capabilities defined as local SKILL.md files. A skill can describe how to perform a task (including when and how to call tools), and can be exposed to an agent as a callable tool.

The examples/skills example demonstrates the full flow: discover local skills, turn them into tools, and let an agent use them.

Skill layout

A skill lives in its own directory and includes a SKILL.md. Skills can also ship helper scripts under scripts/.

examples/skills/
  skills/
    get-random-quote/
      SKILL.md
      scripts/
        get_random_quote/
          main.go

SKILL.md format

Skills are written in Markdown with a small YAML frontmatter header. The body describes when to use the skill and the execution contract.

---
name: get-random-quote
description: Fetches and returns a single random quote by running the Go script scripts/get_random_quote/main.go.
---

# Random Quote Retriever

## Overview

Use this skill when the user asks for a random quote (inspirational, motivational, general). This skill must retrieve the quote by executing the repository's Go script and returning its stdout.

## How It Works

### Primary Action

- Always run `go run ./scripts/get_random_quote/main.go` via the `bash` tool.
- Return exactly the quote line printed by the program (trim surrounding whitespace).

Tip: keep the contract concrete (exact command, expected stdout, and error behavior). This makes it easier for an agent to use the skill reliably.

Arguments, allowed tools and several roots

The model loads a skill through the skill tool (tool/skill), giving its name in command and, when the skill takes any, its arguments. A $ARGUMENTS placeholder in the body is replaced with them; without one, they are appended after the body.

The allowed-tools frontmatter field lists the tools a skill pre-approves, in the space-separated form of the Agent Skills spec (Read Grep) or the comma-separated form with patterns (Bash(git status:*), Read). Skill.Tools() returns the entries, the skill tool returns them to the model, and WithOnLoad lets the application grant them, for example in a permission policy. As in Claude Code, the grant should last only for the run that loaded the skill (AllowForRun), and permission.SkillGrants leaves out grants too broad for a skill a cloned project may ship: bash with no fixed command (Bash, Bash(*), a * tool name), with a quoted or escaped command (\sh), with a command that runs others (sh, sudo, env, python3.12, busybox, …), or with git and no subcommand (git -c can run anything). The refused commands are a heuristic that may grow in later versions, not a contract.

skillTool, err := skilltool.New("",
    skilltool.WithRoots(userSkillsDir, "./skills"), // earlier roots win on the same skill name
    skilltool.WithOnLoad(func(ctx context.Context, s *skill.Skill) {
        granted, refused := permission.SkillGrants(s.Tools())
        _ = policy.AllowForRun(ctx, granted...) // pre-approved for this run only
        if len(refused) > 0 {
            log.Printf("skill %s: not granted: %v", s.Name, refused)
        }
    }),
)
if err != nil {
    return err
}
for _, skipped := range skillTool.Skipped() {
    log.Printf("skill left out: %v", skipped)
}

A skill that cannot be used (a SKILL.md that does not parse, no name or description, or a name an earlier skill already has) is left out of the catalog, and Skipped() says why; the other skills still load. When two roots hold skills of the same name, in directories of different names, the earlier root's skill wins too, so a cloned project cannot turn off or take over the user's skills.

skill.NewMulti(roots...) is the parser behind WithRoots: a user's own skills and a project's, with the earlier root winning when both have a skill directory of the same name. Pass the user's root first, so a cloned project cannot shadow a skill the user trusts. A missing root holds no skills.

Roots, in one place: skilltool.New(root) reads one root (skill.New(root)). WithRoots(roots...) replaces it with several (skill.NewMulti), so pass "" as New's root when you use it. WithParser replaces both, and the last of WithRoots and WithParser wins. Across roots the earlier one wins, both for a skill directory name and for a skill name.

Example: a skill-backed script

The bundled get-random-quote skill fetches a quote by running a small Go program through the skill's bash tool.

# From examples/skills/skills/get-random-quote/SKILL.md

1. Run the script:
   go run ./scripts/get_random_quote/main.go
2. Return the stdout output exactly as printed, trimming surrounding whitespace.
3. If the script fails, surface the error instead of fabricating a quote.
// From examples/skills/skills/get-random-quote/scripts/get_random_quote/main.go

httpReq, err := http.NewRequestWithContext(
    context.Background(),
    "GET",
    "https://zenquotes.io/api/random",
    http.NoBody,
)
// ...
fmt.Printf("%s - %s\n", data[0].Q, data[0].A)

Example: discover skills and add them to an agent

In the example, skills are loaded from a local directory (./skills), parsed, converted into tools, and then registered on an agent.

// From examples/skills (edited for brevity)

skillParser := skill.New("./skills")
list, err := skillParser.List()
if err != nil {
    panic(err)
}

tools := make([]*llm.Tool, 0, len(list))
for _, skillName := range list {
    s, err := skillParser.Parse(skillName)
    if err != nil {
        panic(err)
    }

    tool, err := s.AsTool(
        llmClient,
        // Optional: give the skill-agent memory so it can do multi-step work.
        skill.WithMemory(memory.New(20)),
        // Optional: prevent runaway tool loops.
        skill.WithMaxIterations(10),
    )
    if err != nil {
        panic(err)
    }
    tools = append(tools, tool)
}

// register each skill tool on the agent
for _, tool := range tools {
    _ = a.AddTool(tool)
}

The examples/skills program uses memory/jsonfile for persistent history, and also adds a separate file-writing tool with interactive approval middleware.

Run the example

The example loads skills from a relative path (./skills) and stores history in memory.json, so run it from its directory. Provider setup depends on your chosen LLM; follow the example README.

cd ./examples/skills

go run .

You’ll be prompted to approve tool actions (like running commands or writing files). Reply y to allow.

When to use skills

Related packages