anthropics / anthropics/claude-code
[BUG] Skill re-invocation dedupe is keyed on the RENDERED content, so changing only the arguments re-appends the entire SKILL.md — composing skills costs N × body
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
### Preflight Checklist
- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
### What's Wrong?
**Title:** Skill re-invocation dedupe is keyed on the RENDERED content, so changing only the arguments re-appends the entire SKILL.md — composing skills costs N × body
**Type:** design flaw / enhancement (the behaviour matches the docs; the docs describe a design that defeats skill composition)
## Environment
- Claude Code 2.1.276 (CLI), Linux (WSL2)
- Project skills under `.claude/skills/*/SKILL.md`, invoked by the model through the `Skill` tool
## What happens
The skills docs (§ *Skill content lifecycle*) state:
> When Claude re-invokes a skill whose rendered content is identical to the copy already in context, Claude Code adds a short note that the skill is already loaded rather than a second copy of the content. When the rendered content differs, because the arguments changed […], Claude Code appends the full content again.
So the dedupe key is the skill body **after** argument substitution. Any skill that takes an argument — i.e. any skill designed to be *called* — renders differently on every call with a new argument, and the full body is appended again each time.
## Why this is worth fixing
The control shows Claude Code **already has** the right rendering: instructions once, arguments delivered per call. It only takes that path when the arguments did *not* change — the one case where re-invoking is least useful. It is a loader that hashes a function together with its call frame, so every distinct call looks like a new function.
The re-invocation header even reads as though a delta was intended (*"the arguments or dynamic output below are new"*), but what follows is the whole body, not just the new arguments.
Practical cost: an orchestrating skill that calls a worker skill once per item (per file, per ticket, per finding) pays the worker's full body per item. That pushes authors away from small, composable, argument-taking skills toward either monolithic skills, or passing the subject *implicitly* through conversation state purely to keep the args string constant — ambient state, which is worse engineering and fails silently when mis-resolved.
### What Should Happen?
## Proposed behaviour
Key the dedupe on the skill's **template** (pre-substitution body + resolved file identity), not the rendered output:
- template unchanged, args changed → emit the short note **plus the new `Arguments:`** — the exact shape the identical-args path already produces;
- template unchanged, a dynamic-context command's output changed → emit the note plus **only the changed dynamic output**;
- template changed on disk → append the full content (today's behaviour).
If a skill interpolates `$ARGUMENTS` mid-body, the note can carry the arguments once; the model already holds the body that says where they apply.
### Error Messages/Logs
```shell
```
### Steps to Reproduce
## Repro
Use any reasonably large project skill that accepts arguments — say `.claude/skills/process-item/SKILL.md` (~6–7K tokens), whose body refers to its arguments (via `$ARGUMENTS`, or the auto-appended `ARGUMENTS:` trailer).
1. Have the model call `Skill(skill="process-item", args="item-1")` → the full body enters context. Expected.
2. Have it call `Skill(skill="process-item", args="item-2")`, then `item-3`, then `item-4`.
3. Each of those calls is answered with
```
(Re-invocation of /process-item — the skill instructions were previously loaded; the arguments or dynamic output below are new.)
```
**followed by the complete SKILL.md body again.** Four calls ≈ 20K+ tokens of byte-identical instructions; only the one argument token differs between them.
**Control** — re-invoke a skill with **byte-identical** args. The entire response is one line:
```
Skill /process-item is already loaded above; instructions unchanged. Arguments: item-1
```
### Claude Model
Opus
### Is this a regression?
No, this never worked
### Last Working Version
_No response_
### Claude Code Version
2.1.276
### Platform
Anthropic API
### Operating System
Ubuntu/Debian Linux
### Terminal/Shell
WSL (Windows Subsystem for Linux)
### Additional Information
## Not the same as
- #95177 — duplication of a user's *pasted argument text* across stacked slash-skills in one prompt (a different surface and a different duplicate).
- #21891 — closed as duplicate on the premise that dedupe exists; it does, but only for identical renders.
- #87514 — reload-plugins re-injects full skill roster without evicting the session-start copy (+~7.5k tokens per reload)
- #93987 - /reload-skills reports "no changes" for a skill added to disk mid-session (Desktop app); a fresh process sees it
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the documented “Skill content lifecycle” and reproduce the behavior using `.claude/skills/process-item/SKILL.md` and repeated `Skill` calls with different arguments. Trace the implementation that handles skill loading and re-invocation; done means unchanged skill templates avoid repeating their full body while new arguments or dynamic output remain available.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cli
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100