microsoft / microsoft/upgrade-agent-plugins
hooks.json: PostToolUse hook is unscoped — spawns a process on every tool call in every session
@lonitra is already working on this.
Since Aug 25, 2026.
- Dominant language
- PowerShell
- Stars
- 37
- Forks
- 8
- Avg merge
- 3d 10h
- Merged PRs (30d)
- 6
Description
The PostToolUse entry in plugins/upgrade-agent/hooks.json declares no matcher or
scope, so it is invoked after every tool call, in every session, under every agent —
including sessions and agents unrelated to upgrades.
Environment
- Plugin:
upgrade-agent1.1.404 (commit a4f718e) - Installed via the
ms-dotnettools.upgrade-agentVS Code extension 1.1.290 - Host: VS Code 1.133.0, Copilot Chat
- OS: macOS 26.6 (25G72), arm64
Observed
A plain chat session — default agent, no upgrade context — asking "read plan.md and
summarize it" fires the hook on each file-search call.
Additional symptom: the chat transcript is polluted
The unconditional invocation is also directly user-visible. Because the hook runs on
every tool call, its output is interleaved with the actual work on every step. A
trivial two-step request produces:
Half the visible steps are hook noise. This scales linearly with tool use, so longer
agentic runs become hard to read, and the warnings look alarming despite being
completely inert — the script always returns {"continue":true} and never blocks the
host call. Users have no way to tell they're harmless, or that they come from a plugin
unrelated to what they're currently doing.
The two defects compound here: # makes each invocation fail loudly, and the missing
scope makes it invoked everywhere. Fixing # alone silences the transcript but leaves
the spawn cost described above; adding scope alone reduces the noise to file-read calls
only. Both are worth fixing.
Separately, it may be worth considering whether a non-blocking hook failure should
surface in the user-facing transcript at all, rather than in a log — that would protect
users from any misbehaving third-party hook, not just this one.
Why this matters
All filtering happens inside the script, after the process has already spawned:
TOOL_NAME=$(extract_field "$INPUT" tool_name toolName) # spawns jq or python3
case "$TOOL_NAME" in
read_file|view|Read) ;;
*) write_success ;; # ...only to discard
esac
An unrelated tool call therefore costs a bash spawn plus a jq/python3 spawn purely
to determine the hook isn't interested. Per tool call, in every session, indefinitely.
The whitelist is static and known ahead of time (read_file, view, Read), so this
decision could be made before any process is created.
To be clear
The script itself is well behaved: it never blocks the host tool call, makes no network
requests, and records only the names of its own SKILL.md loads to a temp directory. It
does not capture user code, file contents, or prompts. The concern is solely that the
invocation is unconditional when the filtering criteria are static.
Suggested fix
Scope the hook at the manifest level so the runner can skip it without spawning a
process — e.g. restrict it to the file-read tools the script already whitelists.
If the current hooks schema has no such mechanism, this may belong with #29
(Adopt Agent Plugins 1.0).
Related
See: https://github.com/microsoft/upgrade-agent-plugins/issues/33
Fixing that will make this one's cost real rather than theoretical.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.