SHEET 01 · AI AGENTS
What Claude Code Keeps of Your Skills After Compaction
A 5,000-token snapshot, a 25,000-token shared budget, and a skill index that never comes back. The mechanics, the failure modes, and the patterns that hold up in long sessions.
PLATE · 62s explainer · Captioned, music only
Contents
- How a skill sits in context
- What compaction does
- The skill re-attachment rule
- Five ways a skill degrades after compaction
- Why printing the skill from a hook is the wrong fix
- Patterns that hold up
- 1. Write skills that survive truncation
- 2. A hook that tells Claude to re-invoke, not one that pastes text
- 3. Re-invoke whatever was actually in use
- 4. Put the index back
- 5. Shape the compaction itself
- Skills are guidance. Guardrails live elsewhere.
- Running this across teams
- What is still undocumented
- The short version
- References
- The official guidance for the first two is short: if Claude stops following a skill partway through a session, invoke it again.
- Printing text from a hook is useful for a short rules card. It isn’t a skill reload.
- The split to aim for: skills carry judgement and procedure, hooks and permissions carry invariants. A skill that says “never drop a column without a migration plan” is useful guidance. A PreToolUse hook that rejects DROP COLUMN in an unreviewed migration file is the control.
Three hours into a session, Claude Code is midway through a database migration. Early on you invoked a db-migration-review skill that requires every schema change to ship with a rollback script. For the first forty minutes, every change came with one. Then auto-compaction ran. The next migration arrives without a rollback, and nothing in the terminal says why.
The skill wasn’t deleted. Part of it was kept, part was cut, and the part that was kept no longer has the weight it had when it was the most recent instruction in the conversation. If you build skills for other people to use, you need to know exactly which part is which.
Scroll sideways to see the full diagram
How a skill sits in context
A skill is a directory with a SKILL.md file and optional supporting files. At session start, Claude Code loads a listing: one line per available skill, built from the description and when_to_use frontmatter. Each combined entry is truncated at 1,536 characters. Skills marked disable-model-invocation: true stay out of the listing entirely until someone types /name.
When a skill is invoked, by Claude through the Skill tool or by you with /name, Claude Code renders SKILL.md and appends it to the conversation as a single message. Several properties follow from that:
- The file is read once. Claude Code does not re-read SKILL.md on later turns. Edits you make on disk mid-session have no effect until the skill is invoked again.
- Re-invoking is deduplicated by content. If the rendered output is identical to what is already in context, Claude Code adds a short “already loaded” note. If arguments or dynamic output changed, the full body is appended again.
- !command blocks are frozen at render time. Lines like !`git diff HEAD` run once before the content reaches the model, and their output becomes plain text inside the message. That text doesn't refresh.
- allowed-tools is scoped to one turn. The permission grant applies during the turn that invoked the skill and clears when you send your next message. The instructions stay; the pre-approvals don't.
The skill’s text lives in conversation history. Compaction rewrites conversation history. Everything that follows comes from that.
What compaction does
When context approaches its limit, or when you run /compact, Claude Code replaces the conversation with a structured summary. According to the context window documentation, the summary keeps your requests and intent, key technical concepts, files examined or modified with important snippets, errors and their fixes, pending tasks, and current work. Full tool outputs and intermediate reasoning are discarded.
Content that lives outside the message history is then reloaded:
- System prompt and output style still apply.
- Project-root CLAUDE.md, unscoped rules and auto memory are re-injected from disk.
- A fresh git status snapshot is taken.
- A plan written in plan mode is re-injected from disk.
- Up to five recently read or edited files are re-read, most recently modified first. Any file over 5,000 tokens comes back as a path reference only.
- SessionStart hooks that match the compact source run, and their output is added.
Content that lived inside message history is handled case by case:
- Path-scoped rules (paths: frontmatter) and nested CLAUDE.md files are summarised away. They reload only when Claude reads a matching file again.
- Context added earlier by hooks is summarised with everything else.
- Invoked skills get special treatment, described next.
- The skill listing is not re-injected.
The skill re-attachment rule
The skills documentation states the rule precisely. After the summary, Claude Code re-attaches the most recent invocation of each skill, keeping the first 5,000 tokens of each. All re-attached skills share a combined budget of 25,000 tokens, filled starting from the most recently invoked skill. Older skills can be dropped entirely.
Three details matter in practice.
Truncation keeps the head. Whatever sits beyond roughly the 5,000th token of the rendered skill is gone. A skill that puts its workflow first and its constraints in a closing “Important rules” section loses the constraints first.
“Under 500 lines” is not the same as “under 5,000 tokens.” The docs recommend keeping SKILL.md under 500 lines. Markdown prose runs about 4 characters per token, so a 500-line skill at an average of 60 characters per line is around 30,000 characters, or about 7,500 tokens. That fits the authoring guideline and still exceeds the compaction cap by half. Rendered !command output counts too: a skill that injects a large git diff can blow its own budget before you write a line of instructions.
Eviction is by recency, not importance. If a session invoked a 4,000-token governance skill early and then five 5,000-token productivity skills later, the governance skill is the one that falls off. The budget has no concept of priority.
Five ways a skill degrades after compaction
- Truncation. The tail of a long skill disappears, cutting at the token boundary rather than a section boundary.
- Eviction. Older skills disappear completely once the 25,000-token budget is spent.
- Routing blindness. Because the listing isn’t re-injected, Claude loses the trigger descriptions for skills that were never invoked. In sessions with many installed skills, requests that should route to a skill get handled with raw tools instead. This is tracked in anthropics/claude-code#82017, which reports the problem with around 85 registered skills.
- Stale dynamic context. Re-attached content carries the original !command output. A skill that injected the branch state or a config snapshot now carries data from hours ago, with nothing marking it as old.
- Loss of weight. This one is community-reported rather than documented. In anthropics/claude-code#95745, a user describes CLAUDE.md rules present in context after /compact but no longer followed, while an imperative SessionStart hook in the same context was honoured. The likely explanation is structural: in a fresh session, instructions frame the task; after compaction, the summary becomes the frame and re-injected instructions arrive as attachments behind it. Being in context is not the same as being followed.
The official guidance for the first two is short: if Claude stops following a skill partway through a session, invoke it again.
Why printing the skill from a hook is the wrong fix
The obvious workaround is a SessionStart hook with the compact matcher that runs cat on SKILL.md. Plain stdout from a SessionStart hook is added to context, so this looks like a full reload.
It isn’t. The hooks reference caps hook output at 10,000 characters per field (plain stdout, additionalContext, systemMessage and initialUserMessage are measured separately). Above the cap, Claude Code writes the output to a file and injects the path plus a preview of up to 2,000 characters. Claude can read the file but isn't told to.
10,000 characters is about 2,500 tokens. The built-in re-attachment already keeps 5,000. For any skill large enough to be truncated by compaction, a cat hook restores less than the platform already does, and it does so as plain text: frontmatter, $ARGUMENTS substitution, !command execution and allowed-tools are all skipped.
Printing text from a hook is useful for a short rules card. It isn’t a skill reload.
Patterns that hold up
1. Write skills that survive truncation
This is the cheapest control and the one with the widest effect.
- Put non-negotiable rules in the first screen of SKILL.md, ahead of the workflow.
- Budget the rendered core at around 4,000 tokens to leave headroom under the 5,000 cap, including any !command output.
- Move reference material into supporting files and link them from SKILL.md with a sentence on when to read each. The docs call this progressive disclosure; it also means the material that matters most is the part that survives.
- Phrase guidance as standing rules (“Every migration includes a rollback script”) rather than one-off steps (“Next, write the rollback”). The docs recommend this for persistence across turns, and it reads better as a compressed fragment.
- Lead the description with the main trigger. The listing truncates at 1,536 characters, and the listing is what you lose first.
- Push deterministic steps into scripts. Scripts in scripts/ are executed, not loaded, so they cost no context and can't be truncated.
- For heavy, self-contained work, use context: fork. The skill runs in a subagent with its own context, and nothing it loads competes for the main session's 25,000-token budget.
A layout that applies the above:
---
name: db-migration-review
description: Review and author database schema migrations. Use when creating, editing, or reviewing files under migrations/ or any DDL change.
---
## Rules (kept at the top so they survive truncation)
1. Every migration ships with a tested rollback script.
2. No destructive DDL (DROP, column type narrowing) without an explicit expand/contract plan.
3. Run scripts/lint_migration.py before proposing a change; do not hand-check.
## Workflow
...
## References (read when needed)
- Expand/contract patterns: [patterns.md](patterns.md)
- Lock and timeout guidance per engine: [engines.md](engines.md)2. A hook that tells Claude to re-invoke, not one that pastes text
The SessionStart compact matcher is the right trigger. The payload should be an instruction to call the Skill tool, because a real invocation re-renders from disk: full body, fresh !command output, and re-applied allowed-tools.
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Context was just compacted. Before any other action, re-invoke the db-migration-review skill with the Skill tool, then continue the current task under its rules.'",
"timeout": 10
}
]
}
]
}
}The imperative phrasing is deliberate. It matches the pattern reported in #95745 as being honoured after compaction when static policy text was not. The trigger is deterministic; whether Claude complies is still the model’s decision, so test it.
3. Re-invoke whatever was actually in use
Hard-coding skill names works for one critical skill. For sessions that use several, the hook can work out which skills were active. Every hook receives transcript_path in its input, and the transcript is JSONL. The script below collects skills invoked through the Skill tool and through typed slash commands, keeps them in order of last use, and asks Claude to re-invoke the most recent few.
#!/usr/bin/env python3
# SessionStart(compact) hook: re-invoke skills used earlier in the session.
# The transcript schema is not a documented contract; re-test on upgrades.
import json, re, sys
MAX_SKILLS = 3
ALLOWLIST = None # e.g. {"db-migration-review", "secrets-guard"}
data = json.load(sys.stdin)
order = []
def note(name):
name = name.strip().lstrip("/")
if not name or (ALLOWLIST and name not in ALLOWLIST):
return
if name in order:
order.remove(name)
order.append(name)
try:
with open(data.get("transcript_path"), encoding="utf-8") as f:
for line in f:
try:
content = (json.loads(line).get("message") or {}).get("content")
except ValueError:
continue
if isinstance(content, list):
for b in content:
if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("name") == "Skill":
note(str((b.get("input") or {}).get("skill", "")))
for m in re.finditer(r"<command-name>/?([\w:.-]+)</command-name>", json.dumps(content or "")):
note(m.group(1))
except (OSError, TypeError):
sys.exit(0) # never fail the session over a reminder
if order:
names = ", ".join(order[-MAX_SKILLS:])
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": f"Context was just compacted. Skills active before compaction: {names}. "
"Re-invoke each with the Skill tool, most recent last, then resume the task."
}}))Register it with "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/reinvoke-skills.py\"" under the same compact matcher. MAX_SKILLS keeps re-invocations within roughly the budget the platform would have used anyway. The allowlist lets you restrict automatic re-invocation to skills that need it, so a session doesn't reload every helper it touched. The docs note that the transcript is written asynchronously and can lag the in-memory conversation; that doesn't matter here, because the skills of interest were invoked long before compaction.
4. Put the index back
Re-invoking restores bodies. It doesn’t restore routing for skills that were installed but never used. For large skill catalogues, keep a short SKILL_CATALOG.md (skill name plus the one trigger phrase that should route to it, comfortably under 10,000 characters) and print it from the same compact hook. Issue #82017 also reports /reload-skills and /reload-plugins as manual workarounds; confirm they exist in your version before documenting them for a team.
5. Shape the compaction itself
- Focused compaction. /compact keep the migration rules and the decisions on table partitioning tells the summariser what to keep instead of leaving it to guess.
- Earlier compaction. /autocompact <tokens> moves the threshold so compaction happens before quality has already started to slip.
- Partial compaction. /rewind, pick a message, and choose "Summarize from here" or "Summarize up to here" to compress only the part of the session that no longer matters.
- PreCompact. The hook receives trigger, custom_instructions and compact_summary, and can write task state to a file that the SessionStart compact hook reads back. It can also return "decision": "block" to stop compaction, which is a reasonable gate for a session in the middle of a change that must not lose state.
- PostCompact. It receives tokens_before and tokens_after but can't block or inject context. It is the right place for telemetry.
{
"hooks": {
"PostCompact": [
{
"matcher": "manual|auto",
"hooks": [
{
"type": "command",
"command": "jq -c '{ts: now|todate, trigger, tokens_before, tokens_after}' >> \"$HOME/.claude/compaction.log\"",
"timeout": 10
}
]
}
]
}
}Skills are guidance. Guardrails live elsewhere.
All of the above makes skills more durable. None of it makes them enforceable. A skill is text the model is asked to follow, and the evidence in #95745 shows that text in context can lose weight after compaction even when it is present in full.
Anything that must never happen belongs in a layer that doesn’t depend on the model’s attention:
- Permission deny rules for commands and paths that are off-limits.
- PreToolUse hooks that inspect a tool call and reject it, for example blocking a git push to a protected branch or a write to a production config path.
- UserPromptExpansion hooks when you need to gate direct /skill invocation itself, a path PreToolUse doesn't see because typed slash commands bypass the Skill tool.
The split to aim for: skills carry judgement and procedure, hooks and permissions carry invariants. A skill that says “never drop a column without a migration plan” is useful guidance. A PreToolUse hook that rejects DROP COLUMN in an unreviewed migration file is the control.
Running this across teams
Once skills are shared, compaction behaviour becomes a platform concern rather than an individual one.
- Lint skills in CI. Count tokens in the rendered SKILL.md, including a dry run of !command blocks where feasible, and fail above the threshold you've set. Check that a rules section appears in the opening lines.
- Tier by consequence. Skills that govern irreversible or regulated actions get a re-invoke hook and a backing deterministic control. Productivity skills rely on good structure and the built-in re-attachment. Ad hoc skills get nothing extra.
- Ship hooks inside plugins. A re-invoke hook in one engineer’s ~/.claude/settings.json is a personal habit. Packaged in the plugin that carries the skill, referenced through CLAUDE_PLUGIN_ROOT, it is versioned behaviour every user gets.
- Watch skill load per session. Five heavy skills already fill the 25,000-token budget. Beyond that, eviction is silent. Prefer context: fork for skills that don't need the main conversation.
- Measure. PostCompact logs show how often real sessions compact and how much they shed. Correlate that with defects or review findings before investing in more elaborate restoration.
- Keep a regression test. Invoke the skill, force /compact, issue a task that should trigger a rule from the bottom of the file, and check adherence. Run it on every Claude Code upgrade, because the 5,000, 25,000 and 10,000 figures are implementation choices.
What is still undocumented
- Whether re-attached skill content re-runs !command blocks. Treat it as a frozen snapshot and re-invoke for fresh output.
- The transcript JSONL format. It works for parsing today but is not a stable contract.
- How strongly compaction weakens adherence to instructions that are present. The evidence so far is anecdotal and points in one direction.
- Whether the skill listing will be re-injected in future versions. Issue #82017 is open.
The short version
Compaction keeps the head of each recently used skill, up to 5,000 tokens each and 25,000 in total, and drops the index of everything else. Write skills so the head is what matters. Restore with a hook that asks for a real re-invocation, not one that pastes text past a 10,000-character cap. Put the rules that cannot bend into permissions and PreToolUse hooks, where compaction can't reach them.
References
- Claude Code docs: Extend Claude with skills
- Claude Code docs: Explore the context window
- Claude Code docs: Hooks reference
- Claude Code docs: Automate actions with hooks
- GitHub: anthropics/claude-code#82017, compaction-continued sessions lose the skill inventory
- GitHub: anthropics/claude-code#95745, CLAUDE.md present after /compact but no longer followed