GitHub Copilot CLI Tutorial for Beginners
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.
Key takeaways
- Start every relationship with a repository read-only. An agent that cannot write cannot surprise you, and its explanations tell you whether to trust it further.
- The trust prompt is a real decision. A repository can contain instruction files and hooks that shape the session.
- Read the diff yourself.
/diffcosts one command, and the gap between what you expected and what was written is the whole lesson. - Tests are the only claim in the loop that does not come from a language model. Run them yourself rather than accepting “tests pass”.
/rewindundoes the last turn’s file changes. Learn it before you need it.
Build the sandbox project
Three files, deliberately containing one real bug.
mkdir copilot-cli-demo && cd copilot-cli-demoapp.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.0README.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
copilotOn 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.
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.
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
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.
/diffOr 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_pricedespite 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 -qRun 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:
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
/reviewThe 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.
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.
Rename parse_quantity to convert_quantity everywhere.
Let it. Then:
/rewindThat 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:
/envIt 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
/exitcd .. && rm -rf copilot-cli-demoMaking 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.
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.