Explainer
copilot-setup-steps.yml Explained: Prepare GitHub Copilot Coding Agent for Python and Node.js
What copilot-setup-steps.yml does, the one job it must contain, which keys count, and complete Python and Node.js examples with least-privilege permissions.
Short answer. copilot-setup-steps.yml is a GitHub Actions workflow file
at .github/workflows/copilot-setup-steps.yml that runs before GitHub
Copilot’s cloud agent starts a task, so the agent begins with your
dependencies installed instead of discovering them by trial and error. It
must contain exactly one job, named copilot-setup-steps; it only takes
effect once it is on the default branch; and of everything a workflow job can
say, the agent honours six keys — steps, permissions, runs-on,
services, snapshot and timeout-minutes (max 59) — and ignores the rest.
Two complete files are below, one for a
Python/FastAPI service and one for a
Node.js/TypeScript project. What was and was
not validated about them is stated plainly.
GitHub renamed the feature this year: what the documentation called “Copilot coding agent” is now “Copilot cloud agent”. The file name did not change, and this article uses both terms. The cloud agent lesson explains the environment the file configures; this page is only about the file.
Facts were read from GitHub’s documentation on 18 September 2026 and are quoted where they matter. The examples were validated as described below, not executed by the agent — see the validation section before trusting them.
What the file is for
The cloud agent works in “its own ephemeral development environment, powered by GitHub Actions, where it can explore your code, make changes, execute automated tests and linters and more.” Without help, it has to install your project’s dependencies itself. GitHub’s own assessment of how that goes: “Copilot can discover and install these dependencies itself via a process of trial and error, but this can be slow and unreliable, given the non-deterministic nature of large language models, and in some cases, it may be completely unable to download these dependencies — for example, if they are private.”
The setup-steps file makes that deterministic. Its job runs first; then the
agent starts with whatever the job left behind — a populated virtual
environment, a node_modules, a compiled toolchain. The practical effect is
that the agent can run your tests before it pushes, which is the difference
between a pull request that passes CI and one that does not.
The same file customises Copilot code review’s environment too: “By
default, Copilot code review reuses your copilot-setup-steps.yml file”,
unless a dedicated .github/workflows/copilot-code-review.yml exists, in
which case that one wins for review.
Where it lives, and the rule people miss
Three facts decide whether the file is picked up at all.
- Path.
.github/workflows/copilot-setup-steps.yml. Not.github/copilot-setup-steps.yml, notworkflows/setup.yml. - Job name. The file “must contain a single
copilot-setup-stepsjob”. GitHub’s comment in its own example is blunter: “The job MUST be calledcopilot-setup-stepsor it will not be picked up by Copilot.” A second job in the file is not run by the agent. - Default branch. “The
copilot-setup-steps.ymlworkflow won’t trigger unless it’s present on your default branch.” A file that exists only on the feature branch where you are testing it does nothing for the agent until it is merged. This is the one that produces “I added the file and nothing changed.”
What you can set, and what is ignored
Inside the job, only these are honoured: steps, permissions, runs-on,
services, snapshot, timeout-minutes (maximum 59). “If you try to
customize other settings, your changes will be ignored.” So env at job
level, container, strategy, concurrency, outputs and needs are
silently dropped. If you need an environment variable for a step, set it on
the step with env:, or export it inside a run: block.
One more override: any fetch-depth on actions/checkout “will be
overridden to allow the agent to rollback commits upon request”. Set it if
you like; it will not be what runs.
Two behaviours to plan around:
- You do not have to check out the code. “If you do not check out your
code, Copilot will do this for you.” You only need
actions/checkout— and thereforecontents: read— if a later step needs the repository present, which installing dependencies from a lockfile does. - A failing step does not stop the agent. “If any setup step fails by returning a non-zero exit code, Copilot will skip the remaining setup steps and begin working with the current state of its development environment.” A broken step degrades the environment quietly; the agent still runs and still opens a pull request. Watch the session log for it.
Permissions and least privilege
GitHub’s example carries the guidance in a comment: “Set the permissions to the lowest permissions possible needed for your steps. Copilot will be given its own token for its operations.” The setup job’s token is for the setup job — cloning, fetching packages — and nothing else. In practice:
permissions:
contents: readis the whole of what a dependency-install job needs, and it is only needed
because actions/checkout reads the repository. A job that installs a
language toolchain and nothing from the repository can declare
permissions: {}.
Do not grant contents: write, pull-requests: write or packages: write
here on the theory that the agent might need them. The agent’s own token is
separate and scoped by GitHub; widening the setup job widens only what a
compromised setup step could do. The
GitHub Actions lesson’s permissions section
is the general rule; the setup-steps job is the case where it is easiest to
follow.
Secrets are configured separately, as “Agents” secrets and variables in the repository’s Copilot settings, not as workflow secrets — so a private package registry token belongs there, referenced from a step, never written into the file.
Example 1: Python/FastAPI
A service with pyproject.toml, an optional dev dependency group (pytest,
ruff, mypy), and a lockfile produced by pip-compile or uv. Adjust the
Python version to your requires-python.
name: "Copilot Setup Steps"
# Run on changes to this file so it validates like any other workflow, and
# on demand from the Actions tab. Nothing else: this is not CI.
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
# The job MUST be named copilot-setup-steps or the agent ignores the file.
copilot-setup-steps:
runs-on: ubuntu-latest
timeout-minutes: 15
# Only what the steps below need. Copilot's own work uses its own token.
permissions:
contents: read
steps:
- name: Check out the repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: |
pyproject.toml
requirements*.txt
- name: Install the project and its dev dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements-dev.txt ]; then
pip install -r requirements-dev.txt
fi
pip install -e ".[dev]"
- name: Prove the environment works
# If this fails, the agent starts anyway — but the session log will
# show it, and that is where to look when the agent "cannot run tests".
run: |
python -c "import fastapi, pydantic; print('fastapi', fastapi.__version__)"
ruff --version
pytest --versionWhy each part is the way it is:
on:is narrow on purpose. The file is run as a normal workflow when it changes, “so you can see if it runs successfully”, and GitHub notes that “you wouldn’t typically run on bothpushandpull_request” for a real project — here both are path-filtered to the file itself, so it runs on the pull request that edits it and once more when that lands. It is not your test suite.cache: pipkeeps repeat runs fast. The agent’s environment is ephemeral; the cache is the one thing that persists between sessions.- The last step is a smoke test, not a test run. Its purpose is to make a broken install visible in the session log. Running the whole test suite here would spend minutes before every agent session on work the agent will do again.
Example 2: Node.js/TypeScript
A project with package-lock.json, TypeScript, ESLint and a test script.
Adjust the Node version to your engines field.
name: "Copilot Setup Steps"
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Check out the repository
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: "22"
cache: npm
- name: Install dependencies from the lockfile
# npm ci, not npm install: the lockfile is the contract, and the agent
# should not be able to drift it by installing.
run: npm ci
- name: Build once so type errors surface here, not in the agent's first attempt
run: npx tsc --noEmit
- name: Prove the toolchain works
run: |
node --version
npx eslint --version
npm run test -- --version >/dev/null 2>&1 || echo "no test runner version flag; that is fine"Notes:
npm ciovernpm install.cirefuses to run if the lockfile andpackage.jsondisagree, which is exactly the failure you want surfaced before the agent starts editing.- The
tsc --noEmitstep is optional but cheap. A repository that does not type-check onmainis one where the agent will spend its first minutes discovering that; better the setup log says so. - Monorepos with workspaces.
npm ciat the root installs every workspace. If the agent only ever works in one package,npm ci --workspace packages/apiis faster and narrower. The monorepo instructions article covers the instruction-file side of the same repository.
Validation: how to know it is being used
Three checks, in order, each of which the documentation describes.
- The workflow ran on its own. After you commit the file, it “will automatically be run as a normal GitHub Actions workflow when changes are made … This will show alongside other checks in a pull request where you create or modify the file.” A red check here is a setup problem you can fix before an agent ever sees it.
- Run it by hand from the Actions tab after merging: “you can manually
run the workflow from the repository’s Actions tab at any time to check
that everything works as expected.” That is what
workflow_dispatchin the examples is for. - Read the session log. “When Copilot starts work, your setup steps will be run, and updates will show in the session logs.” Assign the agent a small task, open the session from the pull request timeline, and confirm the setup steps appear at the top and each exits zero. If they are absent, the file was not picked up — re-check the path, the job name, and the default branch.
Debugging failures
The steps do not appear in the session log at all. Path, job name,
default branch — in that order. Then confirm the cloud agent is actually
enabled for the repository; GitHub’s troubleshooting page points at
github.com/settings/copilot/features for individuals and at the
organization’s Copilot settings otherwise.
The steps appear and one fails. The agent continues with whatever state the failing step left. Reproduce by running the workflow from the Actions tab — same runner, same steps — and read the failing step’s output there, where it is easier to see than in the session log.
A dependency cannot be fetched. The agent’s environment sits behind a firewall by default: “Limiting access to the internet helps to manage data exfiltration risks.” A blocked request adds a warning to the pull request body or comment “showing the blocked address and the command that tried to make the request”. Public registries are allowed; a private registry or an internal mirror needs the firewall’s allow list extended, or a self-hosted runner. Do not disable the firewall to make one package install.
The agent’s pull request fails CI even though setup passed. Two
different things. Setup steps prepare the environment; whether the agent
uses it to run tests before pushing depends on what you told it. GitHub’s
troubleshooting page: it “is most likely to do this if given clear
instructions on what to do. The best way to do this is with a
.github/copilot-instructions.md file.” Tell it, in that file, which command
is the definition of done. The Setup Kit
generates an instruction file whose “before completing a task” section says
exactly that.
The workflow does not run when the agent pushes. Separate again: “GitHub Actions workflows will not run automatically when Copilot pushes changes to a pull request.” A person with write access clicks Approve and run workflows in the merge box. This is a safety default, not a setup-steps problem.
Security considerations
The setup job runs arbitrary steps you wrote, on a GitHub-hosted runner, with the permissions you granted, before an AI agent starts editing your repository. Three consequences.
Pin what you run. actions/checkout@v6 and actions/setup-node@v7 are
major-version tags in the examples for readability; the
Actions lesson on pinning
explains why a commit SHA is the stricter choice and how to keep it updated.
A run: step that pipes curl into sh is exactly as dangerous here as
anywhere else.
Keep permissions at contents: read. The point of a separate setup token
is that a supply-chain compromise in a setup action cannot write to your
repository. Granting more here throws that away.
Treat the file as configuration, not documentation. A pull request that
edits copilot-setup-steps.yml is proposing to change what runs before every
agent session. Review it as you would a change to CI, and note that it runs
on that pull request when path-filtered triggers are present — for outside
contributors on public repositories, the
forked pull request rules
apply.
Self-hosted runners change the picture: the built-in firewall “is not compatible with self-hosted runners” and must be disabled, so your own network controls become the only boundary. GitHub recommends “ephemeral, single-use runners that are not reused for multiple jobs”.
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| File on a feature branch only | Nothing; the agent never sees it | Merge to the default branch |
Job named setup or build | Ignored | Rename to copilot-setup-steps |
| Two jobs in the file | Only the correctly named one runs | One job; put everything in its steps |
Job-level env: or container: | Silently ignored | Use step-level env:, or install in a step |
timeout-minutes: 90 | Capped; the job is limited to 59 | Trim the steps; cache |
npm install instead of npm ci | Lockfile drift, slower runs | npm ci |
| Running the full test suite in setup | Minutes spent before every session on work the agent repeats | A smoke check only |
permissions: write-all “to be safe” | The opposite of safe | contents: read |
| Expecting the agent to run tests because they are installed | It runs them when instructed | Say so in copilot-instructions.md |
What was validated
Stated exactly, because “tested” means something.
Statically validated, in this repository’s build: both examples parse as
YAML; each contains exactly one job named copilot-setup-steps; every key on
that job is one of the six the documentation says is honoured;
timeout-minutes is at or below 59; permissions grants only contents: read; every uses: is a pinned major version of a first-party
actions/* action. A test in the site’s suite
(tests/setup-steps-yaml.test.ts) re-runs those checks, so an edit to this
page that breaks a rule fails the test run.
Not executed: neither file has been run by GitHub Actions or by the
Copilot cloud agent from this environment, which has no GitHub Actions and no
Copilot access. The run: steps are ordinary shell and were not run here
against a real project. Before relying on either file, run it yourself from
the Actions tab (validation step 2) and read one session log (step 3).
Frequently asked questions
Does it replace my CI workflow? No. It prepares the agent’s environment; your CI still runs on the agent’s pull request, after a person approves the workflow run.
Can I use a different runner?
Yes — runs-on is honoured, for larger GitHub-hosted runners or a
self-hosted scale set. Only Ubuntu x64 and Windows 64-bit are supported;
“Runners with macOS or other operating systems are not supported.”
Can I run Docker services for integration tests?
services is one of the honoured keys, so a Postgres or Redis service
container can be declared on the job as in any workflow.
Does the agent see my job-level environment variables?
No; job-level env is not among the honoured keys. Use Copilot’s Agents
variables and secrets in the repository settings for values the agent should
have, and step-level env: for values a setup step needs.
What if I need Windows? Supported, but the integrated firewall “is not compatible with Windows”, so GitHub recommends self-hosted runners or larger runners with Azure private networking where you control the network yourself.
Does the same file configure code review?
By default, yes. Add .github/workflows/copilot-code-review.yml if review
needs a different environment; when it exists it is used for review instead.
Recap and next step
One file, one job with the exact name, on the default branch; six honoured
keys; contents: read; install from the lockfile; a smoke check at the end;
then confirm it in a session log. Pair it with an instruction file that names
the command the agent must run before it is done — the file makes the tests
possible, the instructions make them happen.
The Copilot Project Setup Kit generates that instruction file for a Python/FastAPI or TypeScript/Node.js repository in your browser. The cloud agent lesson explains the environment this file configures, the coding agent tutorial covers preparing a repository for delegation, and the Python and TypeScript lessons cover the conventions the agent will be held to. If you would rather start from maintained files — an instruction file per stack, with the reasoning kept — that is what The Copilot Stack Pro is for; the Terraform one is public in full.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Primary sources
Additional references
Was this lesson helpful?
We record which lesson you rated and whether it helped. Nothing identifies you — no account, no cookie, no session.