Skip to content

fix(skills): don't copy Anthropic's synced skills into sandboxes - #226

Draft
khromov wants to merge 4 commits into
mainfrom
fix/skills-large-file-injection
Draft

khromov wants to merge 4 commits into
mainfrom
fix/skills-large-file-injection

Conversation

@khromov

@khromov khromov commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Root cause

  • Who prints the warnings: the claude-code-skills injection (src/container-injections/claude-code-skills.ts) copies the host ~/.claude/CLAUDE.md and everything under ~/.claude/skills/ into the container, one execInContainer write per file. It skips any file over MAX_FILE_BYTES = 90_000. That limit exists because a single exec carries its payload in one env var, which Linux caps at 128 KiB (MAX_ARG_STRLEN), and base64 inflates the bytes by 4/3.
  • What skills/synced/<org>_<account>/ is: Claude Code's own sync of the skills enabled on the user's claude.ai account. The bucket directory is named <org-uuid>_<account-uuid>, lowercased (unbound in place of the account when there isn't one), and next to it sits a .bucket-<id> marker. Inside are a manifest.json (skillId, name, description, source, updatedAt and creatorType for each skill) and a .last-complete-round file.
  • The skipped files: large .xsd schema files bundled with Anthropic's own skills (the docx/xlsx/pptx family). I haven't checked which scripts load them, but a copy missing them isn't the skill as published.

Fix

Anthropic's own skills are no longer copied into sandboxes. The Claude Code inside the sandbox syncs them itself, and I checked this against the 2.1.283 binary. The syncClaudeAiSkills setting's description says: "synced skills are available in every session, re-synced every 10 minutes". Any Claude Code logged in to the same account therefore downloads the complete skills, unless the user has turned the setting off. Copying them was redundant and, because of the size cap, produced incomplete copies.

  • Anthropic skills are skipped. A skill is skipped when its manifest entry has creatorType: "anthropic", which covers both source: "anthropic" and source: "anthropic-example" entries. For a manifest without creatorType, the fallback is a source starting with anthropic. Manifest entries are matched to directories the way Claude Code names them: the name with <>:"|?*\/ replaced by _ and trailing dots/spaces dropped, compared case-insensitively. A directory is never matched by skillId, since Claude Code never names one that way. Skills authored by the user or their org are still copied, as is everything else under ~/.claude/skills/.
  • The manifest is trimmed. It is rewritten to list exactly the directories that were copied (rows whose directory isn't on the host are dropped too), and the host-specific staleDirs/pendingClaims fields are dropped. Claude Code reads the manifest's skills[].name to decide which bucket directories it owns, so the rewritten manifest matches what is on disk.
  • .last-complete-round is not copied. It's the sync's reuse stamp, judged by mtime; a freshly copied one could make the sandbox skip listing and so delay re-downloading the dropped skills.
  • Some buckets are dropped whole, including their .bucket- marker:
    • buckets with nothing left after exclusion;
    • buckets whose manifest is missing, unparseable, or has no skills array (without a manifest, authorship can't be judged; if the sandbox's Claude Code is signed in with sync on, its own sync brings the contents back).
  • Unlisted directories are skipped. A directory in a bucket that the manifest doesn't list has unknown authorship and is skipped. The sync's own skills/.staging and skills/.trash are skipped too.
  • One warning line. MAX_FILE_BYTES is back to 90 KB, and anything still skipped is reported in a single summary line instead of one line per file.

The earlier chunked-write change to container-files.server.ts is reverted.

Tests

New tests in injections.isolated.test.ts check that:

  • Anthropic skills are skipped, while user/org synced skills and plain skills are copied along with a trimmed manifest;
  • a bucket holding only Anthropic skills is dropped along with its marker;
  • a missing, unparseable, or skill-less manifest drops the bucket;
  • a directory the manifest doesn't list is skipped;
  • .staging and .trash are skipped;
  • the creatorType/source fallback works;
  • the real manifest shape (both fields, with anthropic-example skills carrying creatorType: "anthropic") is handled;
  • a manifest without creatorType falls back to source;
  • a skill whose name Claude Code sanitized (team/notes → team_notes) is matched to its directory;
  • a directory named only after a skillId isn't copied, and rows with no directory leave the manifest;
  • several buckets are judged independently, and a missing .bucket- marker isn't invented.

bun run checks passes.

Live boot

Booted a nested instance against a fake host config (via the claude_config_dir setting) holding a synced bucket with an Anthropic docx skill (150 KB .xsd), a user skill named team/notes in team_notes/, an unlisted orphan/ dir, .last-complete-round, staleDirs/pendingClaims, plus a plain skill, a .trash dir and CLAUDE.md. The boot log said Injecting 5 global skill/CLAUDE.md file(s)… / ✓ Global skills & CLAUDE.md injected with no size warning. The sandbox got CLAUDE.md, skills/mine/, the bucket marker, team_notes/, and a manifest listing only team/notes, without staleDirs/pendingClaims. Anthropic's docx, orphan, .last-complete-round and .trash were all left out.

The re-sync of Anthropic's skills inside the sandbox wasn't exercised live, because the fake config has no credentials. From reading the 2.1.283 sync code: a listed remote skill missing from the manifest is downloaded as new, and a kept row with an unchanged updatedAt and a SKILL.md on disk is carried over without a download.

Caveat: this only restores Anthropic's skills when the sandbox's Claude Code is logged in to claude.ai with skill sync on. With an API key, a custom endpoint, or syncClaudeAiSkills off, they're absent from the sandbox, where before they were copied (minus the oversized files).

…d of skipping them

The claude-code-skills injection skipped any host skill file over 90 KB because a single
execInContainer write is capped near 128 KiB (MAX_ARG_STRLEN on the env-var stdin carrier).
The claude.ai-synced docx/xlsx/pptx skills ship ISO-IEC29500 XSDs (wml.xsd, sml.xsd,
dml-main.xsd) above that, so their validate.py broke inside the sandbox.

writeContainerFileBytes now splits payloads across execs into a .codebay-part file that the
last slice renames into place; the per-file guard rises to 16 MiB and any remaining skips are
logged as one summary line.
… files

Reverts the chunked-write approach. Anthropic's own claude.ai skills (manifest creatorType
"anthropic", or source "anthropic*" on manifests that predate creatorType) are no longer
copied into sandboxes: the sandbox's Claude Code syncs them itself for the same login, and
their ISO-IEC29500 schemas were what tripped the 90 KB exec cap.

User/org synced skills are still copied with a manifest trimmed to exactly those dirs; a
bucket with nothing left, or no readable manifest, is dropped along with its .bucket marker.
.last-complete-round and the sync's .staging/.trash dirs are never copied. Oversized-file
skips now log one summary line.
@khromov khromov changed the title fix(skills): chunk large skill files instead of skipping them fix(skills): don't copy Anthropic's synced skills into sandboxes Sep 27, 2026
Claude Code 2.1.283 lands a synced skill in its sanitized name (path-hostile
characters → _, trailing dots/spaces dropped) and never in its skillId, so
match on that, keep only manifest rows whose dir was copied, and read the
bucket marker only when it's a plain file.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant