Skill full body re-attached every turn while "active," bypassing all hooks
Bug report: Claude Code re-attaches full skill body on every turn while a skill is "active," bypassing any hook
Summary
In long-running sessions, Claude Code re-injects the full body of an already-loaded skill into the context on many subsequent turns, without a new Skill tool invocation. This happens via a client-native attachment record of type invoked_skills (paired with a plan_file_reference attachment), not a tool call, so it never fires pre_tool_use, pre_compact, user_prompt_submit, or any other hook event. There is currently no way for a hook-based tool (e.g. token-goat) to intercept, dedupe, or shrink this resend, because the resend does not go through the hook pipeline at all.
Impact
- A single skill (
XY, ~22.5KB body) was loaded via a genuineSkilltool call exactly once, but its full content reappeared as a rawinvoked_skillsattachment 9 additional times across the session (both before and after a/compact), for roughly 200KB+ of redundant context. - After
/compact, context usage stayed around ~60% full instead of dropping close to empty, because these attachment resends (plus similarly-repeatingplan_file_reference/agent_listing_delta/ CLAUDE.md-style file attachment blocks) survive compaction and continue accumulating on every subsequent turn. - This scales with session length: skills that stay "active" longer, and sessions that run for days, accumulate proportionally more redundant bytes with no way for the user or any installed hook to stop it.
What I ruled out
I initially suspected a bug or staleness in a tool's own skill-dedup logic (token-goat's preSkillHandler, which denies a duplicate Skill tool call and redirects the model to a compact-body CLI read). Investigation confirmed:
- That dedup logic is present, current, and correct — it fires properly on genuine repeat
Skilltool_use calls elsewhere in the same transcript. - The 9 extra
XYcopies are notSkilltool_use/tool_result pairs at all — grep across the transcript's raw JSONL shows only one"name":"Skill"entry for that skill. The other copies are"type":"attachment","attachment":{"type":"invoked_skills",...}records with no corresponding tool call. - Because no tool call occurs,
pre_tool_use(or any other hook event) never fires for these resends — there is no hook surface in the current Claude Code hooks API that runs before this attachment is constructed and attached to the turn.
Request
Either of these would resolve it:
- Stop re-attaching a skill's full body on every turn while it's "active." Instead, attach it once, and on subsequent turns pass a lightweight reference (name/path only) unless the content has changed — the model already has the content in its context history within the same (uncompacted) window, and post-compact it should be re-loaded explicitly (via a real tool call) rather than silently reappearing in full.
- If the re-attachment behavior is intentional (e.g. to guarantee the skill stays available after compaction), expose a hook event that fires before this attachment is constructed — e.g. a
pre_attachmentor extendedpre_compacthook carrying the candidate attachment payload — so third-party tools can inspect/shrink/dedupe it the same way they already can for tool calls.