[BUG] Typed slash-command invocation serves a stale SKILL.md snapshot; the Skill tool serves current disk
Preflight Checklist
- [x] I have searched existing issues 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?
Invoking a user skill by typing its slash command can inject a stale snapshot of that skill's SKILL.md, while invoking the same skill through the Skill tool in the same session injects the current file from disk.
Observed once with byte-exact evidence. In one session, two injections of the same skill 2m12s apart:
| time (UTC) | invocation path | content injected |
|---|---|---|
| 18:35:00.217Z | Skill tool | current file on disk — exact |
| 18:37:12.384Z | user typed /<skill> <arg> | a file state that had not existed on disk for 83 minutes |
The stale content was not an older released version. It was the working-tree state that existed for 14.448 seconds, between two successive Edit calls while another process was mid-edit of that file:
17:13:44.917Z— edit 117:14:02.078Z— edit 217:14:16.526Z— edit 3 (final)
Taking the file as committed after edit 3 and reversing only edit 3 reproduces the injected body byte-identical (43,081 characters). So the typed path served a snapshot sampled inside that 14.4-second window and continued serving it 83 minutes later, after the file had been stable on disk the whole time.
The failure is silent. The injected text carries no marker distinguishing it from current, and in this case the stale copy contradicted another section of the same file that the newer text had been written to fix.n
What Should Happen?
Both invocation paths should inject the same content: the file as it exists on disk at invocation time. If a cache backs the typed path, a file modification must invalidate it.
Error Messages/Logs
None. There is no error, warning, or log line. Both invocations succeed and render normally; only the content differs. That is the core of the report — the failure produces no signal at all.
Steps to Reproduce
Not reliably reproduced on demand; the one captured occurrence came from normal use. The conditions present were:
- A user skill at
~/.claude/skills/<name>/SKILL.md. - Two or more CLI processes running concurrently against that shared skills directory (separate terminal panes, different working directories).
- Process A edits
SKILL.mdseveral times in rapid succession — here threeEditcalls spanning 32 seconds, the last two 14.4 seconds apart. - Later, in process B, invoke the skill via the
Skilltool, then by typing/<name>. - Compare the two injected bodies.
Expected: identical. Observed: the typed injection carried the state between edits 2 and 3.
The suspected trigger is the rapid edit burst — specifically a change event for the final edit being coalesced or dropped, leaving a cache holding the second-to-last state.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
Unknown — not fabricating one. The typed path is usually correct on the same version that failed (see Additional Information), so this is an intermittent fault rather than a version where the path is broadly broken.
Claude Code Version
2.1.247 — the process that produced the stale injection. Caveat on any reported version, including mine: a running process keeps the binary it launched with. An update lands on disk without reaching live sessions, and /clear does not help — it starts a new session inside the same process. So the version a user reports is often the version installed, not the version executing. Measured on this machine, 2026-08-31 02:00-06:00 UTC, counting session records by the version that wrote them — seven distinct versions were running simultaneously, spanning 31 releases: | version | records | |---|---| | 2.1.251 | 1,048 | | 2.1.247 | 1,001 | | 2.1.221 | 137 | | 2.1.238 | 107 | | 2.1.222 | 8 | | 2.1.233 | 7 | | 2.1.220 | 4 | One of those panes had been running since 2026-08-04 and was executing 2.1.221, a binary that no longer existed on disk — the updater had deleted it, and the process was still running from the unlinked inode (lsof showed the txt mapping to a path with no directory entry). It was 30 releases behind and doing live work. Restarting the pane is the only thing that moves it; no install can. Two consequences for triage: 1. "Cannot reproduce on latest" is weak evidence here. This bug needs a specific trigger (a rapid multi-edit burst on a skill file concurrent with a typed invocation) and occurred in roughly 1 of 122 typed invocations even on the version where it was captured. 2. If you ask a reporter for their version, consider asking what the running process maps, not what claude --version prints. They can differ by months. The failure and all same-version control evidence in this report are from 2.1.247.
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
Where I got to in the binary (2.1.247)
I read the bundled JS out of ~/.local/share/claude/versions/2.1.247 (strings-style extraction, same technique as #54535). This is offered as a pointer for whoever owns this code, not as a diagnosis — I have not traced which cache each invocation path reads.
Skill body is snapshotted at scan time, not read at invocation. The command object closes over markdownContent:
markdownContent: b,
contentHash: Bun.hash(m).toString(36),
...
async getPromptForCommand(F,O){
let H = h ? `Base directory for this skill: ${h}\n${o}` : ...
o is the captured content. So whatever is cached at scan time is what gets injected, indefinitely, until something invalidates it.
There are two invalidators, and they cover different caches:
clearLoadCaches(){ this.invalidateCommands(), this.skillDirCommands.clear(), this.markdownFiles.clear(), ... }
invalidateCommands(){ this.builtinCommandTable=void 0, ..., this.loadedCommands.clear(),
this.skillToolCommands.clear(), this.slashCommandToolSkills.clear(),
this.skillIndex.clear() }
invalidateSkillDirs(){ this.skillDirCommands.clear(); ... }
Note skillToolCommands and slashCommandToolSkills are separate maps — one per invocation path, which is the split this bug shows up across.
The asymmetry worth checking: the reloadSkills re-scan path calls invalidateCommands() only —
function p9r(){ as().invalidateCommands(), jGs?.() }
function Ype(){ p9r(), PDe(), Ivn(), Ijr() }
// call site: if(m) Ype(), O0e(), AIn.emit(), ce("hook_session_start_reload_skills")
— so it does not clear skillDirCommands. If one path resolves through skillDirCommands while the other rebuilds from the command tables, the two can disagree after a partial invalidation, which is the shape observed here.
Ruled out while looking: skillContentIntern is not implicated. Its helper is a correct interner — it returns the cached string only when it equals the fresh one, otherwise it stores and returns the fresh value:
function Ix(e,t,n){ let r=e.get(t); if(r!==void 0&&r===n) return r; return e.set(t,n), n }
One thing I could not reconcile, and it may matter. I found no file watcher for skill directories (reloadSkills appears only as a SessionStart hook field: "Re-scan skill and command directories after SessionStart hooks complete"). Yet in one session on 2.1.247 the typed path picked up a file change mid-session with no restart: session 77ce6818, typed invocations at 05:19:52Z (pre-change content) and 05:22:04Z (post-change content), with the file modified at 05:20:13Z between them. Something refreshes these caches that I could not find, and whatever it is refreshed correctly there and did not in the failing case.
What I ruled out, and how often this happens
Model is not implicated. The confirmed occurrence was on claude-opus-5, but the skill file is read and injected by the CLI before the model is invoked, so the model cannot influence which bytes are loaded. I report it only because the form asks.
I checked several plausible explanations against a scan of 1,615 skill injections across 542 session transcripts on this machine. Three are ruled out:
1. Not "cached at process start." In one session (2.1.247), typing the same slash command twice, 2m12s apart, straddled a commit that modified the file:
05:19:52Z— served the pre-change content (correct; file changed at05:20:13Z)05:22:04Z— served the post-change content (correct)
Same process, same session. The typed path picked up a file change in under two minutes. Separately, a different long-lived process (uptime 21h) served a change made 11 hours after its start. The cache does refresh on change — it just missed one.
2. Not a per-session cache. The reading session's first event is 18:28:40Z. The stale content was sampled at 17:14:0x, 83 minutes before that session existed. Whatever holds it outlives the session and survives /clear.
3. Not a mis-keyed or cross-wired lookup. Of the injections whose content resolved to a known state of the named file (~1,516), zero carried a different skill's content. Every divergent render is a coherent whole-file state of the correct skill. This argues against a wrong-pointer or allocation fault and for plain staleness.
Frequency. Among skills my comparison could resolve, typed injections matched the live file in 999 of 1,008 cases (99.1%). So this is rare — but it lands precisely on skills edited in a burst, which is the normal editing pattern, and it persists for as long as the cache goes unrefreshed.
Measurement caveat. My comparison could not resolve roughly 6% of injections, concentrated in skills that use inline $ARGUMENTS substitution — the substituted body can never hash-match its source file. Those are excluded from the rates above rather than counted either way. The single confirmed incident was verified by direct byte-level reconstruction, not by that comparison.
How long a stale snapshot can persist: the lifetime of the process. The 83 minutes in the reproduction is not the bound — it is just when I happened to catch it. Because the content is captured into a closure at scan time and the process outlives every session inside it, the ceiling is however long that pane stays open.
Measured on this machine before I restarted them: the live panes had been running 3 to 27 days, with seven of them 9 days old and one 26 days old. That 26-day pane was still doing real work, and the skill text it would inject had been read on the day it launched. Every edit to those files in the intervening weeks was invisible to it through the typed path.
That is the practical severity. A user does not perceive a long-lived pane as stale state — they open a terminal, run /clear, and reasonably believe they are starting fresh. They are, on a month-old copy of their own instructions.
Why this matters beyond one bad render. Skill files are behavioral instructions. A stale copy means the session follows superseded instructions and reports success, and the usual assumption — "I edited the skill, so the next invocation has it" — silently stops holding. Two invocation paths disagreeing about the same file, with no marker on either, makes it undiagnosable from inside a session.