Skip to main content

Creating extensions for CLI do GitHub Copilot

Build extensions that add your own tools and slash commands to CLI do Copilot.

Observação

CLI do GitHub Copilot extensions are currently an experimental feature and are subject to change.

An extension lets you add your own capabilities to CLI do Copilot. Each extension is a small Node.js module that runs as a separate process alongside your interactive session and connects back to it. Through that connection, an extension can add tools that Copilot can call while it works on your behalf, and slash commands that you run yourself.

In this tutorial, you'll build two simple extensions as examples of what you can do:

  • A tool, called tool-time, that Copilot can call to report how long its tool calls have taken so far in a session.
  • A slash command, called /tokencount, that reports how many tokens you've used since you started counting.

Both examples rely only on the SDK that's bundled with the CLI do Copilot, so there's nothing extra to install. For background on how extensions work, see About extensions for CLI do GitHub Copilot.

Aviso

Extensions execute on your computer with your privileges. Only load extension code that you trust, in the same way you would only run any other script you didn't write yourself.

Prerequisites

  • CLI do GitHub Copilot: You need CLI do Copilot installed and set up. See Introdução à CLI do GitHub Copilot.
  • Experimental features enabled: Extensions are currently an experimental feature. The steps in this tutorial turn on experimental features each time you start the CLI, using the --experimental command line option.
  • JavaScript: Extensions are written in JavaScript, so you'll need to be familiar with this language to create your own extensions.
  • A repository: The second example adds a project-level extension, so you'll need a local copy of a Git repository in which to add the extension.

Example extension 1: A "tool time" tool

This example adds a user-level extension called tool-time. It adds a new tool—called session_tool_time—that Copilot can call to report how long tool calls have taken so far this session, and how many calls that covers.

To ensure a tool is used by the CLI, the tool must do something the CLI can't do on its own. In this case, the session_tool_time tool keeps track of how long tool calls take by listening to events from the CLI about when tool calls start and finish, and recording the timings itself. Because the CLI doesn't record these timings anywhere that Copilot can read, the only way for Copilot to know them is to call the tool. The tool's description explains this to the model, which steers it toward using the tool when you ask about tool call timings.

Step 1: Create the extension file

  1. Create the following directory and file in your home directory:

    ~/.copilot/extensions/tool-time/extension.mjs
    

    Because this is under ~/.copilot/extensions/, the extension is available in all your CLI sessions, in every directory—not just one repository.

  2. Add this code to extension.mjs:

    JavaScript
    // Extension: tool-time
    // Adds a session_tool_time tool that reports how long Copilot's tool calls
    // have taken this session. Copilot is never told these timings and they
    // aren't written anywhere, so calling the tool is the only way to find
    // them out.
    
    import { joinSession } from "@github/copilot-sdk/extension";
    
    // The tool's own name, so it can avoid timing its own calls.
    const TOOL_NAME = "session_tool_time";
    
    // Module-level state persists for the whole session, because the extension
    // runs as a single long-lived process.
    const startTimes = new Map(); // tool call id -> Date.now() when call started
    let totalMs = 0; // total milliseconds spent across finished tool calls
    let callCount = 0; // number of finished tool calls measured
    
    const session = await joinSession({
        tools: [
            {
                name: TOOL_NAME,
                description:
                    "Report how long your tool calls have taken in total so far " +
                    "in THIS session, and how many calls that covers. You are " +
                    "never told how long your tool calls take and the timings " +
                    "aren't recorded anywhere you can read, so call this tool " +
                    "whenever you are asked about it rather than estimating.",
                // Always keep this tool's description in the model's tool list,
                // even when tool search is active, so Copilot reliably sees it:
                defer: "never",
                // Force a permissions approval prompt once, for this extension,
                // rather than on every call of this tool:
                skipPermission: true,
                parameters: { type: "object", properties: {} },
                handler: async () => {
                    const seconds = (totalMs / 1000).toFixed(1);
                    return `So far this session, Copilot's tool calls have taken ${seconds}s in total across ${callCount} call(s).`;
                },
            },
        ],
    });
    
    // When a tool starts, record the time, keyed by the tool call id so the
    // matching completion can be found later. The tool's own calls are skipped.
    session.on("tool.execution_start", (event) => {
        const data = event.data ?? {};
        if (data.toolName && data.toolName !== TOOL_NAME) {
            startTimes.set(data.toolCallId, Date.now());
        }
    });
    
    // When a tool finishes, add the elapsed time to the running total.
    session.on("tool.execution_complete", (event) => {
        const data = event.data ?? {};
        const startedAt = startTimes.get(data.toolCallId);
        if (startedAt === undefined) {
            return;
        }
        startTimes.delete(data.toolCallId);
        totalMs += Date.now() - startedAt;
        callCount += 1;
    });
    

Observação

  • @github/copilot-sdk/extension is the extension SDK, which is bundled with the CLI. The CLI resolves this import automatically when it runs your extension, so you don't need to add it to a package.json or run a package manager.
  • The startTimes, totalMs, and callCount values live at module scope. Because the extension runs as a single long-lived process for the whole session, they accumulate for as long as the session is open.

Step 2: Load the extension

  1. Start an interactive session with experimental features enabled:

    Shell
    copilot --experimental
    

    Because the extension lives under ~/.copilot/extensions/, you can start the CLI from any directory and the extension will be available.

    If you already had a session open, run /clear to start a fresh session, which reloads extensions from disk.

  2. Without granting elevated permissions to either the new extension, all extensions, or all tools, you'll be prompted to allow the new extension to skip tool permission prompts. Choose either Yes or Yes, and always allow "user:tool-time" in this directory.

    Observação

    For the minimum elevated permissions to prevent seeing this message when you start the CLI, add this to your CLI startup command:

    Shell
    --allow-tool='extension-permission-access(user:tool-time)'
    

    For more information, see Permitir e negar o uso da ferramenta.

Step 3: Confirm the extension is running

Run the /extensions manage command to open the extension manager. Your tool-time extension should be listed under the User group with a status of running. Press Esc to close the manager.

Step 4: Try it out

  1. Unlike a slash command, you don't invoke a tool yourself—Copilot calls it when it's useful. First, give Copilot some work that involves a few tool calls, for example:

    Copilot prompt
    Explore the files in the current directory and give me a short summary of what's here.
    
  2. Once Copilot finishes responding, ask:

    Copilot prompt
    How long have tool calls taken so far this session?
    

    The agent calls the session_tool_time tool, from the new extension, and uses it to answer your question.

    Dica

    You can confirm that the agent used the new tool by looking at the start of Copilot's response. The response should be prefixed by the name of the tool that was used; in this case, session_tool_time.

How the tool works

The single call to joinSession is what turns a plain Node.js file into a CLI do Copilot extension. It connects the running process to your session and registers everything the extension adds to the CLI—in this case, a single tool.

A tool is defined by these fields:

  • name—the identifier Copilot uses to call the tool.
  • description—what the tool does. The model relies on this text to decide when to call the tool, so it's worth being explicit. This description tells the model that it isn't told these timings itself, which steers it toward calling the tool instead of attempting to work out the timings some other way.
  • parameters—a JSON Schema describing the tool's arguments. This tool takes none, so the schema is an object with no properties.
  • handler—an async function that runs when Copilot calls the tool. Whatever string it returns becomes the tool's result, which the model reads.

Two further fields shape how the tool is offered to the model:

  • defer: "never"—keeps the tool's description in the model's tool list at all times. By default, when many tools are available, the CLI can defer rarely-used tools and let the model search for them on demand. Setting defer to "never" opts this tool out of that behavior, so Copilot always sees it.

    Importante

    defer: "never" simply makes the tool available to Copilot. It doesn't force Copilot to call it. There is no way for an extension to mandate that a particular tool should always be used if there is an alternative means of generating an appropriate response to a prompt. The model always decides for itself which tool to use. In this example, the new tool is reliably used because there's no other way for the model to know the information the tool provides.

  • skipPermission: true allows the tool to run without asking you to approve each call. This is appropriate here because the tool only reads totals the extension has already collected; it doesn't touch your files or run commands.

The timings are gathered by watching the session. The extension subscribes to two events the CLI emits around every tool call:

session.on("tool.execution_start", (event) => { /* ... */ });
session.on("tool.execution_complete", (event) => { /* ... */ });

A tool.execution_start event carries the tool's name (event.data.toolName). A tool.execution_complete event carries a success flag. Neither event carries the other's information, so the extension correlates them using the toolCallId that appears on each: when a tool starts, it records the current time under that ID. When the matching completion arrives, it adds the elapsed milliseconds to the running total. The tool skips its own calls so the figure reflects Copilot's real work.

Because the totals live in the long-lived extension process, they cover the whole session. This is the kind of job extensions are good at: observing what's happening in the session and keeping state across calls.

Observação

  • The totals are held in memory, so they reset whenever the extension is reloaded or the session restarts (for example, after /clear).
  • The figure is wall-clock time measured from the extension's point of view—the gap between each tool call's start and completion events—so it includes any time a call spent waiting for you to approve it.

Example extension 2: A token usage slash command

This example adds a project extension called token-counter. The extension adds a /tokencount slash command that you can use to check how many tokens you've used when interacting with Copilot in the CLI. You run /tokencount start when you want to start measuring your token usage, and then you can run /tokencount at any later point to check how many tokens you've used since starting the count.

The extension keeps a running total of tokens used in the session by subscribing to events emitted by the CLI, which is something a one-off shell command can't do.

Step 1: Create the extension file

  1. In the root of the Git repository for your project, create the following directories and file:

    .github/extensions/token-counter/extension.mjs
    
  2. Add this code to extension.mjs:

    JavaScript
    // Extension: token-counter
    // Adds a /tokencount slash command that reports how many tokens you've used
    // since you started counting.
    
    import { joinSession } from "@github/copilot-sdk/extension";
    
    // Module-level state. The extension runs as a single long-lived process for
    // the whole session, so these values persist between command invocations.
    let tokensUsed = 0; // Running total of tokens used so far this session.
    let startedAt = null; // tokensUsed when "/tokencount start" was last run.
    // null = not started.
    // startedAt allows you to count tokens multiple times in a session.
    
    const session = await joinSession({
        commands: [
            {
                name: "tokencount",
                description: "Report how many tokens you've used since " +
                    "'/tokencount start'.",
                handler: async (ctx) => {
                    const arg = (ctx.args ?? "").trim();
    
                    if (arg === "start") {
                        // Reset: remember the current total as the new baseline.
                        startedAt = tokensUsed;
                        await session.log(
                            "Token counter started. Run '/tokencount' later to " +
                            "see how many tokens you've used.",
                            { level: "info" },
                        );
                        return;
                    }
    
                    if (startedAt === null) {
                        await session.log(
                            "The token counter has not been started. Start by " +
                            "entering '/tokencount start'.",
                            { level: "info" },
                        );
                        return;
                    }
    
                    const used = tokensUsed - startedAt;
                    await session.log(
                        `You have used ${used} tokens since entering ` +
                        "'/tokencount start'.",
                        { level: "info" },
                    );
                },
            },
        ],
    });
    
    // The CLI emits an "assistant.usage" event after each assistant turn. Add the
    // tokens it reports to a running total kept in the extension's memory.
    session.on("assistant.usage", (event) => {
        const { inputTokens = 0, outputTokens = 0 } = event.data ?? {};
        tokensUsed += inputTokens + outputTokens;
    });
    

Step 2: Load the extension

Start an interactive session from the same repository, with experimental features enabled:

Shell
copilot --experimental

If you already had a session open, you can run /clear to start a fresh session, which reloads extensions from disk.

Step 3: Confirm the extension is running

Run the /extensions manage command to open the extension manager. Your token-counter extension should be listed under the Project group with a status of running. Press Esc to close the manager.

You can also run /env to see a summary of everything loaded into the session, including extensions.

Step 4: Try it out

Unlike a tool, you invoke a slash command yourself. Start the counter:

Copilot prompt
/tokencount start

Send Copilot a prompt or two so that it uses some tokens—for example, ask it to explain a file. Then check how many tokens you've used:

Copilot prompt
/tokencount

If you run /tokencount start again, the count restarts from zero.

How the example works

The call to joinSession registers everything the extension adds to the CLI—in this case, a single slash command.

A slash command is defined by three fields:

  • name—the command name, without the leading slash. Registering tokencount is what makes /tokencount available in the session.
  • description—the text shown next to the command in the slash command picker.
  • handler—an async function that runs when you invoke the command. It receives a context object whose args property holds the raw text typed after the command name. For /tokencount start, ctx.args is "start"; for a bare /tokencount, it is an empty string.

The handler writes its output back to the session with session.log(message, { level: "info" }), which prints the message in the transcript.

To know how many tokens have been used, the extension subscribes to a session event:

session.on("assistant.usage", (event) => {
    const { inputTokens = 0, outputTokens = 0 } = event.data ?? {};
    tokensUsed += inputTokens + outputTokens;
});

The CLI emits an assistant.usage event after each assistant turn, carrying the input and output token counts for that turn. The extension adds them to the running total in tokensUsed. When you run /tokencount start, the handler records the current total in startedAt. Entering /tokencount with no argument later in the session reports the difference. Because both variables live in the long-lived extension process, they persist for the whole session.

Observação

The totals are held in memory, so they reset whenever the extension is reloaded or the session restarts (for example, after /clear).

Editing and reloading an extension

As you develop an extension, you'll edit extension.mjs and want to see your changes. After saving the file, you can pick up the new version in any of these ways:

  • Ask Copilot to reload extensions—for example, Reload my extensions.
  • Run /clear to start a new session, which reloads extensions from disk.
  • Restart the CLI.

If an extension fails to start or behaves unexpectedly, run /extensions manage and inspect the extension to see its status and the path to its log file. Each extension writes a log under ~/.copilot/logs/extensions/, which is the best place to look when something goes wrong.

Next steps

  • Adapt one of these examples to your own needs. In the CLI, ask Copilot to modify the behavior of either of the example extensions.
  • Share a user-level extension. For example, move the tool-time extension into a repository's .github/extensions/ directory to share it with everyone who works in that repository.
  • Ask Copilot to create a new extension for you from scratch. CLI do Copilot extensions are powered by the SDK do Copilot, so an extension can do anything the SDK makes possible, allowing you to add interactive views with buttons and forms, as well as new tools and commands. See SDK do Copilot.

Further reading