Skip to content

About

Hands-on, zero-code lesson pack for teaching GitHub Copilot hooks in VS Code: 7 playful hooks, a detective game and build-your-own challenges.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

HERE AND NOW AI

AI is Good


GitHub Copilot Hooks for Absolute Beginners

Teach your robot some house rules

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

Instructor notes (do not hand this page out)

What this lesson teaches

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.

The one rule that runs the whole lesson

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.

Before class

  1. Open this folder in VS Code and run the self-test in the terminal:
    python3 .github/hooks/scripts/selftest.py
    
    Every line must say PASS. 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.
  2. 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.
  3. 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".
  4. On lab machines, confirm python3 --version works in the VS Code terminal. On Windows the command is python, and the hook files already handle that.
  5. Hook 8 needs ruff. Install it with uv tool install ruff (or pip install ruff) and check ruff --version. Without it the self-test prints SKIP for Hook 8 and the hook says so in the chat instead of failing silently.
  6. 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 fresh git clone of this repo is already correct.

Facts checked against the running extension, not the docs

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_name and "file" in tool_name so 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 hookSpecificOutput must carry the matching hookEventName, 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 systemMessage in the hook's answer shows up as a small note in the chat. The report card uses it.
  • Copilot can ignore PostToolUse feedback. 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 at Stop and answers decision: block while 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 reading executeStopHook in the bundled extension: it looks for hookSpecificOutput.decision === "block" with a reason, 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 .mD is not .md. The Stamp compared the extension exactly, so it stayed silent through both the create_file and the apply_patch that filled the file in, and the locked door at Stop never had anything to lock. It now lower-cases the extension before comparing, and the self-test covers .mD, .MD and .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.md goes through run_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.

Failure modes to expect

  • "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) or python (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.

Timing

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

Before you start

  1. Open the folder called github-copilot-hooks-for-beginners in VS Code. File β†’ Open Folder, pick that folder. Not the folder above it.
  2. Open Copilot Chat with Ctrl+Alt+I (Mac: Ctrl+Cmd+I).
  3. 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.

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

The one rule

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.

The five moments

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.


Hook 1: The Greeter

Moment: the first message of a new chat. Trick: the hook whispers a note to Copilot before Copilot reads your message.

Do this

  1. In the Explorer, open .github/hooks/scripts/greeter.py.
  2. Find these two lines near the top:
    MY_NAME = "Friend"
    FUN_TOPIC = "octopuses"
  3. Change Friend to your own name. Keep the quotes. Save with Ctrl+S.
  4. Switch the hook on: rename 01-greeter.json.off to 01-greeter.json.
  5. 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.
  6. Paste this:
Hi! What can you help me with today?

What should happen

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.

Look behind the curtain

  1. View β†’ Output. In the dropdown on the right, choose GitHub Copilot Chat Hooks.
  2. You will see a line for SessionStart and the hook that ran.

This panel is where you look whenever a hook seems to do nothing.

Play

  • Change FUN_TOPIC to "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.

If nothing happened

  • Did you start a new chat with the + button? Old chats do not re-run the Greeter.
  • Does the file name end in exactly .json now?
  • Is the mode Agent?

Hook 2: The Diary

Moment: every time you press Enter. Trick: the hook writes down everything you say, and tells Copilot what time it is.

Do this

  1. Switch it on: rename 02-diary.json.off to 02-diary.json.
  2. 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.
  1. Look at the Explorer. A new file has appeared at the top level: copilot-diary.md. Open it.

What should happen

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.

Prove it

  1. Switch the Diary off: rename it back to 02-diary.json.off.
  2. 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.

Play

  • Open .github/hooks/scripts/diary.py and find the line that starts with f.write(. Change the - between the time and your message to πŸ‘‰. Save. Send a message. Look at the diary.

Hook 3: The Magic Word

Moment: every time you press Enter. Trick: the hook can refuse you. Not only Copilot.

Do this

  1. Switch it on: rename 03-magic-word.json.off to 03-magic-word.json.
  2. Paste this:
Write me a two-line poem about tea.

What should happen

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.

  1. Now paste this:
Please write me a two-line poem about tea.

The poem arrives.

Play

  1. Open .github/hooks/scripts/magic_word.py. Change "please" to "abracadabra". Save.
  2. Send Please tell me a joke. It is blocked. Send Abracadabra, tell me a joke. It works.

Switch it off now

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.


Hook 4: The Bouncer

Moment: just before Copilot uses a tool. Trick: the hook can stop Copilot from doing something, even if you asked for it.

Do this

  1. Look in playground/. There is a file called scratch.txt. It exists to be deleted.
  2. Switch the hook on: rename 04-bouncer.json.off to 04-bouncer.json.
  3. Paste this:
Using the terminal, delete the file playground/scratch.txt.

What should happen

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.

Now let the human decide

  1. Open .github/hooks/scripts/bouncer.py. Find the line:
    MODE = "deny"
  2. Change "deny" to "ask". Save.
  3. 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.

  1. Put MODE back to "deny" and save.

If Copilot did not try to delete

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.


Hook 5: The Pineapple Police

Moment: just before Copilot writes a file. Trick: the hook refuses, explains why, and Copilot fixes its own work.

Do this

  1. Switch it on: rename 05-pineapple-police.json.off to 05-pineapple-police.json.
  2. 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.

What should happen

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.

Play

  • Open .github/hooks/scripts/pineapple_police.py and change "pineapple" to "delicious". Save. Ask for a dessert menu. Watch Copilot write five desserts without once saying delicious.
  • Ask Copilot directly:
    Why did you leave out the pineapple?
    
    It will tell you it was blocked by a policy. It is being honest.

Hook 6: The Stamp

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.

Do this

  1. Switch it on: rename 06-stamp.json.off to 06-stamp.json.
  2. Paste this:
Create playground/story.md with a four-line story about a cat who learns to code.

What should happen

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

Say out loud

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.

Play

  • 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. Open stamp.py and find FILE_TYPES to see why. The hook only stamps .md and .txt.
  • Change the STAMP text to your own catchphrase. Save. Ask for a new file.

Hook 7: The Scoreboard, the Report Card and the Ding

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.

Do this

  1. Switch it on: rename 07-scoreboard.json.off to 07-scoreboard.json.
  2. Turn your sound on.
  3. Paste this:
Read every file inside the playground folder, then create playground/summary.md
with one line describing each file.
  1. While Copilot works, watch the Explorer. A file scoreboard.json appears and keeps changing. Open it and watch the numbers go up.

What should happen, in order

  1. scoreboard.json counts each thing Copilot does: files read, files written, commands run.
  2. When Copilot finishes, scoreboard.json disappears and report-card.md appears with a table and a grade.
  3. A small note appears in the chat: πŸ“‹ Report card written: N tool uses, grade ...
  4. Your computer makes a sound. On a Mac it says "Copilot is done" out loud.

Play: the high score game

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.

Play: your own ding

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


Hook 8: The Linter

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.

Before this one works

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.

Do this

  1. Switch it on: rename 08-linter.json.off to 08-linter.json.
  2. 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.

What should happen

  1. Copilot writes the file.
  2. A note appears in the chat: 🧹 ruff: playground/greet.py still has 1 problem(s)
  3. Open the file. The unused imports are already gone. ruff deleted them before Copilot finished speaking.
  4. Copilot is told about the one ruff could not fix β€” F841 Local variable 'unused' is assigned to but never used β€” and fixes it itself.
  5. The next note reads 🧹 ruff: playground/greet.py is clean.

Say out loud

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

Look behind the curtain

Open .github/hooks/scripts/linter.py. Two things worth pointing at:

  • It hunts for ruff in three places before giving up, because VS Code does not always hand a hook your terminal's PATH.
  • Clean files are written to .lint-clean.json. That file is a note to the next hook. Hook 9 reads it.

If the note never appears

  • ruff --version in the terminal. If that fails, the hook is right and you are missing ruff.
  • The hook only lints .py files. A .md file gets nothing for ruff to lint.

Play

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.


Hook 9: The Committer

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.

Before this one works

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"

Do this

  1. Leave 08-linter.json switched on. Rename 09-committer.json.off to 09-committer.json.
  2. 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.
  1. When Copilot has finished, run git log --oneline in the terminal.

What should happen, in order

  1. Two notes from the Linter: both files are clean.
  2. One note from the Committer: πŸ“¦ Committed 2 file(s) as a1b2c3d: playground/add.py, playground/subtract.py
  3. git log shows a commit no human made.
  4. git show --stat HEAD shows exactly those two files, and nothing else.

Say out loud

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

What it refuses to do

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.

One line in .gitignore makes this work

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.

Play

Open committer.py and change the commit message. Or make it refuse after 6pm β€” you already wrote that rule once, in Challenge A.


Game: Hook Detective

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.

Rules of the game

  1. Do not open .github/hooks/scripts/mystery.py. That is cheating. (It is allowed at the end.)
  2. Switch off hooks 1 to 9 by renaming them to .off, so the mystery is the only voice in the room.
  3. Switch on 99-mystery.json.off β†’ 99-mystery.json.
  4. 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.
  5. Experiment until the counter reads 3 of 3. Write each rule down as you find it.

Detective prompts to try

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 reveal

  • 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. ls is refused. ls -la is 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.

Play it again

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.


Build your own

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.

How to make a new hook, every time

  1. Copy a script in .github/hooks/scripts/ and give it a new name.
  2. Copy the matching .json file, give it a new name, and change the script name inside it.
  3. Make sure the new .json name does not end in .off.
  4. Test it with a prompt. Look at the Output panel if it is quiet.

Challenge A: The Curfew

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.

Challenge B: The Word Counter

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.

Challenge C: The Backup

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.

Challenge D: The Coach

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.

What to watch for while circulating (trainer)

  • The .json copy still points at the old script name. Most common mistake.
  • The new file ends in .off because they copied a .off file. 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.

Cheat sheet

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.


When something goes wrong

Nothing happens at all.

  1. Is the file name ending in .json, not .json.off?
  2. Did you start a new chat? (Greeter and Mystery only run on a new chat.)
  3. Is the mode Agent?
  4. Did you open the github-copilot-hooks-for-beginners folder itself, not a folder above it?
  5. 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.


Wrap (trainer)

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
AI is Good

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
Email info@hereandnowai.com
Mobile +91 996 296 1000
GitHub https://github.com/hereandnowai
LinkedIn https://www.linkedin.com/company/hereandnowai/
YouTube https://youtube.com/@hereandnow_ai
Instagram https://instagram.com/hereandnow_ai
X https://x.com/hereandnow_ai
Blog https://hereandnowai.com/blog

Licensed under the MIT License. See LICENSE.

About

Hands-on, zero-code lesson pack for teaching GitHub Copilot hooks in VS Code: 7 playful hooks, a detective game and build-your-own challenges.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors