microsoft / microsoft/upgrade-agent-plugins

hooks.json: PostToolUse hook is unscoped — spawns a process on every tool call in every session

Open
#34 2 comments 0 reactions 1 assignee View on GitHub

@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-agent 1.1.404 (commit a4f718e)
  • Installed via the ms-dotnettools.upgrade-agent VS 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:

Image

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.