Skip to content

post-tool-use: '*' and 'Skill' are separate entries on the default tools, so one Skill call starts two dispatches #1236

Closed

Description

@CarlosWonMore

Context

#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:

dispatch handlers
* parent (inline) none, after #1232
* detached child dashboard-report, recall-attribution, local-agent-sync
Skill parent (inline) track
Skill detached child webhook

Four node processes for one tool call.

Cost on Windows

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

Nothing here is a regression from #1232 — this is the second multiplier from the original report, re-stated now that the first one is gone.

Related: #1216 (closed), #1232 (merged).

Activity

  1. SaulMoro commented on Oct 20, 2026

    @SaulMoro
    Collaborator

    This was generated by AI during triage.

    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.

    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.

  2. 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
  3. XY-code1 commented on Oct 25, 2026

    @XY-code1
    Contributor

    Hi, I’d like to work on this issue.

    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.

  4. XY-code1 commented on Oct 25, 2026

    @XY-code1
    Contributor

    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.

  5. XY-code1 commented on Oct 25, 2026

    @XY-code1
    Contributor

    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.

  6. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    Collaborator

    This was generated by AI during triage.

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions