Built-in plugins
Four plugins are compiled into the extension. They need no plugins/
folder, no manifest of yours, and no install — uses: pwsh@1 works in a
fresh workspace.
They are the only plugins that are not sandboxed. See the sandbox for why.
shell@1
Runs a command line through the OS shell. This is what a run: task
desugars to, so you rarely write it out — but it is a plugin like any
other, and its outputs are captured the same way.
| Arg | Type | ||
|---|---|---|---|
script |
string |
required | The command line to run. |
shell |
string |
pwsh | cmd | bash |
Which shell runs it. Absent means whatever the OS provides by default — cmd on Windows, bash elsewhere. |
Outputs: stdout, stderr, exitCode.
tasks:
list:
uses: shell@1
args:
script: git status --short
output: STATUS
pwsh must be named explicitly. A $env:NAME script with no shell:
runs in cmd on Windows and comes back empty.
pwsh@1
Runs an inline PowerShell script, or a .ps1 file. PowerShell 7+.
| Arg | Type | ||
|---|---|---|---|
script |
string |
Inline PowerShell. Mutually exclusive with file. |
|
file |
string |
Path to a .ps1, relative to the task’s working directory. |
|
additionalArgs |
string |
Arguments appended when running file, e.g. -Name "World". |
|
shell |
string |
pwsh |
The only value accepted. A task that wants cmd wants a run: task. |
errorActionPreference |
string |
default stop |
Prepended as $ErrorActionPreference. |
progressPreference |
string |
default silentlyContinue |
Prepended as $ProgressPreference. |
failOnStderr |
boolean |
default false |
Fail the task if anything reaches stderr, even on a zero exit. |
ignoreExitCode |
boolean |
default false |
Report a non-zero exit without failing the task. |
errorActionPreference and progressPreference both accept default,
stop, continue and silentlyContinue.
Outputs: stdout, stderr, exitCode.
tasks:
package:
uses: pwsh@1
args:
file: ./scripts/package.ps1
additionalArgs: -Configuration Release
errorActionPreference defaults to stop on purpose, and it is the
single most load-bearing line this plugin emits. Without it a failing
cmdlet is non-terminating: it writes to stderr, leaves $LASTEXITCODE
alone, and the task reports success.
It governs cmdlets only. A native command exiting non-zero is unaffected,
so git diff --quiet and friends still work as questions rather than
failures.
progressPreference defaults to silentlyContinue so progress bars do
not spray control characters into the log.
The script is staged as a UTF-8 .ps1 and invoked, rather than passed to
pwsh -Command. So it is not capped by the command-line length limit,
quotes survive intact, and it is not visible to anything that can list
processes.
shell: cmd is rejected — a staged .ps1 is not something cmd.exe can
run. A task that wants cmd wants a run: task, which already has
shell:.
file@1
Reads a file into a run variable, or writes one from a value — the executes half of “AI proposes, a human approves, a plugin executes”.
| Arg | Type | ||
|---|---|---|---|
operation |
string |
required — read | write | mkdir |
What to do with the path. |
path |
string |
required | Relative to the workspace unless absolute. |
content |
string |
What to write. Required when writing, ignored when reading. | |
createDirectories |
boolean |
default true |
Create parent folders when writing rather than failing on a missing one. |
Outputs: content, path, lines.
tasks:
read-notes:
uses: file@1
args:
operation: read
path: RELEASE_NOTES.md
output: NOTES
write-notes:
needs: read-notes
uses: file@1
args:
operation: write
path: dist/notes.md
content: ${{ run.NOTES.content }}
ai@1
Runs an agent session on whichever provider the task names. It reads, writes and works in the repo.
Put a gate on a later task if you want to review before anything downstream runs. This plugin changes your working tree.
It needs whatever its provider needs. ghcp — GitHub Copilot, and the
only provider that ships today — uses the sign-in your editor already
has. This is the only bundled plugin that needs anything at all.
| Arg | Type | ||
|---|---|---|---|
prompt |
string |
required | What to ask the agent to do. |
provider |
string |
default ghcp |
Which agent runtime the session runs on. The vendor is named here and nowhere else. |
model |
string |
Which model runs the session. Usually set once in a plugins: entry. |
|
allow |
string[] |
Tool patterns the session may use, e.g. ['builtin:*']. Unset means the provider’s usual toolset. |
|
deny |
string[] |
Tool patterns to withhold. Always wins over allow. |
|
mcp |
object |
MCP servers the session may call, keyed by name. | |
context |
object |
Named documents laid into the session before the prompt, keyed by name — usually file args. | |
system |
string |
Standing instructions for every turn, appended to the provider’s own system message. | |
attachments |
array |
Files the session may open, as [{ path, name }]. Paths may sit outside the working directory. |
|
detach |
boolean |
Return the session id without waiting for the turn — for a conversation a person will join in the editor. | |
target |
string |
The file this session is expected to produce. The path is told to the agent, and the task fails if the session ends without it. | |
title |
string |
A short human title, sent as the prompt’s first line, so the chat history names the session. |
Outputs: summary, files.
plugins:
ai:
uses: ai@1
args:
provider: ghcp
model: claude-sonnet-4.5
tasks:
draft:
uses: ai
args:
title: Draft the release notes
prompt: Summarise the commits since the last tag.
context:
changelog: { file: CHANGELOG.md }
output: DRAFT
context pastes a document’s content into the session. attachments
only tells the agent a file exists and lets it open it. Reach for
context when the agent must read something, attachments when it might
want to.
mcp: — reading systems there is no plugin for
tasks:
pull-context:
uses: ai@1
args:
mcp:
ado:
command: npx
args: ["-y", "@azure-devops/mcp", "contoso"]
allow: [wit_get_work_item]
prompt: >
Read work item 4821 and write docs/context/AB-4821.md -
whatever structure serves a developer picking it up cold.
A remote server instead:
mcp:
ado:
type: http
url: https://mcp.dev.azure.com/contoso
Name the tools in allow:. Above 30 tools the runtime defers MCP
definitions behind its tool-search tool rather than loading them all — so
a large server does not cost a context window, but discovering a tool
still costs a round trip. Naming it skips even that: one definition, one
call, no search.
Never inline a credential. ${{ env.NAME }}
resolves before the declaration is handed over, so a server’s env: can
name a variable without the value ever being in the file.
When to use mcp: instead of writing a plugin
Both read a system like Azure DevOps, and they are not competing:
| Reach for | |
|---|---|
| The same read every run, feeding a durable artefact | a plugin |
| Exploratory — you don’t know what you’re looking for yet | mcp: |
Attachments — a .docx spec, a screenshot |
mcp:, clearly |
| It must run headless (schedule, CI, no Copilot sign-in) | a plugin |
The deciding difference is not token cost — a session pinned to one tool is cheap. It is that a plugin runs without a model at all, fails with a message naming the variable, and returns the same thing twice.
It enforces no gate
Not one. Every tool the agent takes is approved automatically, and the
place a human decides anything is a trigger: manual task you put in
the file — the same mechanism every other task uses.
draft:
uses: ai@1
args:
prompt: Write docs/specs/caching/requirements.md from the notes in docs/.
output: SPEC
implement:
needs: draft
uses: pwsh@1
trigger: manual # <- your call, not the plugin's
args:
file: ./scripts/implement.ps1
A permission card per tool was considered and rejected: nobody reads the
fifth one, and a run recording “approved” against an unread card is worse
than a run recording nothing. Use allow/deny to set policy once
instead.
Reviewing is easier after the fact anyway. A 500-line spec cannot be read
in a gate card; it can be read in your editor, which is where files
tells you to look.
One plugin, every vendor
The vendor is provider:, not the plugin’s name. A second vendor is a
second value, not a second plugin.json repeating this whole arg list to
change one line — and a file that runs two of them declares two
plugins: entries over the same ai@1.
There is deliberately no read-only preset. It would have to mean
something specific per vendor, and a safety setting whose meaning changes
with provider: is worse than none — so a session that must not touch
the repository says exactly that in deny:, and the gate below it is
what the workflow actually rests on.
Why these four and no more
There is no marketplace, nothing is downloaded, and nothing is signed. These four are what the engine needs to be useful on its own: a shell, a better shell, the filesystem, and an agent.
Anything past that is a plugin you write — which is a folder, a manifest, and a JavaScript file, with no build step and no dependency on this extension.