Keeping Copilot Instructions in Sync Across Repositories

This started with a concrete annoyance. I maintain a central PowerShell Copilot Standards repository, the one place where my Copilot instructions actually live and get maintained. Recently I went in and updated those instructions: fixed some rules, update Pester 6 specific instructions/migrations, updated PowerShell to 7.6, etc. The changes were good. The problem was everything downstream.

Every other project I own had copied those instruction files at some earlier point in time. My update improved the source, but the copies did not move. Each of those repos was now running on the old guidance, and there was no signal telling me so. A file that never changes never asks for attention, so the drift was completely invisible. I had a fresh set of standards in one repo and stale duplicates scattered across all the others, and the only way to fix it was to remember to go touch each one by hand.

That is a chore I knew I would forget the next time, so I built a small GitHub Actions workflow to do the remembering for me. It keeps a project's Copilot instruction files in sync with the standards repository and opens a pull request whenever they drift apart, which means the next time I update the source, the change trickles down to every consuming repo on its own. This post explains what it addresses, how it works, and how to drop it into your own project. You can grab the file itself from the companion repo: sync_copilot_standards.yml.

The problem: copy once, drift forever

Copilot instruction files live under .github in a repository. The main one is .github/copilot-instructions.md, and alongside it you can keep scoped guidance in .github/instructions/ and reusable prompts in .github/prompts/. These files shape how Copilot behaves in that project, which makes them worth maintaining carefully.

The trouble is that most teams maintain them in one place, a standards repository, and then copy them into each consuming project. That copy is a snapshot. The moment you improve the originals, every downstream copy is a little bit wrong, and the gap only grows. There is no error, no failing build, no red X. The files just sit there, slowly diverging from the guidance you actually intend people to follow.

Manual syncing does not fix this in practice. It relies on someone remembering, across every repository, every time the standards change. That someone eventually forgets, and the drift becomes invisible again.

What the workflow does

The idea is simple: let a scheduled job do the remembering. Once a week the workflow checks out both your project and the standards repository, copies the three instruction paths across, and compares them to what your project currently has. If they match, it does nothing and says so. If they differ, it opens a pull request with the exact diff, so a human reviews the change before anything lands on your default branch.

That last part matters. The workflow does not push directly to your main branch. It proposes changes through a pull request, which means you keep a review step and a clean history of what changed and when.

Here is the shape of the job, step by step.

First it checks out the consuming repository, then checks out the standards repository into a separate .standards folder so the two never collide:

- name: Check out this repository
  uses: actions/checkout@v7
- name: Check out the standards repository
  uses: actions/checkout@v7
  with:
    repository: ${{ env.STANDARDS_REPO }}
    path: .standards

Next it mirrors the files. The single top-level instructions file is copied over directly. The two directories are removed and recopied wholesale, which is what makes this a mirror rather than a merge:

src=.standards/.github
cp "$src/copilot-instructions.md" .github/copilot-instructions.md
for dir in instructions prompts; do
  rm -rf ".github/$dir"
  cp -r "$src/$dir" ".github/$dir"
done
rm -rf .standards

Removing the directories before copying is deliberate. If you only copied files in, a prompt that was deleted upstream would linger downstream forever. By clearing the directory first, deletions in the standards repo actually propagate.

Then it detects drift. The key detail here is the git add -A before the comparison. A file that was added upstream is untracked in your project, and a plain git diff would not see it. Staging first makes the comparison honest:

git add -A .github
if git diff --cached --quiet -- .github; then
  echo "changed=false" >> "$GITHUB_OUTPUT"
else
  echo "changed=true" >> "$GITHUB_OUTPUT"
  git diff --cached --stat -- .github
fi

Finally, only when drift was detected, it opens the pull request using peter-evans/create-pull-request. The branch is reused and deleted after merge, so you do not accumulate stale sync branches.

Mirror, not merge

The most important thing to understand before you install this is that it overwrites. Local edits to the three mirrored paths are replaced on the next run. That is not a bug, it is the entire point. A copy that drifts silently is exactly the problem the workflow exists to solve, so allowing local edits to survive would defeat it.

This has a practical consequence for how you customise. If you want to change one of these files, change it upstream in the standards repository, where every project benefits. If you need guidance that is specific to one project, put it in a file outside the mirrored paths. A scoped *.instructions.md under a different directory, referenced from that project's own VS Code settings, survives every sync untouched.

For clarity, here is precisely what the job touches and what it leaves alone:

Path Behaviour
.github/copilot-instructions.md Overwritten from upstream
.github/instructions/ Mirrored, upstream deletions applied
.github/prompts/ Mirrored, upstream deletions applied
.github/workflows/ Never touched, including this workflow itself
Everything else Never touched

Your other workflows are safe. The sync job stays out of .github/workflows/ entirely, so it will not overwrite itself or anything else you have running there.

How to use it

Copy the file to .github/workflows/sync-copilot-standards.yml in the project you want to keep in sync. If you maintain the standards with an install script, you can pull it in at the same time as the standards themselves.

There is one repository setting you have to enable before the first run, and it trips people up because the failure is misleading. The job needs permission to open pull requests:

Settings, then Actions, then General, then Workflow permissions, then "Allow GitHub Actions to create and approve pull requests."

This is a per-repository setting and it is off by default in many organisations. Without it, the job runs, detects drift correctly, and then fails at the very last step when it tries to create the pull request. So if your first run gets all the way through the diff and then dies, this setting is almost certainly why. Turn it on, or trigger the workflow manually once to confirm everything is wired up.

By default the job runs on a schedule, every Monday at 06:00 UTC, and it also responds to workflow_dispatch, so you can run it on demand from the Actions tab. One small gotcha worth knowing: the manual run button only appears once the workflow file is on your default branch, so do not go looking for it while the file is still on a feature branch.

Configuration

There is really only one line most people need to change. If you maintain a fork of the standards, point STANDARDS_REPO at it:

Setting Default Notes
env.STANDARDS_REPO fadwen/Powershell-Copilot-Standards The only line to change if you maintain a fork
schedule.cron 0 6 * * 1 Stagger this if several repositories sync at once
branch chore/sync-copilot-standards Reused and deleted after merge

If you have many repositories syncing from the same source, spread their cron times out a little rather than firing them all at 06:00. It is polite to the standards repo and it keeps your own Actions minutes from spiking all at once.

A note on action versions

The workflow pins actions/checkout@v7 and peter-evans/create-pull-request@v8. These are pinned to majors that run on the Node 24 runtime, which keeps the job clear of the Node 20 runtime deprecation warnings that older action versions now emit. If you copy the pattern into other workflows, it is worth checking that your actions are on Node 24 majors too.

Why bother

The value of this workflow is not that it does something clever. It is that it removes a decision from the future. The next time I improve the PowerShell Copilot Standards, I do not have to also remember which repos copied the old version and go update each one. The improvement trickles down on its own, and it arrives as a reviewable pull request rather than a silent overwrite. Without the workflow, keeping instructions current depends on someone noticing drift and acting on it, and that person is unreliable by nature, because the drift is invisible. With it, the noticing is automatic. You still decide whether to merge, but you can no longer forget to look.

If you keep any kind of shared guidance across repositories, whether it is Copilot instructions or something else entirely, the same pattern applies: mirror from one source, detect drift on a schedule, and surface it as a pull request. It is a small amount of YAML that buys you a guarantee you would otherwise have to remember to keep by hand.