Custom skills - GitHub Docs
Skip to main content
Skip to content

Custom skills

Skills are reusable prompt modules that extend Copilot's capabilities. Load skills from directories to give Copilot specialized abilities for specific domains or workflows.

Overview

A skill is a named directory containing a SKILL.md file—a markdown document that provides instructions to Copilot. When loaded, the skill's content is injected into the session context.

Skills allow you to:

  • Package domain expertise into reusable modules
  • Share specialized behaviors across projects
  • Organize complex agent configurations
  • Enable/disable capabilities per session

Loading skills

Specify directories containing skills when creating a session:

Code languages navigation

TypeScript
import { CopilotClient } from "@github/copilot-sdk";

const client = new CopilotClient();
const session = await client.createSession({
    model: "gpt-5.4",
    skillDirectories: [
        "./skills/code-review",
        "./skills/documentation",
    ],
    onPermissionRequest: async () => ({ kind: "approve-once" }),
});

// Copilot now has access to skills in those directories
await session.sendAndWait({ prompt: "Review this code for security issues" });

Disabling skills

Disable specific skills while keeping others active:

Code languages navigation

TypeScript
const session = await client.createSession({
    skillDirectories: ["./skills"],
    disabledSkills: ["experimental-feature", "deprecated-tool"],
});

Skill directory structure

Each skill is a named subdirectory containing a SKILL.md file:

skills/
├── code-review/
│   └── SKILL.md
└── documentation/
    └── SKILL.md

The skillDirectories option points to the parent directory (e.g., ./skills). The CLI discovers all SKILL.md files in immediate subdirectories.

SKILL.md format

A SKILL.md file is a markdown document with optional YAML frontmatter:

---
name: code-review
description: Specialized code review capabilities
---

# Code Review Guidelines

When reviewing code, always check for:

1. **Security vulnerabilities** - SQL injection, XSS, etc.
2. **Performance issues** - N+1 queries, memory leaks
3. **Code style** - Consistent formatting, naming conventions
4. **Test coverage** - Are critical paths tested?

Provide specific line-number references and suggested fixes.

The frontmatter fields:

  • name: The skill's identifier (used with disabledSkills to selectively disable it). If omitted, the directory name is used.
  • description: A short description of what the skill does.

The markdown body contains the instructions that are injected into the session context when the skill is loaded.

Skill providers (experimental)

Note

Skill providers are experimental. The API can change in future SDK releases.

A skill provider serves skills from your application's own storage, such as a database in a multi-tenant service, instead of SKILL.md files on disk. Provider skills join the session's skill catalog alongside file-based skills. The model loads them on demand through the skill tool, and users can invoke them like any other skill.

A provider implements two operations:

  • List skills: returns catalog metadata for every skill: a name, a description, and optionally userInvocable, disableModelInvocation, and argumentHint. The runtime calls it when it loads the session's skills.
  • Read skill: returns the markdown for one skill, or a not-found result if the skill no longer exists. The runtime calls it only when the skill is loaded.

Pass the provider when you create or resume a session:

Code languages navigation

TypeScript
import { approveAll, CopilotClient, type SkillProvider } from "@github/copilot-sdk";

const releaseSkills = new Map([
    [
        "release-notes",
        {
            description: "Writes release notes in the team's format.",
            markdown: "Group changes by feature area and link each pull request.",
        },
    ],
]);

const skillProvider: SkillProvider = {
    listSkills: () =>
        [...releaseSkills].map(([name, skill]) => ({ name, description: skill.description })),
    readSkill: (name) => releaseSkills.get(name)?.markdown ?? null,
};

const client = new CopilotClient();
const session = await client.createSession({
    onPermissionRequest: approveAll,
    skillProvider,
});

Provider skill content

Read skill returns the skill's SKILL.md text. YAML frontmatter is optional:

  • Without frontmatter, the whole text is the skill body, and all metadata comes from the listed skill.
  • With frontmatter, an omitted field inherits the listed value. A field that you include must match the listed value, or the skill fails to load.
  • allowed-tools is read only from frontmatter.
  • If the first line of the text is ---, the runtime parses it as frontmatter, so don't start a body-only skill with a Markdown thematic break.

By default, provider skills are text-only: they have no base directory, so they can't reference bundled scripts, templates, or other files. To give a provider skill such files, see Supporting files. skills.list reports provider skills with the source sdk and an empty path, with or without supporting files.

Supporting files (experimental)

A provider skill can come with supporting files, such as scripts, references, templates, and assets, the same way a file-based skill's folder can. Your application keeps each skill's files in a folder it controls and sets directory on the listed skill to that folder's absolute path.

When the model activates the skill, the runtime lists the folder the same way it lists a file-based skill's folder. The model gets the same "Base directory for this skill" line and related-files list that a file-based skill gets, in place of the text-only notice, and reads the files with its ordinary file tools. The runtime never reads, copies, or writes the files itself.

import { approveAll, CopilotClient, type SkillProvider } from "@github/copilot-sdk";

const skillProvider: SkillProvider = {
    listSkills: () => [
        {
            name: "release-notes",
            description: "Writes release notes in the team's format.",
            directory: "/srv/skills/release-notes",
        },
    ],
    readSkill: (name) =>
        name === "release-notes"
            ? "Run `python3 scripts/changelog.py` and fill in `templates/notes.md`."
            : null,
};

const client = new CopilotClient();
const session = await client.createSession({
    onPermissionRequest: approveAll,
    skillProvider,
});

Each SDK sets the folder on its listed-skill type:

SDKField
Node.jsdirectory: "/srv/skills/release-notes"
Pythondirectory="/srv/skills/release-notes"
GoDirectory: copilot.String("/srv/skills/release-notes")
.NETDirectory = "/srv/skills/release-notes"
JavaThe sixth SkillProviderDescriptor constructor argument
Rustdirectory: Some("/srv/skills/release-notes".into())

Where the folder lives

Put all of a skill's files under its directory, one folder per skill. The folder must be reachable where the runtime runs:

  • Local disk. Without a session filesystem, directory is a path on the machine or container that runs the runtime. The model can read the files with any file tool, and it can run scripts.
  • Session filesystem (Multi-tenancy and server deployments). With a sessionFs handler, directory uses the handler's path conventions, and your handler serves the files; the runtime lists the folder through it. view, create, edit, and apply_patch reach the files. grep, glob, and shell commands don't, so scripts can't run. The Java SDK has no sessionFs, so Java providers use local disk.

The runtime lists the folder after read skill returns, so you can create the folder lazily in read skill. If the folder is missing or empty, or can't be listed within 15 seconds of the runtime calling read skill, the model still gets the base directory line, with no related-files list, as for a file-based skill. If your sessionFs handler is disconnected when the runtime lists skills, those skills are text-only until the next listing.

Access

  • Local disk. From the time skills are listed, the runtime treats each enabled provider skill's directory like a configured skillDirectories folder: reads under it skip the outside-workspace path prompt, and sandboxed processes get read-only access to it. Reads still go to your permission handler, as they do for file-based skills. Don't return a broad directory, such as your home folder or a folder that holds every tenant's files.
  • Session filesystem. The runtime adds no access for the folder. Have your permission handler approve reads under it, and when the sandbox is on, add the folder to the sandbox's readonlyPaths.

Your application owns the folder: where it lives, how long it exists, cleaning it up, and keeping the model from changing it. The read-only access covers only sandboxed processes. Outside the sandbox, or when the folder is inside the workspace, the model can write to it unless your permission handler, or your sessionFs handler, refuses.

Writing the skill

  • Refer to supporting files by their path relative to the skill's folder, such as references/guide.md. The model is told the folder's absolute path.
  • Run scripts through an interpreter, such as python3 scripts/changelog.py or bash scripts/build.sh, as Using scripts in skills describes. Scripts run only when the folder is on local disk.

Rules and limits

  • directory must be absolute in the session's path conventions; on a Windows-style session filesystem it needs a drive or UNC prefix. It's resolved the way file tools resolve paths, so . and .. segments and repeated or trailing separators are removed, and it can't resolve to a filesystem root. It counts toward the catalog's 1 MiB metadata limit, and it isn't shown in skill catalogs. An invalid directory rejects the whole catalog, like any other invalid listed skill.
  • The related-files list follows the file-based rules: build and dependency folders, such as node_modules, bin, obj, and dist, are hidden; a folder with more than 50 entries is summarized; files nested more than 10 folders deep are not listed; SKILL.md is never listed; and at most 100 files are shown.
  • After the conversation is compacted, the model keeps the skill's instructions but not its folder path. Invoking the skill again shows it. File-based skills keep their path.
  • Supporting files need a Copilot CLI at least as new as the SDK release that adds them. An older CLI rejects the whole skill catalog when any listed skill has a directory. Providers that don't set directory work with older CLIs.

Serving existing SKILL.md files

To serve SKILL.md files that you already have, such as files stored in a database, parse each file's YAML frontmatter once to build its catalog entry, then return the file unchanged from read skill:

  • Map name and description to the listed skill's name and description. Both are required in the catalog; the runtime doesn't fall back to a folder name or the skill body.
  • Map user-invocable, disable-model-invocation, and argument-hint to the matching optional fields when the frontmatter sets them.
  • Return the original text, frontmatter included, from read skill. Because the listed values came from the same frontmatter, they match, and the runtime still reads allowed-tools from it.

Trusting provider content

Treat provider skills like skill directories: their content is trusted input. The model follows a skill's instructions, and the frontmatter controls how the skill is invoked. allowed-tools doesn't grant permissions in SDK sessions. The runtime reports it in the allowedTools field of the skill.invoked event, and your permission handler still decides every tool request.

Don't serve text that end users or other tenants can edit unless you would let them author a skill file. If you build skills from user input, generate the frontmatter yourself instead of passing user-supplied frontmatter through.

Provider limits and errors

The runtime validates the provider's catalog and content:

  • Names must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ and be unique, ignoring case.

  • A catalog can contain at most 1,024 skills and 1 MiB of metadata. Each description and argument hint can be at most 1,024 characters.

  • Each skill's markdown can be at most 1 MiB of UTF-8.

  • Each call must complete within 30 seconds. When the runtime stops waiting for a call, because it timed out, the session ended, or a resume replaced the provider, it cancels the request and ignores any later result. Each SDK passes that cancellation to your provider in its usual way:

    SDKCancellation signal
    Node.jsThe signal (AbortSignal) in the options passed to listSkills and readSkill is aborted.
    PythonThe provider's asyncio task is cancelled with asyncio.CancelledError. Synchronous providers run to completion.
    GoThe call's context.Context is cancelled.
    .NETThe call's CancellationToken is cancelled.
    JavaThe returned CompletableFuture is cancelled with cancel(true), which doesn't interrupt running work.
    RustThe provider future is dropped.

    These signals cover calls that the runtime cancels. When the connection closes or the client is force-stopped, the .NET, Java, and Rust SDKs also cancel calls that are still running. The Node.js, Python, and Go SDKs don't: a running call continues until it returns, and the SDK discards its result.

If listing fails or returns an invalid catalog, the runtime reports the problem in the errors list returned by skills.reload. A failed reload keeps the provider skills from the last successful list. If reading a skill fails, the skill tool reports a generic load failure to the model. The SDK never forwards the text of errors your provider throws or returns, so that text can't leak into the conversation.

Provider lifecycle

A provider is bound to one session:

  • A provider enables skills unless you set enableSkills to false, which keeps the provider bound but never called. In mode: "empty", skills stay disabled until you set enableSkills to true.
  • The provider is never persisted. Pass it again when you resume a session. Resuming without a provider removes it from the session. Extensions that join the session don't change it.
  • A provider skill with the same name as a file-based skill replaces the file-based skill, and skills.reload reports a warning.
  • Sub-agents use their parent session's provider.
  • The runtime can call the provider concurrently, for example from the skill tool, user invocations, and custom agents that preload skills, so both operations must be safe for concurrent use.
  • Cloud sessions don't support skill providers. The SDK rejects the configuration before it creates the session.

Configuration options

SessionConfig skill fields

LanguageFieldTypeDescription
Node.jsskillDirectoriesstring[]Directories to load skills from
Node.jsdisabledSkillsstring[]Skills to disable
Node.jsskillProviderSkillProviderExperimental provider for SDK-supplied skills
Pythonskill_directorieslist[str]Directories to load skills from
Pythondisabled_skillslist[str]Skills to disable
Pythonskill_providerSkillProviderExperimental provider for SDK-supplied skills
GoSkillDirectories[]stringDirectories to load skills from
GoDisabledSkills[]stringSkills to disable
GoSkillProviderSkillProviderExperimental provider for SDK-supplied skills
.NETSkillDirectoriesList<string>Directories to load skills from
.NETDisabledSkillsList<string>Skills to disable
.NETSkillProviderISkillProviderExperimental provider for SDK-supplied skills
JavasetSkillProviderSkillProviderExperimental provider for SDK-supplied skills
Rustwith_skill_providerArc<dyn SkillProvider>Experimental provider for SDK-supplied skills

Built-in skills and mode: "empty"

The runtime ships with a set of bundled built-in skills that are eligible by default. When you run the client in mode: "empty" (the recommended baseline for Multi-tenancy and server deployments), the SDK excludes every runtime-bundled built-in skill: it sends an empty includedBuiltinSkills list on the post-create and post-resume options patch, alongside the empty installedPlugins list.

This exclusion is the default, not a permanent restriction. To allow selected runtime-bundled skills, set includedBuiltinSkills (or the language-specific casing) to their names. You can also opt into your own custom skills under mode: "empty"—enable skills and pass your own skillDirectories—and those remain fully usable, including a custom skill that shares a name with a built-in. Under mode: "copilot-cli" the field is omitted unless you set the option.

Custom skill directories are scanned when skills first load, including directories that are missing or empty at that point. Filesystem changes in those directories do not automatically refresh the catalog on later turns. After adding, editing, or removing skills, call your session's skills.reload RPC:

SDKReload call
Node.jsawait session.rpc.skills.reload()
Pythonawait session.rpc.skills.reload()
Gosession.RPC.Skills.Reload(ctx)
.NETawait session.Rpc.Skills.ReloadAsync()
Javasession.getRpc().skills.reload().join()
Rustsession.rpc().skills().reload().await?

Changing the configured skill directories also refreshes the catalog.

Best practices

  1. Organize by domain - Group related skills together (e.g., skills/security/, skills/testing/)

  2. Use frontmatter - Include name and description in YAML frontmatter for clarity

  3. Document dependencies - Note any tools or MCP servers a skill requires

  4. Test skills in isolation - Verify skills work before combining them

  5. Use relative paths - Keep skills portable across environments

Combining with other features

Skills + custom agents

Skills listed in an agent's skills field are eagerly preloaded—their full content is injected into the agent's context at startup, so the agent has access to the skill instructions immediately without needing to invoke a skill tool. Skill names are resolved from the session-level skillDirectories.

const session = await client.createSession({
    skillDirectories: ["./skills/security"],
    customAgents: [{
        name: "security-auditor",
        description: "Security-focused code reviewer",
        prompt: "Focus on OWASP Top 10 vulnerabilities",
        skills: ["security-scan", "dependency-check"],
    }],
    onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Note

Skills are opt-in—when skills is omitted, no skill content is injected. Sub-agents do not inherit skills from the parent; you must list them explicitly per agent.

Skills + MCP servers

Skills can complement MCP server capabilities:

const session = await client.createSession({
    skillDirectories: ["./skills/database"],
    mcpServers: {
        postgres: {
            type: "local",
            command: "npx",
            args: ["-y", "@modelcontextprotocol/server-postgres"],
            tools: ["*"],
        },
    },
    onPermissionRequest: async () => ({ kind: "approve-once" }),
});

Troubleshooting

Skills not loading

  1. Check path exists - Verify the skill directory path is correct and contains subdirectories with SKILL.md files
  2. Check permissions - Ensure the SDK can read the directory
  3. Check SKILL.md format - Verify the markdown is well-formed and any YAML frontmatter uses valid syntax
  4. Enable debug logging - Set logLevel: "debug" to see skill loading logs

Skill conflicts

If multiple skills provide conflicting instructions:

  • Use disabledSkills to exclude conflicting skills
  • Reorganize skill directories to avoid overlaps

See also