Tutorial · Updated
VS Code agent hooks: capture every Copilot agent edit
How VS Code's agent hooks (preview) report Copilot agent mode edits, the file they live in, what the PostToolUse payload does and doesn't contain, and how to recover the changed hunks.
VS Code can run a shell command at fixed points in a Copilot agent mode session: when a session starts, when you submit a prompt, before and after each tool call, and when the agent stops. The command gets a JSON payload on stdin. As of September 2026 the feature is in preview, so names and fields may change.
This tutorial sets up a hook that fires on every agent edit, shows what the payload holds and what it leaves out, and turns it into the changed hunks with git. The references are the official VS Code hooks guide and the hooks reference.
Where hooks live
The setting chat.useHooks turns hooks on, and it is on by default. VS Code then reads hook files from these places:
- Workspace: any
.jsonfile in.github/hooks/. - User:
~/.copilot/hooks/*.json. - Custom agents and plugins: hooks in an
.agent.mdfront matter, or a plugin'shooks.json. - Claude Code's files:
.claude/settings.jsonand.claude/settings.local.json, only whenchat.useClaudeHooksis on. It is off by default.
chat.hookFilesLocations adds or disables locations. For a hook that points at a script on your own machine, use a file of its own in .github/hooks/, such as log-edits.json, and keep it out of git.
A minimal config
The format is flat. Each event name maps straight to a list of commands, with no matcher and no nested groups. This file runs one script after every tool call and at each turn's start and end:
- PostToolUse runs after every tool the agent calls: reads, searches, terminal commands and edits. With no matcher, the script has to pick out the edits itself.
- UserPromptSubmit marks a turn's start and Stop its end.
timeoutis in seconds. The reference gives 30 as the default.- A command can also have
windows,linuxandosxoverrides, and anenvobject for environment variables.
For a worked example, this is the file TypeBack's installer writes on a Mac with node on the PATH:
What the script receives
Every event sends timestamp, cwd, session_id, hook_event_name and transcript_path. The reference marks all but the first and the event name as optional. PostToolUse adds tool_name, tool_input, tool_use_id and tool_response. UserPromptSubmit adds prompt. Stop adds stop_hook_active, which is true when the agent is already continuing because a Stop hook blocked it.
The reference does not list the tool names or their inputs. It tells you to read the agent debug logs for them. As of September 2026, Copilot agent edits come through two tools:
editFiles, withtool_input.files: a list of paths.createFile, withtool_input.path.
An edit payload, trimmed to the fields that matter:
That is all you get: which files changed. There is no before text, no after text and no line numbers. Paths may be relative, so resolve each one against cwd.
Recover the changed hunks with git
The file on disk is already the after state, so git can supply the before state. For a tracked file, git diff HEAD -- file gives the hunks. For a new, untracked file, the whole file is the change. Outside a repository there is nothing to diff against, so the whole file is all you have.
This script does that and appends one JSON line per file to ~/agent-edits.jsonl. It logs turn events too, and ignores every other tool. It uses execFileSync with an argument list, so a file name is never parsed by a shell.
It is CommonJS. Keep it outside any folder whose package.json says "type": "module", or name it on-edit.cjs. Try it on a repository with an uncommitted change:
One limit: git diff HEAD shows everything since the last commit. That includes your own edits and the agent's earlier edits to the same file. To get only this edit, keep the last diff you saw for each file and compare, or drop hunks you have already logged.
Gotchas
It is a preview
The hook events, fields and tool names can change between VS Code releases. The docs warn against copying a tool name from another agent: Claude Code's Edit is not Copilot's editFiles. Check the debug logs after an update.
Every hook is a process start the agent waits for
As of September 2026, VS Code hooks have no background option like Claude Code's async. Each hook runs in the foreground, and with no matcher, PostToolUse runs after every tool call, not only edits. Make the script return fast for the tools it does not care about, and set a short timeout.
Stay off stdout, and exit 0
On exit code 0, VS Code reads stdout, and JSON there can add context or control what the agent does next. Exit code 2 makes stderr a blocking error that goes to the model. Any other code shows a warning. A logging hook should print nothing and exit 0.
Claude Code's hooks can fire for Copilot
With chat.useClaudeHooks on, VS Code also runs the hooks in .claude/settings*.json, and it ignores their matchers, so every command for the event runs. A Claude Code edit hook then fires after every Copilot tool call, and Claude Code's turn hooks fire for Copilot turns. To tell the two apart, check for timestamp: every VS Code payload has it, and Claude Code's never do. The Claude Code hooks tutorial covers that side.
Windows runs PowerShell
On Windows, VS Code runs the command with powershell.exe -Command. A command that starts with a quoted path is read as a string there, not run, so the hook never starts. Prefix it with the call operator (& 'C:\Program Files\nodejs\node.exe' ...), or give a separate windows command:
Environment variables belong in the env field, not in a shell prefix such as FOO=1 node ..., which PowerShell does not understand.
Guard what you read
The payload names a path. It does not promise a regular file of a sane size. Reading a FIFO or a device would block the hook until the timeout. Check with stat first, as the script does, and skip anything large.
Keep the hook file out of git
.github/hooks/ is also where a team commits hooks meant for everyone. A file there with your absolute paths would run a script your teammates do not have, after every tool call. Keep your own file out with .git/info/exclude. It works like .gitignore, but it lives in your clone and is never committed.
git rev-parse --git-path info/exclude finds the right file in a linked worktree too. Paths in it are relative to the repository root. git check-ignore -v prints the rule that matched.