Automating GitHub Copilot CLI with GitHub Actions
Everything before this lesson had a human at a terminal. Automation removes them, and with them every control this cluster has relied on: reading approval prompts, noticing scope expansion, declining a command that looked wrong.
What replaces them is configuration decided in advance. That is a weaker control and it is not a bad one — provided the first workflow you build reads rather than writes, and provided you understand what happens when the repository content itself is untrusted.
Key takeaways
- Workflows need
copilot-requests: writeand can authenticate with the built-inGITHUB_TOKEN. No personal access token is required. COPILOT_GITHUB_TOKENtakes precedence overGH_TOKENandGITHUB_TOKEN, so Copilot can be given a narrower token than the rest of the job.- Non-interactive runs need permissions set in advance.
--no-ask-userprevents stalling on a question nobody can answer. --yolois defensible on an ephemeral runner and nowhere else. Even then,--deny-toolstill holds — denial beats every allow.pull_request_targetruns with repository secrets and write access. Combining it with a checkout of untrusted code is the single most dangerous pattern in this lesson.- Start with analysis that produces a report. Automating modification is a separate decision.
The minimal working workflow
GitHub’s documented starting point:
name: Copilot CLI example
on: [push]
permissions:
contents: read
copilot-requests: write
jobs:
copilot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Copilot CLI
run: npm install -g @github/copilot
- name: Run Copilot
run: copilot --yolo -p "Summarize the changes in this commit"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Four things are load-bearing.
copilot-requests: write is the permission that makes Copilot requests
possible from a workflow. Without it the job fails to authenticate.
contents: read is deliberately not write. This job reads a repository and
summarises; it has no reason to be able to push.
GITHUB_TOKEN is the built-in token. Since July 2026 no personal access token
is needed for this.
--yolo removes interactive prompts, which a runner cannot answer.
Prefer explicit tool permissions
--yolo is the blunt option. A job with a defined task usually needs a much
shorter list, and writing it costs one line.
- name: Review changes
run: |
copilot -p "Summarise the changes in this pull request" \
--allow-tool='shell(git:*)' \
--no-ask-user \
-s
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}| Pattern | Decision | Why |
|---|---|---|
shell(git:*) | Allowed | Reading the diff and history is the whole task. |
write | Denied | A review job produces a report, not a commit. |
url | Denied | No reason to fetch anything; closes an exfiltration path. |
shell(git push:*) | Denied | Denial holds even if someone later adds --allow-all-tools. |
Written for this lesson to illustrate least privilege. Not a GitHub default and not a security guarantee — read every rule before using it.
Token scoping
Copilot CLI checks COPILOT_GITHUB_TOKEN, then GH_TOKEN, then GITHUB_TOKEN.
That ordering is useful: a job needing broad access for other steps can still hand
Copilot something narrower.
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.NARROW_COPILOT_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}The safe first automation
The first workflow should produce a report, not a change. Reviewing a pull request or summarising a release cannot break anything, which means a misconfiguration costs you a bad comment rather than a bad commit.
name: Copilot review
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: write
copilot-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- run: npm install -g @github/copilot
- name: Generate review
run: |
copilot -p "Review the changes on this branch against the base branch. Report correctness and security concerns with file and line. Do not modify any files." \
--allow-tool='shell(git:*)' \
--deny-tool='write' \
--deny-tool='url' \
--no-ask-user \
-s > review.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post as a comment
run: gh pr comment "$PR" --body-file review.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}Note the split: Copilot produces text with no write access at all, and a separate
step — plain gh, no model involved — posts it. Keeping generation and publication
in different steps means the agent never needs the permission that posts.
Forked pull requests
This is the section that matters most, and the one most likely to be skipped.
The pull_request event, for a fork, runs with a read-only token and no
access to repository secrets. That is deliberate, and it is what makes the
workflow above safe: a malicious contributor gets an agent that can read a
checkout of their own code and nothing else.
pull_request_target is different. It runs in the context of the base
repository, with repository secrets and a writable token. It exists for
workflows that legitimately need to label or comment on external contributions.
Prompt injection is the real threat model
In CI there is no human to notice. That changes the analysis.
An agent reviewing a pull request reads files the contributor wrote. Any of them can contain text addressed to the agent:
- A comment in a source file
- A line in a README
- An
AGENTS.mdadded in the pull request - A test fixture
- A commit message
An AGENTS.md is the sharpest case, because instruction files are designed to
influence the agent. A contributor adding one is contributing configuration to
your automation.
The layered mitigations, in order of effectiveness:
Deny the tools that matter. An agent that cannot write and cannot reach the network is a poor target, because injected instructions have nothing useful to make it do.
Keep secrets out of the job. A job with no secrets has nothing to exfiltrate.
Separate generation from publication. The agent produces text; a later step publishes it. Injection can produce a misleading comment, not a commit.
Do not check out untrusted code in a privileged context.
Treat output as untrusted. Model output derived from attacker-influenced input should not be fed into a step that executes it.
A second workflow: the modification tier
Once the review workflow has run for a few weeks and you know its false-positive rate, a modification workflow becomes reasonable. The control is that it opens a pull request rather than committing.
name: Copilot maintenance
on:
workflow_dispatch:
inputs:
task:
description: What should Copilot do
required: true
permissions:
contents: write
pull-requests: write
copilot-requests: write
jobs:
maintain:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npm install -g @github/copilot
- name: Run the task
run: |
copilot -p "${{ inputs.task }}" \
--allow-tool='shell(git:*)' \
--allow-tool='write' \
--deny-tool='shell(git push:*)' \
--deny-tool='write(.github/workflows)' \
--no-ask-user -s
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Open a pull request
run: |
git checkout -b "copilot/$GITHUB_RUN_ID"
git add -A
git commit -m "Automated change: ${{ inputs.task }}" || exit 0
git push origin HEAD
gh pr create --fill --label automated
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Four deliberate choices.
workflow_dispatch means a human triggers it with a specific task. There is
no automatic path from repository content to an agent with write access.
--deny-tool='shell(git push:*)' means the agent cannot push. The workflow
pushes, in a later step, to a branch it names. That separation is what makes the
pull request an actual gate rather than a formality.
--deny-tool='write(.github/workflows)' prevents the agent editing the
workflows themselves. An automation that can rewrite its own permissions is not
constrained by them, and this denial is cheap insurance against a whole category
of escalation.
|| exit 0 on the commit handles the agent correctly deciding nothing needed
changing. A workflow that fails when there is nothing to do trains people to
ignore its failures.
Structured output for programmatic use
When another step consumes the result rather than a person reading it,
--output-format json emits JSONL — one JSON object per line.
- name: Analyse and gate
run: |
copilot -p "List every TODO comment introduced by this branch. Output nothing else." \
--allow-tool='shell(git:*)' --allow-tool='shell(rg:*)' \
--no-ask-user -s --output-format json > findings.jsonl
# A later step parses findings.jsonlBe realistic about what this buys. The format is structured; the content is still model output and still varies. Parsing is more reliable than scraping prose, and neither is a guarantee that the same input produces the same findings twice.
Anything that gates a merge should be deterministic — a test, a linter, a scanner. Model output belongs in advisory positions, and building a required check on it produces a flaky gate that people will eventually route around.
Cost, determinism and failure
Cost. Agentic runs consume AI credits, and a workflow on every push to every
branch multiplies quickly. --max-ai-credits bounds a run;
--max-autopilot-continues bounds continuation. Trigger on pull_request rather
than push where you can, and consider path filters so a documentation change does
not invoke a code reviewer.
Determinism. Model output varies between runs. A workflow asserting on the
content of a response is fragile. --model pins the model, and
--output-format json gives structure — but a review comment is advisory, and
building a required status check on top of it is asking for a flaky gate.
Failure. Decide whether a Copilot step failing should fail the build. For an
advisory review, usually not — continue-on-error: true keeps a model outage from
blocking merges. For a job whose output is load-bearing, it should fail loudly
rather than silently producing nothing.
Debugging a workflow that does not behave
Agentic CI failures look different from ordinary CI failures, and the usual instincts are not much help.
The job hangs. Something asked for approval or for a clarification. Add the
tool to --allow-tool, and add --no-ask-user so the agent cannot pause on a
question. This is the most common first failure.
Output is empty. Either the prompt produced nothing, or -s is missing and
you are parsing statistics as content. Run without -s once and read the raw
output.
It works locally and not in CI. Usually the environment: a tool present on
your machine and absent on the runner, a different working directory, a
fetch-depth of 1 hiding the history a git diff needs. fetch-depth: 0 is the
fix for the last one and catches people repeatedly.
Different results each run. Expected. Pin --model to reduce variation, and
do not build assertions on exact content.
Authentication fails. Check copilot-requests: write is in the permissions
block. Then check the organisation has not disabled CLI access, which is a policy
problem no workflow change fixes.
Organisation policy and enterprise controls
Two things can override anything in your workflow file, and both are worth knowing before you spend a day debugging.
Copilot CLI access can be disabled at organisation or enterprise level. When it is, authentication fails regardless of tokens and permissions. The failure looks like a credential problem and is not.
Sandboxing can be enforced by policy. Where an organisation mandates it,
/sandbox disable is refused and a local sandbox.enabled: false is overridden.
If the policy also sets allowBypass: false, a session cannot run shell commands
at all — which in CI means every shell tool call fails. That is a deliberate
control, and the fix is with an administrator rather than in the workflow.
Maturity, one step at a time
Analysis, reporting to a human. Review comments, change summaries, dependency notes. Cannot break anything.
Analysis with structured output. JSON consumed by another step. Still no writes.
Modification behind a gate. The agent opens a pull request; a human reviews it. This is the first tier with write access, and the pull request is the control.
Autonomous modification. Committing directly. Justifiable in narrow, well-tested cases — a formatting job, a changelog update — and only with paths restricted and branch protection intact.
Choosing a trigger
The trigger determines who can cause your agent to run and with what context. It is a security decision more than a convenience one, and the options differ more than they appear.
workflow_dispatch — a human starts it, with parameters. The safest trigger,
and the right one for anything with write access. There is no path from repository
content to execution.
schedule — runs on a timer against your default branch, which is trusted
code. Good for periodic maintenance: dependency review, stale issue triage,
documentation drift.
pull_request — runs for pull requests. For forks, the token is read-only and
secrets are unavailable, which is what makes agentic review of external
contributions tractable at all.
push — every push. Expensive with an agent, and rarely what you want.
issue_comment — lets someone invoke the agent by commenting. Convenient and
worth thinking hard about: the comment body is attacker-controlled text that
becomes part of a prompt. Gate on author association, and never pass a comment
body directly into -p without treating it as untrusted.
Path filters and scoping
An agentic job on every pull request to a large repository is expensive and mostly useless — a documentation typo does not need a security review.
on:
pull_request:
paths:
- 'src/**'
- '!**/*.md'Scoping the prompt matters as much as scoping the trigger. A review asked to consider everything produces shallow output; a review asked about one concern in one directory produces findings worth reading. The same narrowing that helps interactively — covered in the code review lesson — helps more in CI, because nobody is there to ask a follow-up question.
Review the changes under src/api/ on this branch against the base branch.
Focus on one thing: how user-supplied input reaches a database query, a filesystem path, or a subprocess call.
For each path from input to sink, state what validation exists between them. If validation is present and adequate, say so.
Do not comment on style, naming, or test coverage.
“If validation is present and adequate, say so” is worth including. Without an explicit instruction covering the healthy case, a review agent tends to produce marginal findings rather than reporting that things look fine — and a CI comment full of marginal findings is one people learn to ignore.
Using a custom agent in CI
Restricted agents pay off most here. An agent whose tools list is
["read", "search"] carries its restriction into the workflow, so the guarantee
lives in a reviewed file rather than in flags copied between YAML files:
- run: |
copilot --agent security-reviewer \
-p "Review the changes on this branch" \
--no-ask-user -s > findings.mdThe tool restriction is in .github/agents/security-reviewer.agent.md, version
controlled and reviewed. That is a much better property than a workflow author
reconstructing the right --allow-tool flags, and it means a change to the
agent’s capabilities shows up as a diff in a file people review.
What to automate first, concretely
Teams tend to reach for the most impressive automation and get burned. A more useful order, roughly in ascending risk.
Change summaries on pull requests. Read the diff, describe it in prose. Almost impossible to get wrong, immediately useful on large pull requests, and it teaches you how the agent reads your codebase.
Release notes from merged commits. Scheduled, runs against your default branch, no untrusted input.
Dependency update review. When a bot opens a dependency pull request, have the agent summarise what actually changed upstream and whether the changelog mentions anything breaking. High value, entirely read-only.
Test-gap analysis. “Which changed functions have no corresponding test change.” Advisory, and it points at a real class of problem.
Documentation drift. Compare a changed public interface against the documentation that describes it. This one benefits enormously from the agent having read both.
Only after those, and only with a pull request as the gate, does modification become a reasonable next step.
Reviewing the workflow itself
A workflow invoking an agent is a security-relevant file, and it should be reviewed as one. Five things to check on any pull request that adds or edits one.
The permissions: block. Is contents: read or write, and does the job
genuinely need write? This is the highest-value line in the file.
The trigger. Does pull_request_target appear, and if so is untrusted code
checked out anywhere in the same job?
The tool flags. Is --yolo present, and is the permissions block tight enough
to justify it? Are the denials still there?
Secret exposure. Which secrets enter the job environment, and does the agent need them? A job with fewer secrets is a smaller problem when something goes wrong.
What happens to the output. Does model output flow into a step that executes it? That is the pattern that turns a bad review comment into a bad command.
What to carry forward
copilot-requests: write plus GITHUB_TOKEN. No PAT needed.
Keep contents: read unless the job genuinely commits.
Add --deny-tool even with --yolo. Denial is the one control that survives
later edits.
Never check out untrusted code in a pull_request_target job.
Use --no-custom-instructions where the repository content is untrusted.
Separate generation from publication.
Start with reports. Earn write access.
One closing observation about automation generally. Every control in this lesson is structural — a permission, a trigger, a separation between steps. None of them asks the model to behave. That is deliberate, and it is the property to preserve as you extend this: a workflow whose safety depends on the agent making a good decision is a workflow whose safety you cannot reason about.
Auditing what your automation actually did
An unattended agent produces a record, and reading it occasionally is how you find out whether the automation is doing what you think.
--share[=path] writes the session to Markdown at the end of a non-interactive
run, which uploaded as a workflow artifact gives you the full transcript of what
the agent read and did. --output-format json gives structured turns for
anything you want to aggregate.
What to look for on the occasions you read one: tools invoked that you did not expect, files read outside the area the job was scoped to, and reasoning that reached the right answer for a wrong reason. That last category is the one that predicts future failures, because it will not always land right.
Cluster complete
That is Cluster 5. Across twelve lessons the argument has been a single one: Copilot CLI is an agent with access to developer tools, and permission, validation and human review are what define the trust boundary.
You have the pillar for the model, installation and the commands reference for the mechanics, the tutorial for the habits, Linux, Bash, Python and DevOps for the domains, instructions and agents for making configuration durable, and code review plus this lesson for verification and automation.
Cluster 6 — Prompting, Models & Customization — is next, covering prompt engineering for code, model selection and trade-offs, and the AI credit economics this lesson only touched.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Your progress
Saved in this browser only. No account, no server, and nothing leaves your device. Clearing site data resets it.Sync across devices if you want it everywhere.
Saved in this browser and synced to your account, so it follows you between devices. Manage or delete it.
Was this lesson helpful?
We record which lesson you rated and whether it helped. Nothing identifies you — no account, no cookie, no session.