I was pairing with someone last week who had Copilot CLI open and was typing the same preamble into again and again. “You’re reviewing Bicep. Don’t change anything. Look for hard-coded locations, missing tags, old API versions…” and so on, a paragraph of it, before the actual question. They had it saved in a text file and was pasting it in. It worked, but it’s the kind of thing you look at and think there has to be a better way, and there is. That paragraph is a custom agent. It just hasn’t been written down in the right place yet.
I’ve written about Copilot CLI a couple of times, and about building a whole app with it over a weekend, but I’ve never covered custom agents, and they’re the feature I now use most. This post is the one I wish I’d had: one agent, from an empty folder to a working run, with nothing clever in it.
What a custom agent actually is#
It’s a markdown file. That’s the whole thing. A small YAML header at the top says what the agent is called, what it’s for and which tools it’s allowed to touch, and everything underneath the header is the prompt that gets loaded when you pick that agent. No code, no registration step, no portal.
Copilot CLI looks for these files in three places. For something that follows you around every project, put it in ~/.copilot/agents. For something that belongs to one repo and should be shared with everyone who clones it, put it in .github/agents inside that repo. There’s also an organisation level, an agents folder in a .github-private repo, which I haven’t used and won’t pretend to. The filename ends in .agent.md, and if you don’t give the agent a name in the header, it takes the filename minus that suffix.
The same file works for the Copilot cloud agent on github.com and in VS Code, which is a nice surprise. Write it once for the CLI, commit it, and the agent shows up in the other places too. There’s a target field if you want to restrict it to one surface, but for a first agent leave it out.
The agent we’re going to build#
I’m going to build a read-only Bicep reviewer, partly because it’s a thing I actually want and partly because “read-only” is a good first agent. An agent that can’t edit files or run commands can’t do much damage whilst you’re learning how the tools list works.
Make a folder, give it a .github/agents directory, and create bicep-reviewer.agent.md in there:
---
name: bicep-reviewer
description: Reviews Bicep files for naming, API versions, missing tags and hard-coded locations. Read-only.
tools: ["read", "search"]
---
You are a reviewer for Azure Bicep files. You never edit files or run commands.
When asked to review, read every .bicep file in the working directory and report, as a short list:
- hard-coded locations instead of a location parameter
- resources missing a tags property
- apiVersion values older than two years
- parameters with no description decorator
Be brief. If there is nothing wrong, say so in one line.Three things in the header are worth a sentence each.
description is the only required field. Copilot uses it to decide when to pick this agent on its own, so write it for a reader who is choosing between several agents, not as a slogan.
tools is a list of what the agent may use. The names are aliases that map onto groups of tools: read covers reading files, search covers grep and glob, edit covers writing and editing, execute covers the shell, web covers search and fetch, and agent lets it spawn sub-agents. Leave the field out and the agent gets everything. Put [] and it gets nothing. You can also name individual MCP tools as server/tool, or every tool on a server as server/*, which is how you’d give an agent the Azure MCP server and nothing else. Unrecognised names are ignored rather than rejected, so check your spelling, because a typo silently gives you less than you asked for.
model is optional and I’d leave it off until you have a reason. Same for disable-model-invocation, which stops Copilot choosing the agent automatically and makes it something you only pick by hand. For a reviewer that’s arguably what you want, but I’d rather see how often it gets picked first.
The prompt body underneath can be up to 30,000 characters. Mine is six lines, and I’d argue that’s about right for a first go. You will be tempted to write an essay. Don’t. Write the one paragraph you keep pasting in and stop.
Running it#
Drop a deliberately scruffy Bicep file next to it so there’s something to find:
param storageName string
resource sa 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: storageName
location: 'uksouth'
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}Then, from that folder, run it non-interactively with the --agent flag:
copilot --agent bicep-reviewer -p "Review the Bicep in this folder" --allow-all-tools -s-p means run this one prompt and exit, -s means only print the answer and skip the stats, and --allow-all-tools is needed in non-interactive mode because there’s nobody there to approve tool calls. That flag sounds alarming, but remember the agent only has read and search in its header, so “all tools” here means those two. The first time you run it in a new folder the CLI will also want you to trust the folder; --trust-folder does that for a single run.
Here’s what came back for me on CLI 1.0.103, with only the code formatting stripped:
- main.bicep:1: storageName has no @description decorator.
- main.bicep:3: The storage account has no tags property.
- main.bicep:3: API version 2021-04-01 is older than two years.
- main.bicep:5: Location is hard-coded as uksouth instead of using a location parameter.Four findings, one line each, in the order I listed them in the prompt. Well under a minute, and no pasting.
In an interactive session you have two other ways in. Type /agent and pick from the list, or just ask in plain English, “use the bicep reviewer on main.bicep”, and Copilot matches it against the description. That second one is where a decent description pays for itself.
Making it do something useful on a Friday afternoon#
Once an agent works from the command line it can go anywhere a shell can. The obvious next step is a pre-commit hook, so any .bicep change gets a review before it lands:
copilot --agent bicep-reviewer -p "Review only the staged .bicep files and list problems" --allow-all-tools -sKeep it as something that prints rather than a gate. I don’t want an AI blocking commits. But seeing “location is hard-coded” scroll past before you push is a cheap way to catch the things that the Bicep linter doesn’t, like your own team’s tagging rules.
If you do want it to be a gate, there’s --output-format json which gives you one JSON object per line you can parse, but I’d give the plain text version a couple of weeks first and see whether you trust it.
Caveats#
The agent only knows what you tell it. Mine says “older than two years” and the model does the arithmetic from whatever it thinks today’s date is, which has been right so far but isn’t something I’d bet a production review on. If a rule matters, put the actual number in the prompt.
The precedence between levels is documented a bit inconsistently. The CLI docs say a repo-level agent overrides an organisation one, and a system-level one overrides the repo, but neither page I read places ~/.copilot/agents clearly in that order. If you have the same name in two places, test which one wins rather than assuming.
--allow-all-tools in scripts is fine for a read-only agent and considerably less fine for one with execute in its list. Scope the tools down before you automate, not after.
And the CLI updates itself often. Everything here is from 1.0.103 in October 2026; if a flag has moved by the time you read this, copilot --help is quicker than searching.
Final thoughts#
Find the paragraph you keep pasting into Copilot. Everyone has one. Put it in .github/agents/<something>.agent.md with a description and a tools list that only has read and search in it, and run it once with --agent. If that works, and it will, commit the file so the rest of your team gets it for free. You can make it clever later. The first one just needs to exist.


