GitHub Copilot CLI Tutorial for Beginners

GitHub Copilot CLIAcademy lesson 55Cluster 5 · Lesson 4 of 12Beginner16 min readVersion-sensitive
Published
Updated
Last technically verified
GitHub Copilot CLI Tutorial for BeginnersGitHub Copilot CLI4Beginner/github-copilot/cli/tutorial/

Most Copilot CLI tutorials open the agent in a real project and ask it to do something useful. That is the wrong first move, for a reason that has nothing to do with capability: you cannot evaluate a tool’s judgement while you are also worrying about your working tree.

This tutorial uses a project you create in two minutes and delete afterwards. Every mistake is free, which is what makes it possible to actually look at what the agent does rather than hoping it works.

Build the sandbox project

Three files, deliberately containing one real bug.

mkdir copilot-cli-demo && cd copilot-cli-demo

app.py:

def parse_quantity(value):
    """Parse a user-supplied quantity into an int."""
    return int(value)


def total_price(unit_price, quantity):
    """Return the total price for a quantity of items."""
    return unit_price * parse_quantity(quantity)

test_app.py:

from app import total_price


def test_total_price_basic():
    assert total_price(2.5, "4") == 10.0

README.md:

# Copilot CLI Demo

A deliberately small project for learning GitHub Copilot CLI.
`parse_quantity` does not handle invalid input.

The bug is not subtle: parse_quantity("abc") raises ValueError, and parse_quantity("-3") cheerfully returns a negative quantity. That is intentional — we want something real to fix.

Initialise git, because the diff is the point of this exercise:

git init && git add -A && git commit -m "Initial demo project"

Why a throwaway project

It is tempting to skip this and open the CLI in something real. Three reasons not to.

You cannot evaluate and worry at the same time. Watching what an agent does well requires attention that is otherwise spent on whether your branch is safe.

A trivial project makes errors visible. In three files you know exactly what correct looks like, so a wrong explanation is obvious. In a real codebase a plausible-but-wrong answer reads like a correct one, and you learn nothing about how much to trust it.

Approving dangerous things is free here. Later in this tutorial you will deliberately let the agent make a change you do not want, purely to practise undoing it. That exercise is worth doing once, and it needs a project where the answer to “what if this goes wrong” is rm -rf.

Step 1 — Launch and trust

copilot

On first run in a directory, the CLI asks whether you trust it.

For this directory the answer is easy — you just created it, and it contains three files you wrote. That is exactly why it is the right place to learn what the prompt means, rather than meeting it for the first time on a repository you cloned an hour ago.

Step 2 — Ask, without permitting anything

The first prompt should be one where a wrong answer costs nothing.

Copilot prompt

Explain the structure of this repository and what each file does.

Do not modify any files.

Two things to evaluate in the response. Did it correctly identify that parse_quantity is the fragile part? And did it describe the actual code, or a plausible generic Python project?

This matters more than it sounds. An agent that misreads a three-file project will misread a three-hundred-file one, and you will not notice as easily. A cheap accuracy check at the start is worth more than careful prompting later.

Step 3 — Ask for a plan

Now something with consequences, but still without committing to an implementation.

Copilot prompt

I want parse_quantity to reject invalid input properly rather than raising a bare ValueError, and to reject negative quantities.

Use /plan first. Do not write code yet.

Read the plan against three questions:

Does it change the function signature? If it proposes returning None on failure rather than raising, that is a design decision affecting every caller, and it should be your decision.

Does it touch anything beyond app.py? For a change this size, it should not. Scope expansion at the planning stage is a warning about what implementation will look like.

Does it plan tests? A fix without a test for the invalid case has not demonstrated anything.

Disagreeing here costs one sentence. Disagreeing after implementation costs a review.

Step 4 — Let it make the change

Copilot prompt

That plan looks right. Implement it.

Only modify app.py and test_app.py. Add tests for the invalid-input case and the negative-quantity case. Do not change the behaviour of total_price for valid input.

The agent will propose tool calls. Read each one — this is the session where you build the habit, because the cost of reading carefully is currently zero.

Watch specifically for whether it asks to run the tests. An agent proposing to run pytest is doing the right thing, and approving that is safe here.

Step 5 — Read the diff yourself

Do not skip this even if the agent reports success.

/diff

Or from a second terminal, git diff. Both work; the point is that you read it.

Things worth checking in a change this small:

  • Is the validation actually correct, or does it accept "0" when it should not?
  • Did it change total_price despite being told not to?
  • Are the new tests testing behaviour, or testing that the implementation is the implementation?
  • Did it add a dependency? For this, it should not have.

Step 6 — Run the tests yourself

Practical example

Verifying the fix

Confirm the agent's change actually passes the test suite, independently of the agent's own report.

Status
Example implementation
Not executed here
This code was written against the documentation cited at the end of the lesson but was not run while writing it. Treat the commands as the ones to run, not as output that has been observed.
pytest -q

Run this yourself rather than relying on the agent’s summary. “All tests pass” in a chat response is a claim; a green run in your terminal is evidence. They usually agree — and the case where they do not is exactly the case you need to catch.

If the tests fail, that is a good outcome for a tutorial. Feed the failure back:

Copilot prompt

pytest reports this failure:

[paste the actual failure output]

Fix the cause rather than adjusting the test to match the current behaviour.

That last sentence matters. An agent asked to make a test pass has two options, and only one of them is the one you want.

Step 7 — Review

/review

The code review agent analyses your changes. On a diff this small it will not find much, which is itself instructive: /review earns its place on changes with enough surface to hide a problem, not on ten lines you just read.

Try /security-review too — it analyses staged and unstaged changes specifically for security issues. On this project it should be quiet. Knowing what quiet looks like makes a finding meaningful later.

The code review lesson uses a project with deliberately planted flaws, which is a much better test of what these commands actually catch.

Step 7b — Try the read-only agent

Before the review step, spend one exchange with a built-in agent that cannot change anything:

copilot --agent explore -i "What would break if parse_quantity started raising a custom exception instead of ValueError?"

explore is read-only by construction, so this is a question you can ask about any codebase — including one you have just cloned and do not trust — without thinking about permissions at all.

The answer is also a useful calibration check. On a three-file project you know the correct answer, so you can judge whether the agent’s reasoning about consequences is sound before relying on it somewhere you cannot check.

Understanding what the agent is actually doing

Between your prompt and the diff, several things happen that are worth understanding, because they explain most of the surprising behaviour beginners run into.

It gathers context before it acts

The agent does not receive your repository. It reads parts of it, chosen based on your prompt. On a three-file project it will read everything; on a large codebase it reads what it judges relevant, which means the quality of its answer depends on whether that judgement was right.

This is why a prompt naming a specific file usually produces better results than one describing a symptom. “Fix the validation in app.py” gives it a starting point; “the app crashes sometimes” makes it guess.

It is also why the agent occasionally seems to ignore something obvious. If it did not read the file, it does not know. /context shows how much room it has; naming files directly is the cheapest fix.

It proposes tool calls, one at a time

What you approve is not “the change”. It is an individual tool call: read this file, write that file, run this command. A single request can produce a dozen.

Two consequences follow. First, approving a tool call is narrower than approving a plan — you are permitting one action, and the next one comes separately. Second, a long sequence of individually-reasonable approvals can add up to something you would not have approved as a whole. That is precisely why reading the diff at the end is a separate step from approving along the way.

Approval is remembered for the session

Approve pytest once and it typically will not ask again in that session. This is convenient and worth knowing about, because it means your permission surface grows as a session goes on. /reset-allowed-tools clears it, which is useful when a session started as light exploration and has drifted into real changes.

A second exercise: adding a feature

The first exercise fixed a bug in code you wrote. This one is closer to real work, where the agent adds something and you have to decide whether the design is right.

Copilot prompt

Add a function apply_discount(total, percent) to app.py that applies a percentage discount to a total.

Before writing code, tell me how you plan to handle: a percent above 100, a negative percent, and a non-numeric percent. I want to agree the behaviour before you implement it.

The second paragraph is the technique worth learning. Asking the agent to state its edge-case handling before writing produces a much better outcome than reviewing an implementation and discovering it silently clamps values.

Edge cases are where generated code is weakest, not because models are bad at them but because the prompt rarely specifies them and something has to be assumed. Making the assumption explicit moves the decision back to you at the point where it is cheap.

When the implementation lands, check one specific thing: does apply_discount raise, return a sentinel, or clamp? Whichever it does, is it consistent with what parse_quantity does for its invalid input? Internal inconsistency across two functions written minutes apart is a very common failure and one that tests rarely catch, because each function passes its own tests.

When the first session goes wrong

It changed a file you told it not to. Instructions in a prompt are guidance, not enforcement. /rewind, then restart with --deny-tool='write(app.py)' or a narrower scope. This is not the agent misbehaving so much as a demonstration of why the permission flags exist.

It will not read a file one directory up. Working as designed — file access defaults to the working directory and its subdirectories. --add-dir grants one more.

It is stuck in a loop, retrying the same failing approach. Interrupt it and change the framing rather than repeating the request. Telling it what you have already ruled out is more useful than telling it to try again.

It claims a test passes and it does not. Run the tests yourself. This is why step 6 exists as a separate step rather than a footnote.

It asks to run something you do not recognise. Decline, and ask what the command does and why it is needed. An agent that cannot justify a command should not run it, and asking costs one turn.

Step 8 — Practise undoing

Before you finish, break something on purpose.

Copilot prompt

Rename parse_quantity to convert_quantity everywhere.

Let it. Then:

/rewind

That reverts the last turn and its file changes. Confirm with /diff or git diff that the rename is gone.

Now you know what /rewind does, and — more usefully — what it does not. It handles the last turn. It does not reach back through a long session, and it cannot undo anything that left your machine. If the agent had pushed a branch, rewinding the conversation would not unpush it.

One more thing worth trying: /env

Before you tear the project down, run:

/env

It lists what the session actually loaded — instructions, MCP servers, skills, agents, hooks, plugins, language servers, extensions.

On this demo project the list should be nearly empty, and that is the point. You now know what a clean baseline looks like. The first time an agent behaves strangely in a real repository, /env will show you something in that list you had forgotten about — an inherited AGENTS.md, a plugin installed weeks ago, a hook from a template. Recognising the difference requires having seen the empty case once.

/instructions is the narrower version, showing only instruction files and letting you toggle them off. Both are diagnostic commands that cost nothing and answer the question people most often ask about agent behaviour, which is “why is it doing that”.

Step 9 — Clean up

/exit
cd .. && rm -rf copilot-cli-demo

Making the habits permanent

Everything above depends on you remembering to do it. Two mechanisms make that unnecessary.

Put the rules in the repository

Rather than repeating “run the tests, do not change public APIs” every session, write it once. copilot init analyses the project with read-only tools and generates .github/copilot-instructions.md; /init does the same from inside a session.

For this demo project, instructions worth having would be short:

Before completing a code task:

- Run `pytest -q` and report the actual output.
- Do not change the signature of a public function without saying so.
- Add a test for every behaviour change, covering the failure case.
- Do not add dependencies. Explain why if you think one is needed.

Four lines, applied every session, with no memory required. The custom instructions lesson covers every file the CLI reads and how they combine.

Put the boundaries in the launch command

The prompts you write are requests. The flags you launch with are rules. For routine work on a project you care about, a saved alias encodes the second:

alias cop-safe="copilot --deny-tool='shell(git push:*)' --deny-tool='write(.env)'"

Neither denial will ever be relevant on a good day, which is the point — they cost nothing until the day they matter, and on that day they do not depend on you being alert.

Where this fits against the editor

If you have used agent mode in VS Code, this session will have felt familiar and slightly less protected. That impression is accurate.

The editor shows changes in a diff view designed for review, and the agent operates on a workspace. The CLI operates on a shell, which is why it can run your test suite, inspect a running service, or work on a machine with no editor — and why the boundary is a permission policy rather than an architecture.

Neither is better. The CLI is the right tool when the work is shell work; the editor is the right tool when the work is reading and editing code you have open. Cluster 2 covers the editor side, and running both against the same project is a normal way to work.

What to take from this

Five habits, in the order they matter:

Read-only first. Every new repository, every new task where you are unsure. The information is free and the risk is zero.

Plan before implementation for anything touching more than one file.

Read the diff. Not the summary of the diff — the diff.

Run the tests yourself. The agent’s report is a claim about the tests, not the tests.

Configure permissions instead of relying on care. Care runs out; a denied tool does not.

None of these habits are specific to Copilot CLI. They are the habits of working with any tool that can act on your behalf, and the reason to build them on a throwaway project is that building them on a real one is considerably more expensive.

A checklist for your first real project

When you move from the demo to something that matters, run through this once. It takes two minutes and prevents the mistakes that make people distrust the tool.

Commit first. A clean starting commit means git diff and git checkout . always work.

Start read-only. --deny-tool='write' for the first session on any repository you have not used the CLI in before. Learn how it reads the project before letting it change one.

Set your denials. shell(git push:*) and write(.env) cost nothing and close real categories of accident.

Check what is loaded. /env on an established repository often reveals an inherited AGENTS.md or a plugin you had forgotten. Better to know before it surprises you.

Pick a task with a definition of done. A failing test, a lint error, a type error. Objective finish lines suit this workflow; “make this nicer” does not.

Plan before implementing for anything spanning more than one file.

Read the diff, then run the tests yourself. In that order, and both every time.

Next

The commands cheat sheet is the reference to keep beside you, and the pillar explains the permission model these habits are compensating for.

From here the cluster splits by what you actually work on: Linux for administration and diagnosis, Bash for scripting, Python for a full project loop, and DevOps for infrastructure tooling. If you want the agent to stop needing the same instructions every session, custom instructions is the next step.

Sources

Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.

Primary sources