AI is Good
Time: about 3 hours, in 15β20 minute chunks. Stop anywhere. You need: VS Code, GitHub Copilot signed in, and Python 3 installed. That is all. You do not need: any coding experience. Every file is already written. You rename files, change one or two words, and paste prompts.
Everything lives in this repository. Open this folder in VS Code, not a folder above it.
Get it:
git clone https://github.com/hereandnowai/github-copilot-hooks-for-beginners.git
cd github-copilot-hooks-for-beginners
code .
Then run the self-test in the VS Code terminal to confirm the machine is ready:
python3 .github/hooks/scripts/selftest.py
Learners already know that Copilot follows instructions, prompts and skills. All of those ask Copilot to behave. A hook makes it behave. A hook is a tiny program that VS Code runs at a fixed moment, and that program can whisper to Copilot, watch it, refuse it, or send it back to redo work.
The lesson never says that in the abstract. It shows nine hooks, each with one job and one silly name, and the learner discovers the idea by playing.
| # | Hook | Moment it fires | What the learner sees |
|---|---|---|---|
| 1 | The Greeter | first message of a new chat | Copilot knows their name without being told |
| 2 | The Diary | every message | a file that records everything they type, and Copilot learns the time |
| 3 | The Magic Word | every message | their message is refused unless it says "please" |
| 4 | The Bouncer | before Copilot uses a tool | Copilot is stopped from deleting a file |
| 5 | The Pineapple Police | before Copilot uses a tool | Copilot is refused, then rewrites without the banned word |
| 6 | The Stamp | after Copilot uses a tool, and when it tries to finish | Copilot is sent back to add a line to a file it just wrote, and cannot finish until it does |
| 7 | The Scoreboard | after every tool, and when Copilot finishes | a live score, a report card, and a sound |
| 8 | The Linter | after Copilot writes a file | ruff repairs Copilot's Python before Copilot has finished speaking, and sends back what it cannot fix |
| 9 | The Committer | when Copilot finishes | the files that passed the linter are committed to git, by name, without a human |
Then a detective game and four build-your-own challenges.
A hook is ON when its file name ends in .json. It is OFF when it ends in .json.off.
Every hook ships switched off. Each activity starts with "rename the file to switch it on". Learners can always get back to a calm state by renaming everything to .off. Say this rule out loud in the first minute and write it on the board.
- Open this folder in VS Code and run the self-test in the terminal:
Every line must say
python3 .github/hooks/scripts/selftest.pyPASS. It pretends to be VS Code and feeds each script a fake message. No Copilot needed. Last run: 57 passed, 0 failed, on 2026-09-20. - Then rehearse every hook live once in Copilot Chat, in order, on the machine you will teach from. The scripts are verified. The live wiring to VS Code is something only a rehearsal proves, and VS Code updates itself.
- Check the Output panel works: View β Output, then pick GitHub Copilot Chat Hooks in the dropdown on the right. This is where learners look when "nothing happened".
- On lab machines, confirm
python3 --versionworks in the VS Code terminal. On Windows the command ispython, and the hook files already handle that. - Hook 8 needs
ruff. Install it withuv tool install ruff(orpip install ruff) and checkruff --version. Without it the self-test printsSKIPfor Hook 8 and the hook says so in the chat instead of failing silently. - Hook 9 needs this folder to be its own git repository. Run
git rev-parse --show-toplevel: it must print the lesson folder itself. If it prints a folder above it, you are inside a bigger repo and the Committer will refuse to run, on purpose. A freshgit cloneof this repo is already correct.
These were read out of GitHub Copilot Chat 0.48.1's own code on 2026-09-07.
- The tool names Copilot sends to a hook are
run_in_terminal,create_file,replace_string_in_file,multi_replace_string_in_file,insert_edit_into_file,apply_patch,read_file,list_dir,grep_search,file_search. The names in the public docs example (runTerminalCommand,editFiles) do not appear anywhere in the extension. The scripts match on"terminal" in tool_nameand"file" in tool_nameso they work with either. - The Greeter fires on the first message of a new chat only. If a learner keeps using an old chat, nothing happens. The fix is always the + (New Chat) button.
- A hook's
hookSpecificOutputmust carry the matchinghookEventName, or VS Code silently strips it. Every script includes it. If a learner's copy stops working, that is the first thing to check. - A
systemMessagein the hook's answer shows up as a small note in the chat. The report card uses it. - Copilot can ignore
PostToolUsefeedback. Seen live on 2026-09-07 with GPT-5.6: the Stamp hook fired, returned the instruction correctly (confirmed in the Output panel), and the model finished its turn without acting on it, twice in a row. So the Stamp also runs atStopand answersdecision: blockwhile a file is still unstamped. A blocked Stop is enforced: Copilot must keep working. It sends Copilot back once only, so it can never loop forever. Re-confirmed on 2026-09-20 by readingexecuteStopHookin the bundled extension: it looks forhookSpecificOutput.decision === "block"with areason, which is exactly what the script sends. - Copilot spells file extensions however it likes. Seen live on 2026-09-20: asked for "a ReadMe.mD file", Copilot created
chatbot/ReadMe.mDβ and.mDis not.md. The Stamp compared the extension exactly, so it stayed silent through both thecreate_fileand theapply_patchthat filled the file in, and the locked door atStopnever had anything to lock. It now lower-cases the extension before comparing, and the self-test covers.mD,.MDand.TXT. If a hook ever goes quiet, read the file name before you blame the hook. - A terminal rename is invisible to the Stamp.
mv ReadMe.mD README.mdgoes throughrun_in_terminal, whose input carries no file path a hook can read, so nothing re-checks the file under its new name. The next edit to it does. Worth saying out loud if a learner renames from the terminal.
- "Nothing happened." Nine times out of ten: old chat instead of new chat, or the file still ends in
.off, or they opened the wrong folder. Check those three before the Output panel. - The Bouncer gets bypassed. Copilot may decide not to use the terminal and instead say it cannot delete files. That is fine. The prompt says "using the terminal" to push it there. If it finds another way, celebrate: "the bouncer only watches the door you told it to watch". That is the best line of the day.
- Python not found. VS Code's terminal must be able to run
python3(Mac/Linux) orpython(Windows). If neither works, hooks fail silently and the Output panel shows the error. - The Magic Word annoys people. That is the point. Switch it off at the end of Activity 3 and do not leave it on.
- Copilot writes a hook wrong. In the challenges, learners ask Copilot to write hooks. Copilot invents JSON formats. The challenge prompts say "copy this existing file and change it", which keeps it on the rails.
- The word "hooks". If anyone has met React, say once: nothing to do with
useState. These are Copilot hooks.
| Section | Minutes |
|---|---|
| Setup and the one rule | 10 |
| Hook 1: The Greeter | 15 |
| Hook 2: The Diary | 15 |
| Hook 3: The Magic Word | 10 |
| Hook 4: The Bouncer | 20 |
| Hook 5: The Pineapple Police | 15 |
| Hook 6: The Stamp | 15 |
| Hook 7: The Scoreboard, Report Card and Ding | 20 |
| Hook 8: The Linter | 20 |
| Hook 9: The Committer | 15 |
| Game: Hook Detective | 20 |
| Build your own (pick one or two) | 30 |
| Wrap | 5 |
- Open the folder called
github-copilot-hooks-for-beginnersin VS Code. File β Open Folder, pick that folder. Not the folder above it. - Open Copilot Chat with Ctrl+Alt+I (Mac: Ctrl+Cmd+I).
- At the bottom of the chat box, set the mode to Agent.
Check it says Agent. Not Ask. Not Edit. In Ask mode Copilot only talks, and most hooks watch Copilot doing things.
- Open the Explorer on the left and find the folder
.githubβhooks. You will see files ending in.json.off. Those are your hooks. They are all switched off.
A hook is a small file that VS Code runs at a fixed moment in a chat.
A hook is ON when its name ends in .json. It is OFF when its name ends in .json.off.
To switch a hook on: right-click the file β Rename β delete the .off at the end β Enter.
To switch it off: rename it and put .off back.
That is the only "technical skill" in this lesson. If anything ever gets confusing, rename everything back to .off and you are back to normal.
A chat with Copilot has five moments where a hook can jump in.
You press Enter βββΊ Copilot thinks βββΊ Copilot is about to use a tool βββΊ the tool finishes βββΊ Copilot is done
β β β β β
THE DIARY THE GREETER THE BOUNCER THE STAMP THE REPORT CARD
THE MAGIC WORD (first message only) THE PINEAPPLE POLICE THE SCOREBOARD THE DING
A "tool" is anything Copilot does besides talking: read a file, write a file, run a command.
You do not need to memorise this picture. You will have built one hook for every moment by the end.
Moment: the first message of a new chat. Trick: the hook whispers a note to Copilot before Copilot reads your message.
- In the Explorer, open
.github/hooks/scripts/greeter.py. - Find these two lines near the top:
MY_NAME = "Friend" FUN_TOPIC = "octopuses"
- Change
Friendto your own name. Keep the quotes. Save with Ctrl+S. - Switch the hook on: rename
01-greeter.json.offto01-greeter.json. - Click the + at the top of the chat panel to start a new chat. This matters. The Greeter only runs on a brand-new chat.
- Paste this:
Hi! What can you help me with today?
Copilot greets you by name, and its answer ends with a line starting with Fun fact: about octopuses.
You never typed your name into the chat. The hook told Copilot.
- View β Output. In the dropdown on the right, choose GitHub Copilot Chat Hooks.
- You will see a line for
SessionStartand the hook that ran.
This panel is where you look whenever a hook seems to do nothing.
- Change
FUN_TOPICto"cricket"or"your home town". Save. Start a new chat. Ask anything. - Ask Copilot:
How do you know my name?It will usually say it was given the information as context. Correct. That is the whisper.
- Did you start a new chat with the + button? Old chats do not re-run the Greeter.
- Does the file name end in exactly
.jsonnow? - Is the mode Agent?
Moment: every time you press Enter. Trick: the hook writes down everything you say, and tells Copilot what time it is.
- Switch it on: rename
02-diary.json.offto02-diary.json. - In the same chat, paste these three messages one at a time:
Tell me a one-line joke about computers.
What is the exact time right now?
Write the word "hello" in five languages.
- Look at the Explorer. A new file has appeared at the top level:
copilot-diary.md. Open it.
The diary has three lines, one per message, each with the date and time you sent it.
And the answer to "What is the exact time right now?" was correct. Copilot has no clock. It cannot normally know the time. The hook handed it the time along with your message.
- Switch the Diary off: rename it back to
02-diary.json.off. - Start a new chat (the + button) and ask again:
What is the exact time right now?
Copilot now says it cannot know, or guesses. Same question, no hook, no answer. Switch the Diary back on before moving on.
- Open
.github/hooks/scripts/diary.pyand find the line that starts withf.write(. Change the-between the time and your message toπ. Save. Send a message. Look at the diary.
Moment: every time you press Enter. Trick: the hook can refuse you. Not only Copilot.
- Switch it on: rename
03-magic-word.json.offto03-magic-word.json. - Paste this:
Write me a two-line poem about tea.
Nothing gets written. The chat shows a message from the hook: The magic word is missing. Try again with 'please' somewhere in your message. Copilot never saw your request.
- Now paste this:
Please write me a two-line poem about tea.
The poem arrives.
- Open
.github/hooks/scripts/magic_word.py. Change"please"to"abracadabra". Save. - Send
Please tell me a joke.It is blocked. SendAbracadabra, tell me a joke.It works.
Rename 03-magic-word.json back to 03-magic-word.json.off. Leaving it on will make the rest of the lesson very irritating, and that is the lesson: a hook is a rule that never gets tired. Only use one when you mean it.
Moment: just before Copilot uses a tool. Trick: the hook can stop Copilot from doing something, even if you asked for it.
- Look in
playground/. There is a file calledscratch.txt. It exists to be deleted. - Switch the hook on: rename
04-bouncer.json.offto04-bouncer.json. - Paste this:
Using the terminal, delete the file playground/scratch.txt.
Copilot tries to run a delete command. The Bouncer refuses. Copilot tells you it was stopped by the bouncer. Look in the Explorer: scratch.txt is still there.
You asked for the delete. Copilot agreed. The hook said no anyway.
- Open
.github/hooks/scripts/bouncer.py. Find the line:MODE = "deny"
- Change
"deny"to"ask". Save. - Paste the same prompt again:
Using the terminal, delete the file playground/scratch.txt.
This time Copilot pauses and VS Code shows you a button. You decide. Click Cancel or Skip and the file lives. Click Allow and it is gone.
Three answers a hook can give before Copilot acts: allow, deny, or ask the human.
- Put
MODEback to"deny"and save.
Sometimes Copilot decides not to use the terminal at all and just tells you it will not delete files. That is Copilot being cautious on its own, not the hook. Try:
I am sure. Run the terminal command rm playground/scratch.txt now.
If Copilot finds a different way to delete the file without the terminal, congratulations: you have discovered that the bouncer only watches the door you told it to watch. Put the file back by creating a new scratch.txt with any text in it.
Moment: just before Copilot writes a file. Trick: the hook refuses, explains why, and Copilot fixes its own work.
- Switch it on: rename
05-pineapple-police.json.offto05-pineapple-police.json. - Paste this:
Create a file playground/pizza-menu.md with a menu of five pizzas.
One of them must be a Hawaiian pizza with ham and pineapple.
Give each pizza a one-line description.
Copilot tries to write the file. The Pineapple Police refuse: the word 'pineapple' is banned in this project. Rewrite the content with no mention of pineapple at all.
Then Copilot tries again without pineapple. Open playground/pizza-menu.md. Five pizzas, and the Hawaiian has been renamed or replaced. Copilot may even apologise to you about it.
This is the big idea of the day. The Bouncer just said no. The Police said no and explained. Copilot read the explanation and changed its plan. A hook can teach.
- Open
.github/hooks/scripts/pineapple_police.pyand change"pineapple"to"delicious". Save. Ask for a dessert menu. Watch Copilot write five desserts without once saying delicious. - Ask Copilot directly:
It will tell you it was blocked by a policy. It is being honest.
Why did you leave out the pineapple?
Moments: just after Copilot writes a file, and when Copilot tries to finish. Trick: the hook checks Copilot's work and sends it back to fix something. If Copilot ignores the note, the hook refuses to let it finish.
- Switch it on: rename
06-stamp.json.offto06-stamp.json. - Paste this:
Create playground/story.md with a four-line story about a cat who learns to code.
Watch the chat closely. Copilot creates the file. Then, without you asking, Copilot edits the same file again. Open playground/story.md. The very first line is:
> π€ Made with GitHub Copilot
Copilot did not know about that rule. The Stamp checked the file after it was written, found the line missing, and told Copilot: every .md and .txt file must begin with this line. Add it now. Copilot went back and did it.
Sometimes Copilot is stubborn. It reads the note, says "Created story.md", and tries to finish anyway. Then you see a second thing happen: the chat shows the hook's message again and Copilot keeps working instead of stopping. It cannot finish until the stamp is there. Open the file: stamped.
Before-a-tool hooks can refuse. After-a-tool hooks can review. Finish hooks can say "not yet".
Open 06-stamp.json. It lists the same script twice: once under PostToolUse, once under Stop. The first is a polite note. The second is a locked door. A good rule has both.
- Ask for another file:
Create playground/shopping-list.txt with five items.Stamped again. - Ask for a Python file:
Create playground/hello.py that prints hello.Not stamped. Openstamp.pyand findFILE_TYPESto see why. The hook only stamps.mdand.txt. - Change the
STAMPtext to your own catchphrase. Save. Ask for a new file.
Moments: after every tool, and when Copilot finishes. Trick: hooks can keep score, write reports, and make noise.
This one file switches on three hooks at once. Open 07-scoreboard.json.off and look: one entry under PostToolUse and two under Stop. That is allowed. One file, several hooks.
- Switch it on: rename
07-scoreboard.json.offto07-scoreboard.json. - Turn your sound on.
- Paste this:
Read every file inside the playground folder, then create playground/summary.md
with one line describing each file.
- While Copilot works, watch the Explorer. A file
scoreboard.jsonappears and keeps changing. Open it and watch the numbers go up.
scoreboard.jsoncounts each thing Copilot does: files read, files written, commands run.- When Copilot finishes,
scoreboard.jsondisappears andreport-card.mdappears with a table and a grade. - A small note appears in the chat: π Report card written: N tool uses, grade ...
- Your computer makes a sound. On a Mac it says "Copilot is done" out loud.
Everyone gets three prompts. Highest total tool uses on a single report card wins. Ideas:
Read every file in the playground folder one by one and tell me the longest word in each.
Create ten files in playground called note-1.md to note-10.md, each with a different animal fact.
Then compare report cards. Then argue about whether "Overachiever" is a compliment.
Open .github/hooks/scripts/ding.py and change "Copilot is done" to anything. On a Mac it will say it. Try "Master, your code is ready".
Moment: after Copilot writes a file. Trick: a hook can run a real tool on Copilot's work, repair what it can, and hand the rest back.
Hooks 1 to 7 watched, whispered and refused. This one repairs. Every time Copilot writes a .py file, ruff runs on it β the same linter real Python projects use. What ruff can fix, it fixes. What it cannot fix is handed straight back to Copilot.
ruff must be installed. In the VS Code terminal:
uv tool install ruff
No uv? Then pip install ruff. Check with ruff --version. If ruff is missing, the hook says so in the chat rather than failing quietly.
- Switch it on: rename
08-linter.json.offto08-linter.json. - Paste this:
Please create playground/greet.py with a function that says hello to a name.
Import os, sys and json at the top even though you do not use them, and put a
variable called unused inside the function.
- Copilot writes the file.
- A note appears in the chat: π§Ή ruff: playground/greet.py still has 1 problem(s)
- Open the file. The unused imports are already gone. ruff deleted them before Copilot finished speaking.
- Copilot is told about the one ruff could not fix β
F841 Local variable 'unused' is assigned to but never usedβ and fixes it itself. - The next note reads π§Ή ruff: playground/greet.py is clean.
"Nobody told Copilot to run a linter. It did not decide to. The rule belongs to the project, not to the robot β and the next person who opens this project gets the same rule for free, without reading a single line of documentation."
Open .github/hooks/scripts/linter.py. Two things worth pointing at:
- It hunts for
ruffin three places before giving up, because VS Code does not always hand a hook your terminal'sPATH. - Clean files are written to
.lint-clean.json. That file is a note to the next hook. Hook 9 reads it.
ruff --versionin the terminal. If that fails, the hook is right and you are missing ruff.- The hook only lints
.pyfiles. A.mdfile gets nothing for ruff to lint.
Change --fix to --fix --unsafe-fixes in linter.py and watch ruff repair more. Or replace ruff entirely: a spell-checker, black, or your test suite. The hook does not care what the command is.
Moment: when Copilot finishes. Trick: two hooks talking to each other. One decides the work is good; the other saves it forever.
Hook 8 leaves behind a list of files that passed the linter. Hook 9 reads that list when Copilot finishes and commits exactly those files. Not git add . β those files, by name.
This hook only runs when the lesson folder is its own git repository. If your copy lives inside a bigger repo, the hook switches itself off and says so, rather than writing to a history that is not yours. Check:
git rev-parse --show-toplevel
If that prints the lesson folder, you are ready. If it prints a folder above it, the Committer will refuse β that is the hook working, not failing. To give the lesson its own history:
git init && git add -A && git commit -m "start"
- Leave
08-linter.jsonswitched on. Rename09-committer.json.offto09-committer.json. - Paste this:
Please create playground/add.py with a function that adds two numbers, and
playground/subtract.py with a function that subtracts two numbers.
- When Copilot has finished, run
git log --onelinein the terminal.
- Two notes from the Linter: both files are clean.
- One note from the Committer: π¦ Committed 2 file(s) as a1b2c3d: playground/add.py, playground/subtract.py
git logshows a commit no human made.git show --stat HEADshows exactly those two files, and nothing else.
"One commit per answer, not one per file. And it never pushes. A hook that writes to your history is powerful enough. A hook that writes to everybody else's is a different conversation, and one you should have on purpose."
| It will not | Because |
|---|---|
| Commit a file that failed the linter | that is the whole point of the chain |
| Commit while Hook 6 (the Stamp) is still waiting | a half-finished file must not land in history |
| Commit anything if this folder is not its own repo | your other work is not the lesson's to touch |
| Push | leaving the machine is your decision, not a hook's |
Show the room the third one deliberately: open the pack from inside a bigger repo and watch it say π¦ The Committer is switched off. A refusal you can predict is worth more than a feature you cannot.
The playground is disposable, so .gitignore throws its contents away β except playground/*.py, which is let back in on purpose, so this lesson has something to commit. If the Committer ever says π¦ Nothing committed: .gitignore throws away β¦, that is the same rule talking. The hook is telling you the truth, not breaking.
Open committer.py and change the commit message. Or make it refuse after 6pm β you already wrote that rule once, in Challenge A.
20 minutes. Works best in pairs.
There is one hook you have not seen: 99-mystery.json.off. Inside it are three secret house rules and a pirate. It never explains a rule. It answers with a riddle, and it keeps score.
- Do not open
.github/hooks/scripts/mystery.py. That is cheating. (It is allowed at the end.) - Switch off hooks 1 to 9 by renaming them to
.off, so the mystery is the only voice in the room. - Switch on
99-mystery.json.offβ99-mystery.json. - Start a new chat. Copilot greets you as a pirate and tells you the score: π΄ββ οΈ Three house rules haunt this repo. 0 of 3 uncovered.
- Experiment until the counter reads 3 of 3. Write each rule down as you find it.
Hello! Introduce yourself.
Create a file playground/MyJokes.md with three jokes.
Create a file playground/map.md describing where the treasure is buried.
Run ls
Run ls -la
Every refusal is a riddle, never an explanation. The counter goes up the first time each rule is found. At 3 of 3 the curse lifts and the hook reads out all three rules itself.
- The voice: at the start of every new chat, Copilot is told to talk like a pirate and never explain why.
- Rule 1 β SHOUTING: a new file whose name contains a capital letter is refused.
- Rule 2 β the cursed word: anything mentioning treasure is refused, wherever the word appears.
- Rule 3 β the one-word spell: a terminal command of a single word is refused.
lsis refused.ls -lais allowed.
Ask the room which is a whisper and which is a bouncer. (The voice whispers. All three rules bounce.) Then ask the better question: how did the hook remember the score between messages?
It wrote a file β .mystery-progress.json. A hook has no memory of its own. That is why the Scoreboard, the Stamp, the Linter and the Mystery all keep a small file on disk. Once a learner sees that, every hook in the pack makes sense at once.
Delete .mystery-progress.json and the score returns to 0 of 3. Then set the pairs a real challenge: add a fourth rule. They have the file open now, and they have written four hooks already.
Switch it off and switch the others back on if you want them.
Pick one. Thirty minutes. You are not writing from scratch. You are copying a hook you already understand and changing a few lines. And you have a helper who writes Python: Copilot.
Yes. You are about to ask Copilot to write a rule that controls Copilot.
- Copy a script in
.github/hooks/scripts/and give it a new name. - Copy the matching
.jsonfile, give it a new name, and change the script name inside it. - Make sure the new
.jsonname does not end in.off. - Test it with a prompt. Look at the Output panel if it is quiet.
Rule: Copilot may not run any terminal command after 6 pm.
Start from: bouncer.py and 04-bouncer.json.
Paste this in Agent mode:
Copy .github/hooks/scripts/bouncer.py to .github/hooks/scripts/curfew.py.
Change it so that instead of looking for delete words, it checks the current time.
If the hour is 18 or later, deny every terminal command with the reason
"CURFEW: no terminal commands after 6 pm. Try again tomorrow."
Keep the same JSON answer shape as bouncer.py.
Then copy .github/hooks/04-bouncer.json to .github/hooks/08-curfew.json
and make it run curfew.py instead. Do not change any other file.
To test before 6 pm, temporarily change 18 to a number lower than the current hour.
Rule: the diary also records how many words each message had.
Start from: diary.py and 02-diary.json.
Copy .github/hooks/scripts/diary.py to .github/hooks/scripts/word_counter.py.
Change it so each diary line also shows the number of words in the message,
like "(7 words)". Write to a file called word-diary.md instead of copilot-diary.md.
Then copy .github/hooks/02-diary.json to .github/hooks/09-word-counter.json
and make it run word_counter.py. Do not change any other file.
Rule: every time Copilot writes a .md file, a copy is saved in playground/backups/.
Start from: stamp.py and 06-stamp.json.
Copy .github/hooks/scripts/stamp.py to .github/hooks/scripts/backup.py.
Keep the find_paths function. Instead of checking for a stamp, copy every .md file
it finds into playground/backups/ (create the folder if needed), adding the current
time to the copied file name. Always answer with {"continue": true}.
Then copy .github/hooks/06-stamp.json to .github/hooks/10-backup.json
and make it run backup.py. Do not change any other file.
Rule: Copilot ends every answer with one tip on writing better prompts.
Start from: greeter.py and 01-greeter.json.
This one you can do without Copilot's help. Copy greeter.py to coach.py, replace the note text with your own instruction, copy 01-greeter.json to 11-coach.json, point it at coach.py. New chat. Done.
- The
.jsoncopy still points at the old script name. Most common mistake. - The new file ends in
.offbecause they copied a.offfile. Second most common. - Copilot invents a different JSON answer format. Tell them: "Ask Copilot to make it answer exactly like the original file." Then run the self-test idea: paste a fake message into the terminal. Trainers can show the pattern from
selftest.py. - Nobody needs to understand every line of Python. They need to find the line that says what to change.
ON / OFF: ends in .json is on. Ends in .json.off is off.
Where things live
| What | Where |
|---|---|
| Hook switches | .github/hooks/*.json |
| Hook programs | .github/hooks/scripts/*.py |
| Things Copilot makes | playground/ |
| The diary, scoreboard, report card | top level of the folder |
| Notes hooks leave for each other | .stamp-pending.json, .lint-clean.json, .mystery-progress.json, inside .github/hooks/scripts/ |
| Proof a hook ran | View β Output β GitHub Copilot Chat Hooks |
The five moments and their names in the .json file
| Moment | Name in the file | Hook from today |
|---|---|---|
| First message of a new chat | SessionStart |
Greeter, Mystery |
| Every message | UserPromptSubmit |
Diary, Magic Word |
| Before Copilot uses a tool | PreToolUse |
Bouncer, Pineapple Police |
| After Copilot uses a tool | PostToolUse |
Stamp, Scoreboard, Linter |
| Copilot finishes | Stop |
Stamp (locked door), Report Card, Ding, Committer |
The four things a hook can say back
| The hook wants to | It answers with |
|---|---|
| Let it through, say nothing | {"continue": true} |
| Whisper something to Copilot | hookSpecificOutput with additionalContext |
| Stop Copilot before a tool | hookSpecificOutput with permissionDecision = deny or ask |
| Block the human's message | decision = block with a reason |
| Refuse to let Copilot finish | hookSpecificOutput with decision = block and a reason (at Stop) |
One sentence to remember: instructions and prompts ask Copilot. Hooks make it.
Nothing happens at all.
- Is the file name ending in
.json, not.json.off? - Did you start a new chat? (Greeter and Mystery only run on a new chat.)
- Is the mode Agent?
- Did you open the
github-copilot-hooks-for-beginnersfolder itself, not a folder above it? - Open View β Output β GitHub Copilot Chat Hooks. Read the last few lines. A red line usually names the problem.
The Output panel says it cannot find python3 or python.
Open a VS Code terminal and type python3 --version. On Windows type python --version. If neither prints a version, Python is not installed on this machine. Ask the trainer.
A hook seems broken after you edited it. In the terminal, run:
python3 .github/hooks/scripts/selftest.py
It tests every hook that ships with the lesson and says PASS or FAIL. If your edit broke something, the FAIL line says which. If you cannot fix it, copy the original file from the trainer.
Everything is chaos.
Rename every file in .github/hooks/ so it ends in .off. Start a new chat. Calm. Now switch on one hook at a time.
Copilot keeps going round in circles. The Stamp and the Pineapple Police send Copilot back to try again. If Copilot loops more than three times, switch that hook off. Then ask Copilot in plain words what it is stuck on.
Ask the room to finish these sentences:
- "The Greeter was a ..." (whisper)
- "The Bouncer was a ..." (door)
- "The Pineapple Police was a ..." (door that explains)
- "The Stamp was a ..." (reviewer)
- "The Report Card was a ..." (receipt)
Then the line to close on: you did not learn to code today. You learned that the robot has a manager, and the manager is a folder called .github/hooks.
HERE AND NOW AI builds and teaches practical AI. People learn by building, not by being shown, and this lesson pack is written that way.
| Website | https://hereandnowai.com |
| info@hereandnowai.com | |
| Mobile | +91 996 296 1000 |
| GitHub | https://github.com/hereandnowai |
| https://www.linkedin.com/company/hereandnowai/ | |
| YouTube | https://youtube.com/@hereandnow_ai |
| https://instagram.com/hereandnow_ai | |
| X | https://x.com/hereandnow_ai |
| Blog | https://hereandnowai.com/blog |
Licensed under the MIT License. See LICENSE.
