Automating GitHub Copilot CLI with GitHub Actions

GitHub Copilot CLIAcademy lesson 63Cluster 5 · Lesson 12 of 12Advanced17 min readVersion-sensitive
Published
Updated
Last technically verified
Automating GitHub Copilot CLI with GitHub ActionsGitHub Copilot CLI12Advanced/github-copilot/cli/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.

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 }}
Example policyA CI review job: read the diff, produce a report, touch nothing.
Example tool permission policy
PatternDecisionWhy
shell(git:*)AllowedReading the diff and history is the whole task.
writeDeniedA review job produces a report, not a commit.
urlDeniedNo reason to fetch anything; closes an exfiltration path.
shell(git push:*)DeniedDenial 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

Read first, write later

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.md added 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.jsonl

Be 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.

Copilot prompt

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.md

The 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.

Primary sources