Claude Code silently truncates files loaded internally at ~28KB (28,672 bytes):
CLAUDE.md— loaded at session start; truncated silently if >~30KBcommands/files — loaded when slash command invoked; truncated silently if >~30KB- Skill tool output —
/invokepath truncates at ~30KB; PostToolUse hooks bypassed
Content loss is silent. No warning. Model sees partial content and infers the rest.
When hook output or tool output exceeds ~20KB, Claude Code truncates to a 2KB preview and saves the rest to a temp file — but the model often ignores the saved file.
Solution: Replace the full content with a READ directive that forces the model to use the Read tool.
apply_skill_limiter() {
local skill_name="$1"
local max_bytes="${2:-20000}"
if [[ ${#SKILL_CONTENT} -gt $max_bytes ]]; then
local tmpfile="/tmp/skill_content_${skill_name}.md"
printf '%s' "$SKILL_CONTENT" > "$tmpfile"
SKILL_CONTENT="READ THE FILE NOW. STOP. DO NOT PROCEED.
Use the Read tool on: $tmpfile
READ THAT FILE COMPLETELY BEFORE TAKING ANY ACTION.
[This message repeated to dominate the truncated preview]
READ THE FILE. READ $tmpfile NOW.
READ THE FILE. READ $tmpfile NOW."
fi
}Verified working in: skill_selector.sh, userprompt_archived_skill_inject.sh,
precompact_state_snapshot.sh
Prevents CLAUDE.md from being written past the truncation threshold.
See pre_claude_md_size_guard.sh in this Gist.
Fires on Write|Edit to CLAUDE.md:
- >28KB (28,672 bytes) → BLOCK with overflow instructions
- >22KB (22,528 bytes) → WARN with current/projected sizes
post_claude_md_size_audit.sh — fires after any Write|Edit to CLAUDE.md.
If actual size >28KB, emits a READ directive to the overflow guidance file.
Checks CLAUDE.md size at session start. Flags if already oversized.
Do NOT batch jq calls with @tsv for large strings:
# WRONG — read only reads one line; large new_string silently truncated → DELTA=0 → guard bypassed
read -r TOOL FILE OLD NEW < <(echo "$HOOK_INPUT" | jq -r '[.tool_name,.tool_input.old_string,.tool_input.new_string] | @tsv')
# CORRECT — separate calls preserve full multiline content
TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // empty')
OLD_STRING=$(echo "$HOOK_INPUT" | jq -r '.tool_input.old_string // empty')
NEW_STRING=$(echo "$HOOK_INPUT" | jq -r '.tool_input.new_string // empty')Caught by functional test: showed continue: true for a 29KB edit when it should be continue: false.
For commands/ files that are inherently large (workflow specs, etc.).
Pattern:
~/.claude/commands/my-command.md ← stub (<2KB, always loads fully)
↓ points to ↓
~/.claude/skills-lazy-loaded/my-command/SKILL.md ← full content (read via Read tool)
Stub template:
# /my-command
[2-3 sentence description]
---
## ⚠️ MANDATORY: Read Full Workflow Before Taking Any Action
This command's workflow specification is too large for inline loading (~NNkb).
The full spec is in the SKILL.md file. You MUST read it before proceeding.
cat ~/.claude/skills-lazy-loaded/my-command/SKILL.md
READ THAT FILE NOW. DO NOT take any action until you have read the complete spec.SKILL.md requirements:
- YAML frontmatter (
name:,description:with trigger phrases) - Register via
register_skill.py --force --skip-skill-router
Now automatic: The workflow-skill-creation skill includes a mandatory Phase 5a-GATE:
after writing SKILL.md, if >28KB (28,672 bytes) → stub pattern applied automatically.
| Path | Problem | Workaround |
|---|---|---|
Skill tool /invoke |
Claude Code internal, zero hook access | Use commands/ stubs instead |
| PostToolUse on truncation | Hooks bypassed when output is truncated | Fix 1 (poison-the-preview) |
| CLAUDE.md at session start | Already loaded before any hook fires | Fix 2 prevents it getting large |