GitHub Copilot support¶
steer is built for Claude Code, but teammates who use GitHub Copilot — either the Copilot CLI or Copilot in VS Code — can pick up the same org engineering standards. This page explains how all surfaces share one source of truth and how to install and refresh the Copilot side.
Scope
The Copilot target covers the always-on standards
(.github/copilot-instructions.md, read by both the CLI and VS Code), the
skills (as cross-tool SKILL.md on the CLI, and as
.github/prompts/*.prompt.md slash-commands in VS Code), custom agents
(.github/agents/*.agent.md — the steer-reviewer port), path-scoped
instructions (.github/instructions/*.instructions.md), MCP servers
(.vscode/mcp.json), an opt-in cloud coding-agent setup workflow
(copilot-setup-steps.yml), and the gate hooks (the version-pin
policy and the trunk-push graduation gate, CLI-only, as soft asks).
Skill enforcement still differs from Claude Code and hooks do not
exist in VS Code — see the sections below for the caveats.
Surfaces at a glance¶
| Capability | Claude Code | Copilot CLI | Copilot in VS Code |
|---|---|---|---|
| Always-on standards | SessionStart hook → additionalContext |
.github/copilot-instructions.md |
.github/copilot-instructions.md (read natively) |
| Path-scoped standards | rule inject-when traits |
not delivered — emitted only to .github/instructions/ (see below) |
.github/instructions/*.instructions.md (applyTo glob) |
| Skills | plugin skills/ (/steer:<skill>) |
plugin skills/ via Copilot manifest |
.github/prompts/*.prompt.md (/steer-<skill>) |
| Subagents | plugin agents/ |
not declared — the Copilot manifest carries skills + hooks only |
.github/agents/*.agent.md (agent picker) |
| MCP servers | plugin .mcp.json |
not declared — the Copilot manifest has no mcpServers key |
.vscode/mcp.json |
| Cloud coding agent | — (Claude @claude workflow) |
— | .github/workflows/copilot-setup-steps.yml (opt-in) |
| Gate hooks | hooks/hooks.json (deny on version pins, ask on the trunk-push gate) |
hooks/copilot-hooks.json (softened to ask) |
none (no hook mechanism) |
| Source of truth | rules/*.md + skills/ + agents/ |
the same rules/ + skills/ + agents/ |
the same rules/ + skills/ + agents/ |
Every Copilot artifact — instructions, per-skill prompts, custom agents, the
VS Code mcp.json, the CLI hook manifest, and the plugin + marketplace manifest
versions — is generated from that one source and guarded by a build-time drift
gate (see below) that fails the build the moment a
committed artifact drifts. A symmetry meta-gate (check_copilot_symmetry.py,
part of plugin-check) further asserts every gen_copilot_*.py is wired into
gen:copilot and every check_copilot_*.py into plugin-check — so a generator
no task runs, or a gate no task invokes, fails the build. It asserts wiring, not
generator↔gate pairing: gen_copilot_manifests.py has no check_copilot_manifests.py
counterpart, because the manifest versions are gated by check_plugin.py's
version-sync check instead. No Copilot artifact is hand-maintained.
Why the surfaces differ¶
On Claude Code, steer's rules reach every session through a SessionStart hook
(inject-standards.sh) whose stdout becomes the session's context. GitHub
Copilot has no equivalent: its sessionStart hook ignores stdout, so it
cannot inject context that way. Copilot's always-on context instead comes from a
static custom-instructions file, .github/copilot-instructions.md, which both
the Copilot CLI and Copilot in VS Code read.
A build-time generator (mise run gen:copilot) concatenates the rules into that
committed artifact, and a sync gate (check_copilot_instructions.py, part of
plugin-check) fails the build if the artifact ever drifts from the rules. The
same generator step also renders the per-skill prompt files (below), with its own
drift gate (check_copilot_prompts.py).
Why .github/copilot-instructions.md, not AGENTS.md¶
Copilot reads several repository instruction files and merges them — including
AGENTS.md and CLAUDE.md/GEMINI.md — resolving conflicts
non-deterministically. Emitting an AGENTS.md would therefore double-load the
org standards alongside a repo's existing CLAUDE.md, while Claude Code (which
does not read AGENTS.md) would ignore it entirely.
.github/copilot-instructions.md is Copilot's primary instructions file, is
never read by Claude Code, and lives under .github/ so it does not compete
at the repo root with CLAUDE.md. That keeps each surface reading exactly one
copy of the standards.
Using it as a Copilot teammate¶
The standards file and the prompt files are installed by /steer:init (new repos)
or /steer:adopt (existing repos), run from Claude Code during bootstrap —
see the Adopt workflow. Copilot teammates only consume
the files; they do not need to generate them.
Copilot CLI¶
The CLI loads the skills via the Copilot plugin manifest and reads the standards
from .github/copilot-instructions.md in the repo.
Copilot in VS Code¶
VS Code does not use the Copilot CLI plugin marketplace, so there is nothing
to install — it reads the committed repo files directly:
- Standards —
.github/copilot-instructions.mdis read automatically as the repository's custom instructions (governed by thegithub.copilot.chat.codeGeneration.useInstructionFilessetting, default-on in recent VS Code). To confirm it loaded, expand the References section of a Copilot Chat response — the file is listed there (or right-click the Chat view → Diagnostics). - Skills — each user-invocable steer skill ships as a
.github/prompts/steer-<skill>.prompt.mdprompt file, surfaced in Copilot Chat as a/steer-<skill>slash-command (governed by thechat.promptFilessetting). Type/steer-in Chat to see them.
The bundled .vscode/settings.json sets both settings explicitly, so the
standards load regardless of a teammate's VS Code defaults.
Refreshing after a steer update¶
The Copilot files are a static snapshot, so they go stale when steer's rules
or skills change. Refresh them with /steer:sync from Claude Code:
copilot plugin update steer # CLI only: pull the new plugin version
# then, from Claude Code in the repo:
/steer:sync # re-copies copilot-instructions.md, prompts/,
# agents/, instructions/ from the new plugin
/steer:sync --check # read-only: reports the surface as mis-wired
# when it has fallen behind
/steer:sync owns this because the refresh is a capability repair:
copilot-surface-current is wired only when every generated file is
byte-identical to its plugin source, and the repair is a verbatim re-copy.
/steer:init is not the refresh path — it installs the surface at bootstrap
and then deliberately stops on an already-initialized repo, so re-running it does
nothing.
Because Copilot has no context-injecting SessionStart hook, this static set is its entire standards surface — so a repo that never refreshes leaves Copilot teammates working against the rules of whatever plugin version bootstrapped it, while their Claude Code colleagues are current. Put the refresh on whoever owns plugin updates; the launch checklist carries it as a rollout item.
The files are fully steer-managed — overwritten on refresh and never
hand-edited. Repo-specific Copilot guidance belongs in a separate
*.instructions.md file, not in these; the re-copy never touches a file you own.
Skills on Copilot¶
steer's skills are authored as SKILL.md files. They reach the two Copilot
surfaces differently:
- Copilot CLI reads
SKILL.mdnatively (an open cross-tool standard). A Copilot-specific plugin manifest (plugins/steer/.github/plugin/plugin.json, which Copilot prefers over the.claude-plugin/manifest Claude Code uses) points Copilot atskills/. Its version — and the Copilot marketplace manifest's — is stamped from the sourceplugin.jsonbygen_copilot_manifests.py(mise run gen:copilot), so no Copilot manifest is hand-versioned either. - Copilot in VS Code uses prompt files instead. The build renders one
.github/prompts/steer-<skill>.prompt.mdper user-invocable skill from the skill's frontmatter.
Two differences from Claude Code matter on both Copilot surfaces:
- Tool-permission scoping is inert. Copilot does not honor steer's
allowed-tools/disallowed-tools. steer's read-only skills rely ondisallowed-toolsto guarantee they never write; on Copilot that guard is ignored. Treat those skills as advisory there. - Bodies are Claude-centric. Skill instructions reference
${CLAUDE_PLUGIN_ROOT}paths and/steer:<skill>invocation, which do not resolve in a repo-committed prompt file. The VS Code prompt files are therefore intent capsules — purpose, when-to-use, and arguments — that drive the same workflow on top of the always-on standards, not verbatim reproductions of the skill procedure./steer:<skill>cross-references are rewritten to the/steer-<skill>prompt names, and the capsule points review-gated workflows at thesteer-reviewercustom agent (below). The authoritative procedure still lives in the plugin.
Custom agents on Copilot¶
steer's subagents in the plugin's agents/ reach VS Code as custom agents —
.github/agents/<name>.agent.md, selectable from the Copilot Chat agent picker
(this is the format formerly called "custom chat modes"/.chatmode.md). Today
that is steer-reviewer, the read-only reviewer that /steer-audit,
/steer-work --reviewed and /steer-loop delegate a single bounded slice to.
The build renders one .agent.md per subagent (gen_copilot_agents.py, drift
gate check_copilot_agents.py). The subagent's Claude tools (Read/Grep/
Glob) are mapped to Copilot's read-only built-in tool sets (codebase,
search), so the ported reviewer stays write-free on VS Code the same way it is
in Claude Code.
Path-scoped instructions¶
Most rules are repo-wide and live in the flat copilot-instructions.md. A rule
that is genuinely area-specific — currently the infra/IaC stack rule — is emitted
instead as a path-scoped instruction file,
.github/instructions/<name>.instructions.md, carrying an applyTo glob so
Copilot loads it only when working on matching files (e.g. **/*.tf, infra/**).
This is the Copilot analog of the Claude SessionStart hook's inject-when trait
gating; the same rule source drives both, and a scoped rule is excluded from
the flat file so it never double-loads. The same generator + drift gate as the
flat instructions (gen_copilot_instructions.py / check_copilot_instructions.py)
keeps them in sync.
A scoped rule reaches VS Code only
Because the exclusion is unconditional (iter_rule_files filters SCOPED_RULES),
a path-scoped rule is not in .github/copilot-instructions.md — today that
means rule 12-stack-infra, the IaC stack standards. That directory is read by
Copilot in VS Code and by the cloud coding agent; whether the Copilot CLI
reads it is unverified here, so a CLI teammate working on Terraform may receive
no IaC standards. If you need them there, load the file explicitly.
Do not "fix" this by dropping the rule from SCOPED_RULES: that key drives both
the flat-file exclusion and the scoped emission, and main() prunes the
orphaned file — so you would move the rule into every consumer's always-on
context and delete infra.instructions.md, not resolve the gap.
Repo-specific Copilot guidance you author yourself also goes in a separate
*.instructions.md you own — never edit the steer-generated ones.
MCP servers in VS Code¶
Copilot in VS Code does not read the plugin's .mcp.json (that wires Claude
Code only). So the scaffold ships .vscode/mcp.json — VS Code's servers
schema — mirroring the same servers: the GitHub MCP server that the tracker
gateway (tracker-sync, reached through /steer-issues and /steer-work — it is
user-invocable: false, so it has no prompt file of its own) is built around, and
context7 for current library docs. The GitHub server prompts once for a PAT
(stored in VS Code secret storage). Without it, Copilot's tracker workflow falls
back to gh only.
Like the other Copilot artifacts, this file is generated — gen_copilot_mcp.py
renders it from the plugin's .mcp.json (mise run gen:copilot), translating the
one sanctioned difference: the auth placeholder (env var → prompted input, mapped
in the generator's AUTH_INPUTS). A byte-equality drift gate
(check_copilot_mcp.py, part of plugin-check) fails the build if the committed
mirror falls out of sync. Edit .mcp.json and regenerate — never hand-edit the
template in this repo.
That byte-gate governs the plugin-side template only. Unlike the four artifacts
under .github/, the installed .vscode/mcp.json is not steer-managed: it sits
outside /steer:sync's copilot-surface-current capability, so a consumer owns
their copy and is expected to merge additively and remove servers they don't use.
Nothing re-copies it over their edits; only a one-shot ledger migration amends it.
Cloud coding agent (opt-in)¶
The GitHub-side Copilot coding agent (assign it an issue, it works in an
ephemeral environment and opens a PR) reads the same
.github/copilot-instructions.md + .github/instructions/ for standards. To make
it boot a steer repo correctly, the scaffold carries
.github/workflows/copilot-setup-steps.yml — it installs the pinned mise
toolchain and runs dev:setup. The job name copilot-setup-steps is required;
MCP + firewall for the agent are set in repo Settings → Copilot → Coding agent,
not in-repo.
It is opt-in — /steer:init does not install it automatically; add it only
for repos that use the coding agent. It fits steer's autonomous-loop rules: the
coding agent opens draft PRs and never merges, so the human merge gate stands.
Point it only at PR-flow repos (protected main), never solo-trunk.
Gate hooks on Copilot¶
The Copilot CLI manifest points hooks at a Copilot-native file
(hooks/copilot-hooks.json) rather than letting Copilot fall back to Claude's
hooks/hooks.json — important because Copilot's preToolUse hooks are
fail-closed (a hook that errors denies the tool), so a mis-run Claude hook
could block edits.
Two gates are ported so far, both surfacing as a soft ask (Copilot prompts
you to confirm): the version-pin policy (check-version-pins.sh, a hard
deny on Claude softened to ask here) and the trunk-push graduation gate
(check-bash-actions.sh, an ask on both surfaces). One hook script serves both
surfaces, each emitting Copilot's flat permissionDecision envelope when invoked
with STEER_HOOK_TARGET=copilot — but the two paths are not identical: the
trunk-push gate's repeat push downgrades to a non-blocking additionalContext
reminder on Claude and to a silent allow under Copilot, which has no
non-blocking channel (check-bash-actions.sh — the STEER_HOOK_TARGET check on
the marker-present branch). That caveat lives in the gates reference doc, not
inline in rule 45-commit-autonomy, and the generated
.github/copilot-instructions.md does not carry it either — so it is absent
from the always-on standards both surfaces read. It reaches a reader only on the
CLI, which loads the real reference skill from the Copilot plugin manifest;
that doc also states a push declined there must not be retried in the hope of a
quieter second attempt. In VS Code the prose is unreachable — the prompt files are
intent capsules and their ${CLAUDE_PLUGIN_ROOT} paths do not resolve (see
Limitations) — but nothing is lost there, since VS Code has no hooks and so never
raises the repeat-push decision the caveat is about.
The advisory spec-first / issue-first
nudges — and the issue-create contract guard that also lives in
check-bash-actions.sh — are not ported as hooks (Copilot's preToolUse
cannot inject non-blocking context); their intent is carried by the standards in
.github/copilot-instructions.md.
copilot-hooks.json is generated from hooks.json by gen_copilot_hooks.py
(mise run gen:copilot): the ported subset is declared in the generator's
COPILOT_HOOKS table, and it reshapes each selected hook into Copilot's flat
schema — adding STEER_HOOK_TARGET=copilot and the fail-open || true, and
mapping timeout → timeoutSec. It is emitted as strict JSON (no header
comment), because the Copilot CLI hook parser is not documented to accept JSONC —
unlike the VS Code mcp.json mirror, which is JSONC. A byte-equality drift gate
(check_copilot_hooks.py, part of plugin-check) fails the build if the
committed manifest drifts, and additionally verifies each referenced script still
exists on disk. Renaming, dropping, or retiming a hook script on the Claude side
then fails the build until you regenerate, instead of silently leaving the Copilot
manifest pointing at a dead path.
VS Code has no hook mechanism at all — the gates are Copilot-CLI-only. In VS Code the version-pin and trunk-push policies live only as text in the standards.
Known limitations¶
- Skill enforcement/invocation differs. See Skills on Copilot
— tool-permission scoping is inert and skill bodies are intent capsules (though
the
steer-reviewersubagent now ports as a custom agent). - Two gates, soft, CLI-only. Only the version-pin and trunk-push graduation
gates are ported, as
asks, and only on the Copilot CLI. VS Code gets no hooks. The advisory nudges live in the standards text, not as hooks. - Invocation form differs by surface, and the instructions file is shared.
.github/copilot-instructions.mdcarries the rules verbatim, so every skill cross-reference in them reads/steer:<skill>. In VS Code the invocable form is/steer-<skill>(prompt files); on the CLI skills load from the plugin manifest. Because one file serves both surfaces, a blanket rewrite would be wrong for one of them — the generated file therefore opens with a note stating the mapping. The prompt artifacts are rewritten to the hyphen form bygen_copilot_prompts.py. - The two ported gates depend on
CLAUDE_PLUGIN_ROOT.copilot-hooks.jsonbuilds each script path from that Claude-named variable. Whether the Copilot CLI exports it is unverified — so treat the gates as ported, not proven. They are guarded on the resolved path and report `CLAUDE_PLUGIN_ROOT unresolved —