You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
post-tool-use: '*' and 'Skill' are separate entries on the default tools, so one Skill call starts two dispatches #1236
#1232 emptied the inline post-tool-use pass, so what the host now waits for is start-up. That makes the number of dispatches the remaining cost, and on Windows a Skill tool call still runs two of them.
BUILTIN_HOOK_SPECS registers post-tool-use twice — matcher: '*' and matcher: 'Skill' — and each entry is a separate host invocation. Inside each, the background: true handlers get their own detached child (hook-dispatch.ts:36-43), so one Skill call is:
Measured on Windows 11 25H2, Git Bash, Node 22.22.2, Defender real-time on:
step
ms
node -e 0
529–681
node dist/index.js --version (bundle load)
1185–1511
dispatch with no matching handler
1965–2541
So the second entry costs roughly 2–2.5 s of start-up on a Windows host, inside the same 10 s budget the first one just used.
Why this is not the same as the timeout granularity
The split makes sense for TodoWrite, which carries TODOWRITE_HINT_TIMEOUT_MS (3 s) instead of the 10 s the rest use. But * and Skill both carry FOREGROUND_HOOK_TIMEOUT_MS — identical — so nothing is expressed by keeping them apart.
Merging those two entries would leave TodoWrite as the only separate one, and halve the process count per Skill call (one parent plus one detached child, running dashboard-report / recall-attribution / local-agent-sync / track / webhook together).
Options
Emit one post-tool-use entry per distinct timeout (* and Skill merged, TodoWrite on its own), letting the dispatch match handlers by matcher internally.
Or teach the dispatcher to accept more than one --matcher and keep the entries as they are.
Reproduced locally on macOS with the built CLI at main (8487c34). A small host simulator fired each installed hook entry whose matcher matches the tool name, and every node process start was logged. Windows was not run. There, each background launch also starts a PowerShell helper (trySpawnDetachedViaWmi) and the parent waits for it, so the cost per call is higher than the node count alone shows.
call
host entries fired
node processes
handlers
Claude Skill
2 (*, Skill)
4
as in the issue's table
Claude Read
1
2
wildcard set only
Cursor Read of a SKILL.md
2 (*, Read)
4
as for Claude Skill
Cursor Read of README.md
2 (*, Read)
4
track records nothing, but webhook still sends skill-use with data: {}
On the options. Option 1 is the one that removes a dispatch. With option 2 the host still starts each entry as its own process. --matcher also takes a single value today, and the last one given wins. On Cursor the gain is larger than the issue states, because the Skill entry is rendered as Read, so every file read pays the second dispatch today.
Approach
One host entry per distinct timeout, not per matcher.* and Skill merge, and TodoWrite stays separate for its 3 s budget. A skill use drops from four processes to two, and so does every Cursor Read. Calls that never matched the Skill entry keep their count.
Gate the skill handlers on the payload, and keep today's per-host eligibility.track and the webhook data already share one recognizer (normalizeToolName plus resolveSkillUse), so do not write a second one. That recognizer does not know the host, though. It accepts a Read of any SKILL.md. Today only hosts whose skill entry is rendered as Read (Cursor) ever send it such a payload. Under *, a Claude Read of a SKILL.md would start counting as a skill use. The host restriction that SKILL_TOOL_MATCHER expresses today has to survive the merge.
Make "not a skill use" stop the webhook. Today webhook sends skill-use even when the payload is not a skill use. That already happens on every Cursor Read (last row above). Under * it would happen on every tool call on every host. This must be fixed before or together with the merge. It is tracked separately in [bug] Cursor: every file Read sends a skill-use webhook with empty data #1238.
Cover every place that dispatches post-tool-use, not only the settings entries. The generated OpenCode plugin issues its own * and Skill dispatches. If it keeps both, OpenCode either runs the skill handlers twice or keeps the extra startup.
Existing installs. For Claude, Codex and Cursor, a plain pull already removes a retired built-in entry. That was verified by planting a stale entry. The exact-match cleanup paths are a different case. They are Copilot's user install and the team-only cleanup, and they recognize built-ins from the current definitions, so they need to keep recognizing the retired one.
Decision for maintainers. A team's builtin: override can disable the Hook dispatch post-tool-use Skill key or change its timeout. After the merge, that key no longer exists on its own. Silently ignoring it would turn skill tracking and webhooks back on for a team that disabled them. The project needs to decide what that override means from now on before the change lands.
Proof of done. With the same simulator, one skill use fires one host entry and runs track and webhook exactly once. A plain Read on Cursor and Claude fires neither, and so does a Claude Read of a SKILL.md. No skill-use webhook is sent for any of them. A Windows timing from the reporter, before and after, would confirm the saving there.
changed the title [-]Windows: post-tool-use still runs two dispatches per Skill call — '*' and 'Skill' share the same 10 s budget[/-][+]post-tool-use: '*' and 'Skill' are separate entries on the default tools, so one Skill call starts two dispatches[/+]on Oct 21, 2026
Before implementing it, could you confirm the intended compatibility behavior for existing builtin: overrides targeting Hook dispatch post-tool-use Skill?
My proposed approach is:
merge the * and Skill host entries when they use the same timeout;
keep TodoWrite separate because it has a different timeout;
preserve host-specific skill eligibility so a Claude Read of SKILL.md does not become a skill use;
update the generated OpenCode plugin as well as normal hook settings;
retain cleanup recognition for the retired entry;
add regression coverage for Cursor and Claude ordinary reads, real skill use, webhook behavior, and legacy overrides;
record a representative built-CLI run and Windows before/after timing.
Once the legacy override behavior is confirmed, I can keep the implementation and tests focused on that contract.
I’ll implement one post-tool-use host entry per timeout: merge the wildcard and skill dispatches that share the foreground timeout, while keeping TodoWrite separate. I’ll preserve the current host-specific skill eligibility, update the generated OpenCode path and stale-entry cleanup, and make sure non-skill calls never emit skill-use.
For compatibility, I plan to translate the retired Hook dispatch post-tool-use Skill builtin override into the merged entry rather than silently ignoring an existing disable/timeout override. I’ll add focused regression coverage for legacy overrides, Cursor and Claude ordinary reads, real skill use, webhook delivery, and cleanup of existing installs, plus a representative built-CLI run and Windows before/after timing. If you prefer a different migration rule for the retired override, please let me know.
Correction after checking the latest main: #1245 (22dfe27) already merged the Skill pass into the wildcard post-tool-use pass. The current built-ins have one wildcard entry plus the separate TodoWrite timeout, skill-use is gated in skillUseHandler, the generated OpenCode plugin dispatches once, and the retired Skill key is preserved for compatibility. So I won’t open a duplicate PR for this issue. The remaining useful action is verification/closure rather than another implementation.
Thanks @XY-code1 for checking main before opening a PR. You're right: this is fixed by #1245 (22dfe276).
We re-ran the simulator from the triage above, through the built CLI on macOS:
call
0.28.0-beta.7
main (13c891e0) and 0.28.0-beta.8
Claude Skill
2 entries, 4 node processes
1 entry, 2
Cursor Read of README.md
2 entries, 4
1 entry, 2
Cursor Read of a SKILL.md
2 entries, 4
1 entry, 2
The default tools now install one * entry plus TodoWrite. A plain pull removes the old Skill entry on Claude, Codex and Cursor.
On the override question the triage left open: a team builtin: override that disables Hook dispatch post-tool-use Skill still keeps skill tracking from tool calls and the skill-use webhook off. A timeout override on that key is not carried over. The merged entry is Hook dispatch post-tool-use wildcard, so a team that wants another timeout has to name that key.
One correction to the proof of done above: #1245 counts a Read of an installed SKILL.md as a skill use by default on every host with a post-tool-use hook, Claude included. That is by design, so that row now records one skill use, not none.
0.27.0 (latest) still has the double dispatch on Claude Skill. The fix is in 0.28.0-beta.8 (npm i -g teamai-cli@0.28.0-beta.8) and will be in the next stable release; please test there. After upgrading, one teamai pull removes the old Skill entry; no configuration change is needed. Not checked: Windows timing.
Closing as completed. Reopen if you still see two dispatches per skill call on 0.28.0-beta.8 or later.
Activity
SaulMoro commentedon Oct 20, 2026
Triage
Reproduced locally on macOS with the built CLI at
main(8487c34). A small host simulator fired each installed hook entry whose matcher matches the tool name, and every node process start was logged. Windows was not run. There, each background launch also starts a PowerShell helper (trySpawnDetachedViaWmi) and the parent waits for it, so the cost per call is higher than the node count alone shows.Skill*,Skill)ReadReadof aSKILL.md*,Read)SkillReadofREADME.md*,Read)trackrecords nothing, butwebhookstill sendsskill-usewithdata: {}On the options. Option 1 is the one that removes a dispatch. With option 2 the host still starts each entry as its own process.
--matcheralso takes a single value today, and the last one given wins. On Cursor the gain is larger than the issue states, because theSkillentry is rendered asRead, so every file read pays the second dispatch today.Approach
*andSkillmerge, andTodoWritestays separate for its 3 s budget. A skill use drops from four processes to two, and so does every CursorRead. Calls that never matched theSkillentry keep their count.trackand the webhook data already share one recognizer (normalizeToolNameplusresolveSkillUse), so do not write a second one. That recognizer does not know the host, though. It accepts aReadof anySKILL.md. Today only hosts whose skill entry is rendered asRead(Cursor) ever send it such a payload. Under*, a ClaudeReadof aSKILL.mdwould start counting as a skill use. The host restriction thatSKILL_TOOL_MATCHERexpresses today has to survive the merge.webhooksendsskill-useeven when the payload is not a skill use. That already happens on every CursorRead(last row above). Under*it would happen on every tool call on every host. This must be fixed before or together with the merge. It is tracked separately in [bug] Cursor: every file Read sends a skill-use webhook with empty data #1238.post-tool-use, not only the settings entries. The generated OpenCode plugin issues its own*andSkilldispatches. If it keeps both, OpenCode either runs the skill handlers twice or keeps the extra startup.pullalready removes a retired built-in entry. That was verified by planting a stale entry. The exact-match cleanup paths are a different case. They are Copilot's user install and the team-only cleanup, and they recognize built-ins from the current definitions, so they need to keep recognizing the retired one.Decision for maintainers. A team's
builtin:override can disable theHook dispatch post-tool-use Skillkey or change its timeout. After the merge, that key no longer exists on its own. Silently ignoring it would turn skill tracking and webhooks back on for a team that disabled them. The project needs to decide what that override means from now on before the change lands.Proof of done. With the same simulator, one skill use fires one host entry and runs
trackandwebhookexactly once. A plainReadon Cursor and Claude fires neither, and so does a ClaudeReadof aSKILL.md. Noskill-usewebhook is sent for any of them. A Windows timing from the reporter, before and after, would confirm the saving there.[-]Windows: post-tool-use still runs two dispatches per Skill call — '*' and 'Skill' share the same 10 s budget[/-][+]post-tool-use: '*' and 'Skill' are separate entries on the default tools, so one Skill call starts two dispatches[/+]XY-code1 commentedon Oct 25, 2026
Hi, I’d like to work on this issue.
Before implementing it, could you confirm the intended compatibility behavior for existing
builtin:overrides targetingHook dispatch post-tool-use Skill?My proposed approach is:
*andSkillhost entries when they use the same timeout;TodoWriteseparate because it has a different timeout;ReadofSKILL.mddoes not become a skill use;Once the legacy override behavior is confirmed, I can keep the implementation and tests focused on that contract.
XY-code1 commentedon Oct 25, 2026
Hi, I’d like to work on this issue.
I’ll implement one post-tool-use host entry per timeout: merge the wildcard and skill dispatches that share the foreground timeout, while keeping TodoWrite separate. I’ll preserve the current host-specific skill eligibility, update the generated OpenCode path and stale-entry cleanup, and make sure non-skill calls never emit skill-use.
For compatibility, I plan to translate the retired Hook dispatch post-tool-use Skill builtin override into the merged entry rather than silently ignoring an existing disable/timeout override. I’ll add focused regression coverage for legacy overrides, Cursor and Claude ordinary reads, real skill use, webhook delivery, and cleanup of existing installs, plus a representative built-CLI run and Windows before/after timing. If you prefer a different migration rule for the retired override, please let me know.
XY-code1 commentedon Oct 25, 2026
Correction after checking the latest main: #1245 (22dfe27) already merged the Skill pass into the wildcard post-tool-use pass. The current built-ins have one wildcard entry plus the separate TodoWrite timeout, skill-use is gated in skillUseHandler, the generated OpenCode plugin dispatches once, and the retired Skill key is preserved for compatibility. So I won’t open a duplicate PR for this issue. The remaining useful action is verification/closure rather than another implementation.
SaulMoro commentedon Oct 25, 2026
Thanks @XY-code1 for checking main before opening a PR. You're right: this is fixed by #1245 (
22dfe276).We re-ran the simulator from the triage above, through the built CLI on macOS:
main(13c891e0) and 0.28.0-beta.8SkillReadofREADME.mdReadof aSKILL.md*entry plusTodoWrite. A plainpullremoves the oldSkillentry on Claude, Codex and Cursor.builtin:override that disablesHook dispatch post-tool-use Skillstill keeps skill tracking from tool calls and the skill-use webhook off. A timeout override on that key is not carried over. The merged entry isHook dispatch post-tool-use wildcard, so a team that wants another timeout has to name that key.One correction to the proof of done above: #1245 counts a
Readof an installedSKILL.mdas a skill use by default on every host with a post-tool-use hook, Claude included. That is by design, so that row now records one skill use, not none.0.27.0 (
latest) still has the double dispatch on ClaudeSkill. The fix is in 0.28.0-beta.8 (npm i -g teamai-cli@0.28.0-beta.8) and will be in the next stable release; please test there. After upgrading, oneteamai pullremoves the oldSkillentry; no configuration change is needed. Not checked: Windows timing.Closing as completed. Reopen if you still see two dispatches per skill call on 0.28.0-beta.8 or later.