/claude-api skill loads the entire multi-language bundle (~230k tokens) instead of the detected project language

Status Fixed / completed
Reported on v2.1.233
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Aug 16, 2026 · closed Aug 17, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Summary

Invoking the claude-api skill with no arguments in a Python project caused the entire bundled skill document — every supported language (C#, Go, Java, PHP, Python, Ruby, TypeScript), plus the full Managed Agents API reference, model migration guide, prompt caching internals, etc. — to be loaded into context as a single ~928 KB / ~230,000-token user message.

This directly contradicts the skill's own stated instructions:

"Choose the right surface based on your needs, detect the project language, then read the relevant language-specific documentation."

Instead of doing language detection first and then reading only the matching section (e.g., just "Claude API — Python"), the base skill invocation appears to dump the whole multi-language reference doc up front, before any detection step runs.

Repro steps

  1. In a Python project (in my case: two files, src/logsum.py and tests/check_logsum.py, no existing LLM integration)
  2. Run /claude-api with no arguments
  3. Observe the first tool-adjacent message injected into context is the full skill bundle, not a language-scoped subset
  4. Only after that full load does the assistant proceed to ls, find, and read the two local source files — i.e., actual language detection happens downstream of the expensive load, not before it

Impact

  • Unnecessary token spend on every bare /claude-api invocation, scaling with the full size of the bundled doc regardless of which language is actually relevant
  • In my case, a monthly usage spend limit jumped from ~$49.18 to $57.05 (~$7.87) in the minutes immediately following a single invocation that was cancelled after only 4 quick tool calls (ls, find, 2x Read) — before any actual code was read for a "surface" decision or written
  • Worth noting the interrupted session never even got a concrete task (no argument was passed, and the target files had no existing LLM code to modify), so the full-bundle load bought nothing

Expected behavior

Per the skill's own "Language Detection" section, the doc load should be scoped to the detected project language's section (e.g., just the ~2,100-line "Claude API — Python" block) rather than the full ~15,900-line, all-language bundle.

Environment

  • Claude Code CLI
  • Skill: claude-api (bundled skill path referenced /tmp/claude-1000/bundled-skills/2.1.233/.../claude-api)
  • Project: single-file Python script + test, no existing Anthropic/OpenAI SDK usage

View original on GitHub ↗

4 Comments

jasonayre · 14 days ago

Root cause, decompiled from the 2.1.233 binary — and it explains why detection failed in your Python project.

Language detection reads the top level of the cwd only, non-recursively.

const LANG_MARKERS = {
  python:     ['.py', 'requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'],
  typescript: ['.ts', '.tsx', 'tsconfig.json', 'package.json'],
  java:       ['.java', 'pom.xml', 'build.gradle'],
  go:         ['.go', 'go.mod'],
  ruby:       ['.rb', 'Gemfile'],
  csharp:     ['.cs', '.csproj'],
  php:        ['.php', 'composer.json'],
  curl:       []
}

async function detectLang() {
  const entries = await readdir(cwd())        // <-- ROOT ONLY. Not recursive.
  for (const [lang, markers] of Object.entries(LANG_MARKERS)) {
    if (markers.length === 0) continue        // curl can never be detected
    for (const m of markers) {
      if (m.startsWith('.')) { if (entries.some(e => e.endsWith(m))) return lang }
      else                   { if (entries.includes(m))             return lang }
    }
  }
  return null
}

Your files were src/logsum.py and tests/check_logsum.py. Neither is at the root, and you had no requirements.txt / pyproject.toml / setup.py / Pipfile there either — so readdir saw only src and tests, matched nothing, and returned null.

So the title is slightly off, and I think in a way that matters for the fix: the bundle wasn't loaded "instead of the detected project language." Detection failed, and the failure branch is what dumps everything:

function buildSkillBody(lang, userArgs, bundle) {
  ...
  if (lang) {
    files = Object.keys(bundle.SKILL_FILES)
              .filter(f => f.startsWith(lang + '/') || f.startsWith('shared/'))
  } else {
    parts.push("No project language was auto-detected. Ask the user which language they are using, then refer to the matching docs below.")
    files = Object.keys(bundle.SKILL_FILES)     // <-- ALL 65 FILES
  }
  parts.push("---\n\n## Included Documentation\n\n" + renderDocs(files, ...))
  ...
}

Each file is then wrapped in <doc path="...">…</doc> and inlined verbatim. No truncation, no lazy read — the skill declares allowedTools: ["Read","Grep","Glob","WebFetch"] and ships a "Quick Task Reference" routing table that names the exact file per task, but the docs are pasted in wholesale regardless, so the routing table has nothing left to route.

Note the fallback string: it inlines all nine languages' docs and then says "Ask the user which language they are using." It pays maximum context cost precisely in the case where it knows least.

This makes the bug much broader than it looks. Root-only detection fails on any layout that doesn't keep source files or a manifest in the repo root — src/-layout Python (yours), Cargo/Rust, CMake/C++, Gradle multi-module, monorepos with packages/*, docs-only repos. I hit it on a C++/CMake repo: root has CMakeLists.txt, core/, hosts/, docs/ and nothing on the marker list, so it takes the same null path.

Measurements from the on-disk bundle (bundled-skills/2.1.232/.../claude-api, 65 files):

| Bucket | Bytes |
|---|---|
| shared/ | 565,768 |
| python / typescript / java / go / ruby / csharp / php / curl | 65,659 / 54,833 / 39,450 / 36,916 / 16,239 / 26,355 / 29,118 / 17,962 |
| All 65 files (the null branch) | 852,300 |

Single largest file: shared/model-migration.md at 174,809 bytes — 20% of the corpus, inlined whether or not you are migrating a model.

Two things worth separating in the fix:

  1. Detection is too narrow. Root-only readdir should at minimum be a shallow walk, or fall back to the marker set before giving up. But detection will always miss sometimes.
  2. The null branch is the actual cost bug, and it is fixable independently. "I don't know the language" should inline less, not everything — ask first, or inline nothing language-specific and let the model Read on demand. It already has the tool and the routing table.

And worth flagging even if both land: the success path isn't cheap either. A correctly detected language still inlines all of shared/ — 565,768 bytes, ~210k+ tokens — on top of the language set. Someone asking a one-line pricing question gets shared/models.md (12,146 bytes) plus 553KB they didn't ask for.

Prior reports of this same branch, for linking: #83818 is the closest (explicitly identifies the failed-detection path, and additionally observes the null gets interpolated into the rendered doc paths as a literal unknown/claude-api/README.md that doesn't exist). Also #80190, #81312, #86817, #74473.

If you want to size the blast radius from telemetry: the skill emits tengu_claude_api_skill_loaded with a detected_lang field — the share of "none" is the answer.

blue-az · 14 days ago

@jasonayre — this fully explains what I saw, thank you for digging into the binary. Confirms it: my repo had src/logsum.py and tests/check_logsum.py, nothing at root, no manifest file — exactly the shape that hits readdir returning nothing and falling into the null branch.

Good catch that the framing in my title was backwards — it wasn't "bundle instead of detected language," it was "detection silently failed, and failure is the expensive path." That's a worse bug than I originally described: the fallback should degrade gracefully (ask first, or inline nothing and let the model Read on demand — it already has the tool and the routing table per your point), not max out cost exactly when it has the least information to justify it.

The shared/ finding is useful context too — even in my case, correct detection wouldn't have made this cheap, just less bad. 565KB of shared docs going out regardless of the actual question is its own problem.

Given five other issues already hitting this same branch (#83818, #80190, #81312, #86817, #74473), seems like this is well past "one user's edge case" — the detected_lang: "none" telemetry share you mentioned would be the fastest way for the team to see how often this fires. Hoping this gets prioritized given the recurrence.

jasonayre · 14 days ago

NP. Ya I was pretty annoyed when my context shot up over 330k+ tokens (had a semi bloated claude.md in my repo I forgot to pull fix from other computer which put me at 500k+ total), when I opened a new (fable) session, asked a single question about api/token cost and it loaded the skill, and I was like, wtf...

I'm not holding my breath given the bot has auto closed the last issue's I've opened after x amount of time; but I did create a thread on the claude discord in hopes that someone will see it and escalate.

<img width="1085" height="990" alt="Image" src="https://github.com/user-attachments/assets/79f92366-ce28-4f6b-a4c4-83ab614d140b" />

bcherny collaborator · 13 days ago

This is fixed in 2.1.234: the built-in claude-api skill now loads its reference docs on demand instead of inlining them (changelog entry: "Reduced the context cost of loading the built-in claude-api skill from ~200k+ tokens to ~25k by loading reference docs on demand").

Verified on 2.1.234 (Linux): invoking /claude-api in a directory with no language markers (the failed-detection case described above) injects ~73 KB (~18k tokens) with no doc files inlined, and ~92 KB (~23k tokens) in a Python project — versus the ~850–930 KB you measured on 2.1.233. The doc files still ship on disk and are read individually when needed.

Changelog: https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md

Closing as fixed — reply here if you still see the whole bundle injected on 2.1.234 or later and we'll reopen.
🤖 Generated with Claude Code

---
_Generated by Claude Code_