Environment Health
Diagnose the user's Claude Code environment health. Outputs either an inline markdown
report or a self-contained interactive HTML dashboard. Covers 10 diagnostic areas — 6
graded scores against official thresholds (§4 splits into §4a at-rest and §4b post-compact;
§9 Skill Security Scan uses security baselines rather than docs-cited thresholds)
plus 5 observational areas (raw numbers, no tier).
Instructions
Input Parsing
Parse these arguments:
| Flag |
Values |
Default |
Meaning |
--format |
html | md |
html |
Output mode. html generates a full interactive dashboard (default — recommended). md produces an inline markdown report for non-browser contexts or chat pasting |
--lang |
ISO code (en, ko, fr, etc.) |
detected |
Report language. Falls back to detecting the user message language, then en |
--paste-context |
(flag) |
off |
Ask the user to paste their /context output and use it to correct the estimated startup load |
--use-instructions-loaded-hook |
(flag) |
off |
Guide the user through temporarily enabling the InstructionsLoaded hook for file-level ground-truth data, then offer to revert it |
Phase 1 — Data Collection
Determine the context window size. The scan subprocess cannot detect the active
session's window from process.env. Derive it from the active model ID:
- Model ID contains
[1m] (e.g. claude-opus-4-6[1m]) → 1000000
- Any other model ID →
200000
Pass this as --window-size=<N> so the scan records it and downstream formulas use
the correct denominator.
Run the scan:
node ${CLAUDE_PLUGIN_ROOT}/skills/context-health-visual/scripts/env-health-scan.js --window-size=<N>
The script writes a JSON blob to stdout with these sections:
scan_date, context_window_size, env_and_settings (normalized
enable_tool_search per mcp.md's 5-value table, agent_teams_enabled,
anthropic_base_url)
installed_plugins ({plugins, orphan_count, orphans}), disabled_plugins
installed_skills, installed_commands, local_skills (each entry carries
desc_chars, when_to_use_chars, combined_chars, disabled, user_invocable)
skill_bodies (at-rest 500-line flag + post-compact 5K-token flag)
hook_inventory (type counts, event collisions, llm_hooks — includes inline
hooks from plugin.json's hooks field: schema_issues and schema_issue_counts
for hook schema validation)
skill_security (scanned_count, findings with confidence levels, counts_by_severity,
counts_by_category — covers 6 pattern categories: prompt_injection, data_exfil,
destructive, hardcoded_credential, obfuscation, safety_override)
context_metrics (MCP server count + source scopes — includes plugin .mcp.json
files AND inline mcpServers from plugin.json)
plugin_components (bin / monitors / lsp_servers / output_styles /
channels per plugin + totals)
subagent_preloads (agents with skills: frontmatter that preload bodies at start)
plugin_options (per-plugin option keys only, values omitted — sensitive)
claude_md (walks cwd → filesystem root + ~/.claude/CLAUDE.md, plus enumerates
lazy-loaded nested files below cwd; respects claudeMdExcludes; each entry carries
scope, load_mode, compact_resilient)
rules, memory
Save to /tmp/env-health-<pid>/scan.json.
Optional ground truth refinement:
- If
--paste-context is set: use AskUserQuestion to ask for the /context output,
parse the reported always-loaded token counts, and override the estimates in the
report. Clear the estimate-caveat when doing so.
- If
--use-instructions-loaded-hook is set: walk the user through adding a temporary
command-type InstructionsLoaded hook to ~/.claude/settings.json that logs to
/tmp/env-health-<pid>/instructions-loaded.log. Instruct them to start a new Claude
Code session so the hook fires, then parse the log for exact per-file loading. Offer
to revert the hook after reading the log.
Phase 2 — Analysis
Read ${CLAUDE_PLUGIN_ROOT}/skills/context-health-visual/references/health-criteria.md
for the full threshold specification. Apply it as follows:
Compute the effective description budget:
effective_budget = env_and_settings.desc_budget_override
?? max(8000, floor(context_window_size * 0.01))
Graded areas (§3, §4a, §4b, §7, §8, §9): classify into
🟢 healthy / 🟡 attention / 🔴 critical using the rules in health-criteria.md. Every
threshold you apply must cite its docs source. (§9 Skill Security Scan is an exception —
graded on security baselines without docs-cited thresholds; see health-criteria.md §9.)
Observational areas (§1 Plugin Inventory, §2 Startup Context Budget aggregate,
§5 Trigger Collisions, §6 Hook Complexity, §10 Plugin Components): do NOT assign a
tier. Emit raw numbers and info-level notes only. Delegate individual component
grading in §2 to the owner area per the status-delegation table.
Trigger collisions (§5): delegate to the trigger-collision-inspector subagent.
Build the input inventory by concatenating every installed skill and command
description, one per line:
[plugin-name] skill-name: description text
Invoke the subagent via the Agent tool with subagent_type set to
trigger-collision-inspector. The subagent returns {total_descriptions_analyzed, collisions: [...]}. Surface the pairs verbatim — DUPLICATE / OVERLAP classification
is shown as reported, without aggregating into a tier (the prior 1-2 vs 3+ OVERLAP
thresholds had no official basis and were removed in favor of raw observation).
Do NOT implement Jaccard or pairwise comparison in the orchestrator or the scan
script. The subagent owns the entire comparison (direct adoption of Waza
inspector-context.md:113).
Top lever computation: rank possible actions by raw numeric impact (chars freed,
tokens saved) — not by severity — so observational areas can still surface a lever.
Examples:
- Adding
disable-model-invocation: true to the N skills with the largest
combined_chars → frees sum(combined_chars) (% of budget). Prefer skills users
invoke manually (/commit, /deploy, /release)
- Trimming entries over 1,536 chars → frees
combined_chars - 1536 per entry
- Moving body content of a 5K+ token SKILL.md to
references/ → removes it from both
at-rest and compact budgets
Pick the single lever with the largest projected savings and promote it to the
header + recommendations top card.
Phase 3 — Report Generation
Principle: HTML is the default because a dashboard renders the 10 diagnostic areas as KPI cards, bars, and collision heatmaps that markdown can't match. Only choose md when running in a non-browser context (cowork, headless CI), when the user asks for markdown explicitly, or when pasting into a chat thread is more useful than opening a file.
Mermaid generation force-loads Layer 0's semantic-tokens.md (tokens) / diagram-type-selection.md (type mapping) / diagram-density-rules.md (budget) / taste-gate.md (checklist), and is auto-validated by scripts/taste-gate.js.
Markdown mode (--format md):
Emit an inline markdown report with this structure:
# Environment Health — <scan_date>
**Graded:** N 🟢 / N 🟡 / N 🔴 (6 areas) · **Observational:** Plugin Inventory, Context Budget, Trigger Collisions, Hook Complexity, Plugin Components
**Top lever:** <one sentence>
**Estimated startup load:** ~N tokens (X% of <window> window) — *estimate, run `/context` for ground truth*
## §1 Plugin & Skill Inventory ℹ️
<raw tables, orphan/7-day-grace notes, plugin-option keys>
## §2 Startup Context Budget ℹ️
<component breakdown, delegation references>
## §3 Skill Description Obesity <status>
<numbers, truncated entries, disable-model-invocation candidates, subagent preload totals>
## §4 Skill Body Size <status>
<4a at-rest, 4b post-compact — report separately>
## §5 Trigger Collisions ℹ️
<subagent-returned DUPLICATE/OVERLAP pairs, verbatim, no tier>
## §6 Hook Complexity ℹ️
<type breakdown, event collisions, inline-vs-file sources>
## §7 MCP Overview <status>
<server count, ENABLE_TOOL_SEARCH 5-value table, effective mode, proxy fallback>
## §8 CLAUDE.md & Memory Health <status>
<file list with scope/load_mode/compact_resilient, MEMORY.md capacity, nested lazy-loaded totals>
## §9 Skill Security Scan <status>
<findings list with confidence; safe/likely_safe collapsed; hook schema_issues as info notes>
## §10 Plugin Components ℹ️
<bin / monitors / lsp_servers / output_styles / channels per plugin>
## Recommendations
<grouped by severity, top lever promoted>
Cite sources inline where a threshold fires. Keep it under 200 lines.
HTML mode (default, --format html):
Read ${CLAUDE_PLUGIN_ROOT}/skills/context-health-visual/references/section-structure.md
for the JSON schema. Then follow ${CLAUDE_PLUGIN_ROOT}/references/report-generation-workflow.md
with these parameters:
| Parameter |
Value |
{output-path} |
${CLAUDE_PLUGIN_DATA}/reports/<scan_date>-context-health-visual.html |
{template-name} |
context-health-visual.html |
{skill-prefix} |
env-health |
{expected-sections} |
10 |
{report-title} |
"Environment Health — <scan_date>" |
{aesthetic-hint} |
"Dashboard" |
{agent-prompt-data} |
The analyzed scan data, subagent collision results, computed tiers per area, top lever, and info notes. Pass the raw scan.json separately so the writer can reference exact numbers. |
The sections-data.json must set metadata.report_type = "context-health-visual" so
render-sections.js dispatches to the context-health-visual renderers (not the
plugin-visual ones).
Privacy guard (both modes): strip any raw file content before rendering. The
report emits counts, sizes, line numbers, and file paths — never CLAUDE.md body text,
MEMORY.md body text, API keys, or arbitrary file contents. Enforce this as a
pre-render pass: walk the sections-data.json tree and remove any field named body,
content, raw, text (excluding purposefully-set text fields in info_notes and
recommendation action strings, which contain only computed messages).
Cleanup
Remove the temp directory:
rm -rf /tmp/env-health-<pid>
If the user enabled the InstructionsLoaded hook, remind them to revert it now.
Gotchas
- Skill description budget is 1% of context window, not 2%. Earlier drafts used 2%
/ 16K chars. The official number (skills.md — "Skill descriptions are cut short") is
1% scaling, 8,000-char fallback. Use the effective budget formula:
SLASH_COMMAND_TOOL_CHAR_BUDGET ?? max(8000, floor(context_window * 0.01)).
- Skill description loading is a 3-layer model, not a single number. Per skills.md:
(A) each entry's combined
description + when_to_use is truncated at 1,536
characters — hard per-entry cap. (B) Total across all listing-included entries is
capped at effective_budget = SLASH_COMMAND_TOOL_CHAR_BUDGET ?? max(8000, floor(window × 0.01)). When the total exceeds budget, every entry is dynamically shortened
(keywords stripped). (C) Even below saturation, a few oversize entries can dominate.
Earlier drafts cited "250 chars" — that number does not appear anywhere in the docs.
§3 of the report evaluates all three axes separately. disable-model-invocation: true
removes a skill from the listing entirely per the frontmatter table, so it contributes
zero to the budget.
- 5K/25K skill body limit is POST-COMPACT, not at-rest. The 5,000-tokens-per-skill
/ 25,000-total limit applies only to re-injection after
/compact, and only for
skills that were invoked in the session. The at-rest recommendation is separate:
keep SKILL.md under 500 lines (skills.md tip). Report them as two distinct
sections (§4a and §4b) — do not conflate.
- Skill descriptions are NOT re-injected after compact. Only invoked skills
survive compaction. The always-loaded description budget vanishes after compact,
which changes what "always-loaded cost" means mid-session. Mention this caveat in
the report.
- Auto-compaction can enter a thrashing state and halt. Per how-claude-code-works.md:
"If a single file or tool output is so large that context refills immediately after
each summary, Claude Code stops auto-compacting after a few attempts and shows an
error." The scan cannot detect thrashing directly, but it can surface its
ingredients — large SKILL.md bodies, many rules without
paths:, and large
CLAUDE.md chains all amplify the risk. When all three co-occur, flag it in the
report and link users to
https://code.claude.com/docs/en/troubleshooting#auto-compaction-stops-with-a-thrashing-error.
- Trigger collision uses a subagent, not inline orchestrator logic. The
trigger-collision-inspector subagent owns the entire comparison (Waza-style). Do
NOT add Jaccard code in the scanner or do the comparison in the main orchestrator —
both would bloat main-session context and duplicate the subagent's job. If accuracy
becomes a problem, revise the subagent prompt before adding deterministic pre-filter
stages (YAGNI).
- Context budget is always-loaded only. Don't count deferred items in the
always-loaded budget unless
ENABLE_TOOL_SEARCH is auto (with pressure) or
false. Report deferred items separately so users see both views.
/context is ground truth, not the scan's estimate. The scan uses public
formulas but Claude Code's actual context accounting can drift per version. The
report must say "Estimated — run /context for ground truth" and optionally
accept pasted /context output via --paste-context.
InstructionsLoaded hook is the file-level ground truth. For users who want
exact per-file instruction loading data, recommend temporarily enabling the
InstructionsLoaded hook (hooks.md) to log which CLAUDE.md / rules / skills files
actually loaded. Offered as --use-instructions-loaded-hook.
- Skill body tokens are estimated (chars/4). Actual tokenization varies. Flag as
approximate.
- MEMORY.md path varies by project. The encoded path format can vary. The scan
script tries multiple paths — if none match, report "no memory file found" rather
than erroring.
- prompt/agent hooks cost tokens per event. Unlike command/http hooks,
prompt/agent hook types invoke an LLM call each time they fire. This is a per-event
runtime cost, not a startup cost.
ENABLE_TOOL_SEARCH changes the MCP cost model. The default (deferred) means
only tool names in context. auto may load schemas upfront if they fit in 10% of
context. false always loads them. The scan reads this env var and adjusts the
MCP budget line accordingly.
claudeMdExcludes and --add-dir CLAUDE.md loading. Respect
claudeMdExcludes (exclude matching paths from CLAUDE.md total) and
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 (include CLAUDE.md from --add-dir
paths). Both affect the always-loaded budget.
- Privacy. The scan reads CLAUDE.md, MEMORY.md, and settings files, but the
report must emit counts and sizes only — never file contents, API keys, or memory
body text. Enforce as a pre-render guard that strips raw content fields before
producing sections-data.json.
- Context window size is not self-detectable from the scan. The
node env-health-scan.js subprocess has no way to know the current session's window size
(200K vs 1M). Detect it from the active model ID (e.g. claude-opus-4-6[1m] →
- and pass via
--window-size=<N>. Without this, percentage-of-window
calculations default to 200K and will be wrong on 1M sessions.
- Shell-level env vars only — child process inheritance. The scan reads
SLASH_COMMAND_TOOL_CHAR_BUDGET, ENABLE_TOOL_SEARCH,
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD, CLAUDE_CODE_DISABLE_AUTO_MEMORY
from process.env. These are only visible to the Node subprocess if the user
exported them from their shell init. Env vars set only inside a Claude Code
session (not exported before CC launched) are invisible. Surface this caveat next
to any env-var-derived field.
- CLAUDE.md walk scope: cwd → filesystem root, plus
~/.claude/CLAUDE.md. Per
memory.md, Claude Code loads CLAUDE.md from every ancestor directory above cwd plus
~/.claude/CLAUDE.md. The docs do NOT specify a $HOME boundary. The scan walks up
to the filesystem root so that projects under /tmp, /var, or other non-home
locations still get a complete ancestor chain. Scope is reported as one of
project-root / ancestor / ancestor-local / nested / local-root / user.
- Nested CLAUDE.md files are lazy-loaded and NOT re-injected after
/compact.
Per memory.md, nested CLAUDE.md files (below cwd) load only when files in their
subtree are read. Unlike project-root ./CLAUDE.md / ./.claude/CLAUDE.md, they
don't survive compaction — only always-loaded project-root files are re-injected.
The scan reports load_mode and compact_resilient per file, and keeps
nested_lines / nested_bytes / nested_est_tokens separate from always-loaded
totals so the report doesn't exaggerate startup load.
- Observational areas never contribute to the tally. §1 and §2 emit raw numbers
and info-level notes only. They are counted separately from the graded tally. A
report showing
0 critical means zero critical among graded areas — observational
areas may still have info-level observations worth surfacing.
- Skill security scan uses heuristic confidence, not binary safe/unsafe. Every
finding is kept in the output — nothing is silently discarded. A
confidence field
adjusts how prominently findings are shown: safe / likely_safe findings are
collapsed by default (temp-path cleanup, loopback-only calls, frontmatter metadata
lines, and scanner self-references do not warrant loud alerts). Grade is driven only
by uncertain / suspicious findings. When multiple heuristics apply to one finding,
the most cautious level (lowest confidence) is used.
- Self-exclusion by frontmatter
name:, not by path. The scanner skips the skill
whose name: frontmatter field equals context-health-visual. Path-based exclusion
would break across install locations (cache vs local). Scope is this skill only —
other vision-powers skills (plugin-visual, doc-visual, fact-check, etc.) remain
in-scope so any legitimate security findings surface.
- Hook schema validation applies only to tool-gated events.
PreToolUse and
PostToolUse entries are flagged when they lack a matcher field. Other events
(SessionStart, UserPromptSubmit, Stop, etc.) don't use matchers — flagging
them would be noise. This scope is hard-coded in env-health-scan.js; update the
MATCHER_EVENTS set if future docs add more tool-gated event types.
Reference Files
references/health-criteria.md — Grading thresholds (cites every docs source) and
recommendation templates
references/section-structure.md — JSON schema for the 10-section HTML report
agents/trigger-collision-inspector.md — Subagent spec for trigger collision
detection (Waza-adapted)
scripts/env-health-scan.js — Data collection script
../../references/report-generation-workflow.md — Shared HTML generation pipeline
(render → assemble → validate → log → open)
../../references/design-system/semantic-tokens.md — Color and font semantic tokens
../../references/design-system/diagram-type-selection.md — 13-type selection guide
../../references/design-system/diagram-density-rules.md — Complexity budget per type
../../references/design-system/taste-gate.md — Pre-output quality checklist
1---2name: context-health-visual3description: Diagnose Claude Code environment health — context budget, description obesity, trigger collisions, hooks, MCP, plugins, CLAUDE.md, memory, and skill-security scan. Use when asked to audit the environment, check context budget, review plugins, or scan installed skills for risky patterns.4---5
6# Environment Health
7
8Diagnose the user's Claude Code environment health. Outputs either an inline markdown
9report or a self-contained interactive HTML dashboard. Covers 10 diagnostic areas — 6
10graded scores against official thresholds (§4 splits into §4a at-rest and §4b post-compact;
11§9 Skill Security Scan uses security baselines rather than docs-cited thresholds)
12plus 5 observational areas (raw numbers, no tier).
13
14## Instructions
15
16### Input Parsing
17
18Parse these arguments:
19
20| Flag | Values | Default | Meaning |
21|------|--------|---------|---------|
22| `--format` | `html` \| `md` | `html` | Output mode. `html` generates a full interactive dashboard (default — recommended). `md` produces an inline markdown report for non-browser contexts or chat pasting |
23| `--lang` | ISO code (`en`, `ko`, `fr`, etc.) | detected | Report language. Falls back to detecting the user message language, then `en` |
24| `--paste-context` | (flag) | off | Ask the user to paste their `/context` output and use it to correct the estimated startup load |
25| `--use-instructions-loaded-hook` | (flag) | off | Guide the user through temporarily enabling the `InstructionsLoaded` hook for file-level ground-truth data, then offer to revert it |
26
27### Phase 1 — Data Collection
28
29**Determine the context window size.** The scan subprocess cannot detect the active
30session's window from `process.env`. Derive it from the active model ID:
31
32- Model ID contains `[1m]` (e.g. `claude-opus-4-6[1m]`) → `1000000`
33- Any other model ID → `200000`
34
35Pass this as `--window-size=<N>` so the scan records it and downstream formulas use
36the correct denominator.
37
38**Run the scan:**
39
40```bash
41node ${CLAUDE_PLUGIN_ROOT}/skills/context-health-visual/scripts/env-health-scan.js --window-size=<N>
42```
43
44The script writes a JSON blob to stdout with these sections:
45
46- `scan_date`, `context_window_size`, `env_and_settings` (normalized
47 `enable_tool_search` per mcp.md's 5-value table, `agent_teams_enabled`,
48 `anthropic_base_url`)
49- `installed_plugins` (`{plugins, orphan_count, orphans}`), `disabled_plugins`
50- `installed_skills`, `installed_commands`, `local_skills` (each entry carries
51 `desc_chars`, `when_to_use_chars`, `combined_chars`, `disabled`, `user_invocable`)
52- `skill_bodies` (at-rest 500-line flag + post-compact 5K-token flag)
53- `hook_inventory` (type counts, event collisions, `llm_hooks` — includes inline
54 hooks from `plugin.json`'s `hooks` field: `schema_issues` and `schema_issue_counts`
55 for hook schema validation)
56- `skill_security` (scanned_count, findings with confidence levels, counts_by_severity,
57 counts_by_category — covers 6 pattern categories: prompt_injection, data_exfil,
58 destructive, hardcoded_credential, obfuscation, safety_override)
59- `context_metrics` (MCP server count + source scopes — includes plugin `.mcp.json`
60 files AND inline `mcpServers` from `plugin.json`)
61- `plugin_components` (`bin` / `monitors` / `lsp_servers` / `output_styles` /
62 `channels` per plugin + totals)
63- `subagent_preloads` (agents with `skills:` frontmatter that preload bodies at start)
64- `plugin_options` (per-plugin option **keys only**, values omitted — sensitive)
65- `claude_md` (walks cwd → filesystem root + `~/.claude/CLAUDE.md`, plus enumerates
66 lazy-loaded nested files below cwd; respects `claudeMdExcludes`; each entry carries
67 `scope`, `load_mode`, `compact_resilient`)
68- `rules`, `memory`
69
70Save to `/tmp/env-health-<pid>/scan.json`.
71
72**Optional ground truth refinement:**
73
74- If `--paste-context` is set: use `AskUserQuestion` to ask for the `/context` output,
75 parse the reported always-loaded token counts, and override the estimates in the
76 report. Clear the estimate-caveat when doing so.
77- If `--use-instructions-loaded-hook` is set: walk the user through adding a temporary
78 command-type `InstructionsLoaded` hook to `~/.claude/settings.json` that logs to
79 `/tmp/env-health-<pid>/instructions-loaded.log`. Instruct them to start a new Claude
80 Code session so the hook fires, then parse the log for exact per-file loading. Offer
81 to revert the hook after reading the log.
82
83### Phase 2 — Analysis
84
85Read `${CLAUDE_PLUGIN_ROOT}/skills/context-health-visual/references/health-criteria.md`
86for the full threshold specification. Apply it as follows:
87
88**Compute the effective description budget:**
89
90```
91effective_budget = env_and_settings.desc_budget_override
92 ?? max(8000, floor(context_window_size * 0.01))
93```
94
95**Graded areas** (§3, §4a, §4b, §7, §8, §9): classify into
96🟢 healthy / 🟡 attention / 🔴 critical using the rules in `health-criteria.md`. Every
97threshold you apply must cite its docs source. (§9 Skill Security Scan is an exception —
98graded on security baselines without docs-cited thresholds; see health-criteria.md §9.)
99
100**Observational areas** (§1 Plugin Inventory, §2 Startup Context Budget aggregate,
101§5 Trigger Collisions, §6 Hook Complexity, §10 Plugin Components): do NOT assign a
102tier. Emit raw numbers and info-level notes only. Delegate individual component
103grading in §2 to the owner area per the status-delegation table.
104
105**Trigger collisions (§5):** delegate to the `trigger-collision-inspector` subagent.
106Build the input inventory by concatenating every installed skill and command
107description, one per line:
108
109```
110[plugin-name] skill-name: description text
111```
112
113Invoke the subagent via the `Agent` tool with `subagent_type` set to
114`trigger-collision-inspector`. The subagent returns `{total_descriptions_analyzed,
115collisions: [...]}`. Surface the pairs verbatim — DUPLICATE / OVERLAP classification
116is shown as reported, without aggregating into a tier (the prior 1-2 vs 3+ OVERLAP
117thresholds had no official basis and were removed in favor of raw observation).
118
119Do NOT implement Jaccard or pairwise comparison in the orchestrator or the scan
120script. The subagent owns the entire comparison (direct adoption of Waza
121`inspector-context.md:113`).
122
123**Top lever computation:** rank possible actions by raw numeric impact (chars freed,
124tokens saved) — not by severity — so observational areas can still surface a lever.
125Examples:
126
127- Adding `disable-model-invocation: true` to the N skills with the largest
128 `combined_chars` → frees `sum(combined_chars)` (% of budget). Prefer skills users
129 invoke manually (`/commit`, `/deploy`, `/release`)
130- Trimming entries over 1,536 chars → frees `combined_chars - 1536` per entry
131- Moving body content of a 5K+ token SKILL.md to `references/` → removes it from both
132 at-rest and compact budgets
133
134Pick the single lever with the largest projected savings and promote it to the
135header + recommendations top card.
136
137### Phase 3 — Report Generation
138
139**Principle:** HTML is the default because a dashboard renders the 10 diagnostic areas as KPI cards, bars, and collision heatmaps that markdown can't match. Only choose `md` when running in a non-browser context (cowork, headless CI), when the user asks for markdown explicitly, or when pasting into a chat thread is more useful than opening a file.
140
141Mermaid generation force-loads Layer 0's semantic-tokens.md (tokens) / diagram-type-selection.md (type mapping) / diagram-density-rules.md (budget) / taste-gate.md (checklist), and is auto-validated by `scripts/taste-gate.js`.
142
143**Markdown mode (`--format md`):**
144
145Emit an inline markdown report with this structure:
146
147```
148# Environment Health — <scan_date>
149
150**Graded:** N 🟢 / N 🟡 / N 🔴 (6 areas) · **Observational:** Plugin Inventory, Context Budget, Trigger Collisions, Hook Complexity, Plugin Components
151
152**Top lever:** <one sentence>
153
154**Estimated startup load:** ~N tokens (X% of <window> window) — *estimate, run `/context` for ground truth*
155
156## §1 Plugin & Skill Inventory ℹ️
157<raw tables, orphan/7-day-grace notes, plugin-option keys>
158
159## §2 Startup Context Budget ℹ️
160<component breakdown, delegation references>
161
162## §3 Skill Description Obesity <status>
163<numbers, truncated entries, disable-model-invocation candidates, subagent preload totals>
164
165## §4 Skill Body Size <status>
166<4a at-rest, 4b post-compact — report separately>
167
168## §5 Trigger Collisions ℹ️
169<subagent-returned DUPLICATE/OVERLAP pairs, verbatim, no tier>
170
171## §6 Hook Complexity ℹ️
172<type breakdown, event collisions, inline-vs-file sources>
173
174## §7 MCP Overview <status>
175<server count, ENABLE_TOOL_SEARCH 5-value table, effective mode, proxy fallback>
176
177## §8 CLAUDE.md & Memory Health <status>
178<file list with scope/load_mode/compact_resilient, MEMORY.md capacity, nested lazy-loaded totals>
179
180## §9 Skill Security Scan <status>
181<findings list with confidence; safe/likely_safe collapsed; hook schema_issues as info notes>
182
183## §10 Plugin Components ℹ️
184<bin / monitors / lsp_servers / output_styles / channels per plugin>
185
186## Recommendations
187<grouped by severity, top lever promoted>
188```
189
190Cite sources inline where a threshold fires. Keep it under 200 lines.
191
192**HTML mode (default, `--format html`):**
193
194Read `${CLAUDE_PLUGIN_ROOT}/skills/context-health-visual/references/section-structure.md`
195for the JSON schema. Then follow `${CLAUDE_PLUGIN_ROOT}/references/report-generation-workflow.md`
196with these parameters:
197
198| Parameter | Value |
199|-----------|-------|
200| `{output-path}` | `${CLAUDE_PLUGIN_DATA}/reports/<scan_date>-context-health-visual.html` |
201| `{template-name}` | `context-health-visual.html` |
202| `{skill-prefix}` | `env-health` |
203| `{expected-sections}` | `10` |
204| `{report-title}` | `"Environment Health — <scan_date>"` |
205| `{aesthetic-hint}` | `"Dashboard"` |
206| `{agent-prompt-data}` | The analyzed scan data, subagent collision results, computed tiers per area, top lever, and info notes. Pass the raw `scan.json` separately so the writer can reference exact numbers. |
207
208The sections-data.json must set `metadata.report_type = "context-health-visual"` so
209`render-sections.js` dispatches to the context-health-visual renderers (not the
210plugin-visual ones).
211
212**Privacy guard (both modes):** strip any raw file content before rendering. The
213report emits counts, sizes, line numbers, and file paths — never CLAUDE.md body text,
214MEMORY.md body text, API keys, or arbitrary file contents. Enforce this as a
215pre-render pass: walk the sections-data.json tree and remove any field named `body`,
216`content`, `raw`, `text` (excluding purposefully-set `text` fields in `info_notes` and
217recommendation `action` strings, which contain only computed messages).
218
219### Cleanup
220
221Remove the temp directory:
222
223```bash
224rm -rf /tmp/env-health-<pid>
225```
226
227If the user enabled the `InstructionsLoaded` hook, remind them to revert it now.
228
229## Gotchas
230
231- **Skill description budget is 1% of context window, not 2%.** Earlier drafts used 2%
232 / 16K chars. The official number (skills.md — "Skill descriptions are cut short") is
233 1% scaling, 8,000-char fallback. Use the effective budget formula:
234 `SLASH_COMMAND_TOOL_CHAR_BUDGET ?? max(8000, floor(context_window * 0.01))`.
235- **Skill description loading is a 3-layer model, not a single number.** Per skills.md:
236 (A) each entry's combined `description` + `when_to_use` is truncated at **1,536
237 characters** — hard per-entry cap. (B) Total across all listing-included entries is
238 capped at `effective_budget = SLASH_COMMAND_TOOL_CHAR_BUDGET ?? max(8000, floor(window
239 × 0.01))`. When the total exceeds budget, every entry is **dynamically shortened**
240 (keywords stripped). (C) Even below saturation, a few oversize entries can dominate.
241 Earlier drafts cited "250 chars" — that number does not appear anywhere in the docs.
242 §3 of the report evaluates all three axes separately. `disable-model-invocation: true`
243 removes a skill from the listing entirely per the frontmatter table, so it contributes
244 zero to the budget.
245- **5K/25K skill body limit is POST-COMPACT, not at-rest.** The 5,000-tokens-per-skill
246 / 25,000-total limit applies only to re-injection after `/compact`, and only for
247 skills that were **invoked** in the session. The at-rest recommendation is separate:
248 keep `SKILL.md` under 500 lines (skills.md tip). Report them as two distinct
249 sections (§4a and §4b) — do not conflate.
250- **Skill descriptions are NOT re-injected after compact.** Only invoked skills
251 survive compaction. The always-loaded description budget vanishes after compact,
252 which changes what "always-loaded cost" means mid-session. Mention this caveat in
253 the report.
254- **Auto-compaction can enter a thrashing state and halt.** Per how-claude-code-works.md:
255 "If a single file or tool output is so large that context refills immediately after
256 each summary, Claude Code stops auto-compacting after a few attempts and shows an
257 error." The scan cannot detect thrashing directly, but it can surface its
258 ingredients — large SKILL.md bodies, many rules without `paths:`, and large
259 CLAUDE.md chains all amplify the risk. When all three co-occur, flag it in the
260 report and link users to
261 `https://code.claude.com/docs/en/troubleshooting#auto-compaction-stops-with-a-thrashing-error`.
262- **Trigger collision uses a subagent, not inline orchestrator logic.** The
263 `trigger-collision-inspector` subagent owns the entire comparison (Waza-style). Do
264 NOT add Jaccard code in the scanner or do the comparison in the main orchestrator —
265 both would bloat main-session context and duplicate the subagent's job. If accuracy
266 becomes a problem, revise the subagent prompt before adding deterministic pre-filter
267 stages (YAGNI).
268- **Context budget is always-loaded only.** Don't count deferred items in the
269 always-loaded budget unless `ENABLE_TOOL_SEARCH` is `auto` (with pressure) or
270 `false`. Report deferred items separately so users see both views.
271- **`/context` is ground truth, not the scan's estimate.** The scan uses public
272 formulas but Claude Code's actual context accounting can drift per version. The
273 report must say _"Estimated — run `/context` for ground truth"_ and optionally
274 accept pasted `/context` output via `--paste-context`.
275- **`InstructionsLoaded` hook is the file-level ground truth.** For users who want
276 exact per-file instruction loading data, recommend temporarily enabling the
277 `InstructionsLoaded` hook (hooks.md) to log which CLAUDE.md / rules / skills files
278 actually loaded. Offered as `--use-instructions-loaded-hook`.
279- **Skill body tokens are estimated (chars/4).** Actual tokenization varies. Flag as
280 approximate.
281- **MEMORY.md path varies by project.** The encoded path format can vary. The scan
282 script tries multiple paths — if none match, report "no memory file found" rather
283 than erroring.
284- **prompt/agent hooks cost tokens per event.** Unlike command/http hooks,
285 prompt/agent hook types invoke an LLM call each time they fire. This is a per-event
286 runtime cost, not a startup cost.
287- **`ENABLE_TOOL_SEARCH` changes the MCP cost model.** The default (`deferred`) means
288 only tool names in context. `auto` may load schemas upfront if they fit in 10% of
289 context. `false` always loads them. The scan reads this env var and adjusts the
290 MCP budget line accordingly.
291- **`claudeMdExcludes` and `--add-dir` CLAUDE.md loading.** Respect
292 `claudeMdExcludes` (exclude matching paths from CLAUDE.md total) and
293 `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1` (include CLAUDE.md from `--add-dir`
294 paths). Both affect the always-loaded budget.
295- **Privacy.** The scan reads CLAUDE.md, MEMORY.md, and settings files, but the
296 report must emit counts and sizes only — never file contents, API keys, or memory
297 body text. Enforce as a pre-render guard that strips raw content fields before
298 producing sections-data.json.
299- **Context window size is not self-detectable from the scan.** The `node
300 env-health-scan.js` subprocess has no way to know the current session's window size
301 (200K vs 1M). Detect it from the active model ID (e.g. `claude-opus-4-6[1m]` →
302 1000000) and pass via `--window-size=<N>`. Without this, percentage-of-window
303 calculations default to 200K and will be wrong on 1M sessions.
304- **Shell-level env vars only — child process inheritance.** The scan reads
305 `SLASH_COMMAND_TOOL_CHAR_BUDGET`, `ENABLE_TOOL_SEARCH`,
306 `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD`, `CLAUDE_CODE_DISABLE_AUTO_MEMORY`
307 from `process.env`. These are only visible to the Node subprocess if the user
308 `export`ed them from their shell init. Env vars set only inside a Claude Code
309 session (not exported before CC launched) are invisible. Surface this caveat next
310 to any env-var-derived field.
311- **CLAUDE.md walk scope: cwd → filesystem root, plus `~/.claude/CLAUDE.md`.** Per
312 memory.md, Claude Code loads CLAUDE.md from every ancestor directory above cwd plus
313 `~/.claude/CLAUDE.md`. The docs do NOT specify a `$HOME` boundary. The scan walks up
314 to the filesystem root so that projects under `/tmp`, `/var`, or other non-home
315 locations still get a complete ancestor chain. Scope is reported as one of
316 `project-root` / `ancestor` / `ancestor-local` / `nested` / `local-root` / `user`.
317- **Nested CLAUDE.md files are lazy-loaded and NOT re-injected after `/compact`.**
318 Per memory.md, nested CLAUDE.md files (below cwd) load only when files in their
319 subtree are read. Unlike project-root `./CLAUDE.md` / `./.claude/CLAUDE.md`, they
320 don't survive compaction — only always-loaded project-root files are re-injected.
321 The scan reports `load_mode` and `compact_resilient` per file, and keeps
322 `nested_lines` / `nested_bytes` / `nested_est_tokens` separate from always-loaded
323 totals so the report doesn't exaggerate startup load.
324- **Observational areas never contribute to the tally.** §1 and §2 emit raw numbers
325 and info-level notes only. They are counted separately from the graded tally. A
326 report showing `0 critical` means zero critical among graded areas — observational
327 areas may still have info-level observations worth surfacing.
328- **Skill security scan uses heuristic confidence, not binary safe/unsafe.** Every
329 finding is kept in the output — nothing is silently discarded. A `confidence` field
330 adjusts how prominently findings are shown: `safe` / `likely_safe` findings are
331 collapsed by default (temp-path cleanup, loopback-only calls, frontmatter metadata
332 lines, and scanner self-references do not warrant loud alerts). Grade is driven only
333 by `uncertain` / `suspicious` findings. When multiple heuristics apply to one finding,
334 the most cautious level (lowest confidence) is used.
335- **Self-exclusion by frontmatter `name:`, not by path.** The scanner skips the skill
336 whose `name:` frontmatter field equals `context-health-visual`. Path-based exclusion
337 would break across install locations (cache vs local). Scope is this skill only —
338 other vision-powers skills (plugin-visual, doc-visual, fact-check, etc.) remain
339 in-scope so any legitimate security findings surface.
340- **Hook schema validation applies only to tool-gated events.** `PreToolUse` and
341 `PostToolUse` entries are flagged when they lack a `matcher` field. Other events
342 (`SessionStart`, `UserPromptSubmit`, `Stop`, etc.) don't use matchers — flagging
343 them would be noise. This scope is hard-coded in `env-health-scan.js`; update the
344 `MATCHER_EVENTS` set if future docs add more tool-gated event types.
345
346## Reference Files
347
348- `references/health-criteria.md` — Grading thresholds (cites every docs source) and
349 recommendation templates
350- `references/section-structure.md` — JSON schema for the 10-section HTML report
351- `agents/trigger-collision-inspector.md` — Subagent spec for trigger collision
352 detection (Waza-adapted)
353- `scripts/env-health-scan.js` — Data collection script
354- `../../references/report-generation-workflow.md` — Shared HTML generation pipeline
355 (render → assemble → validate → log → open)
356- `../../references/design-system/semantic-tokens.md` — Color and font semantic tokens
357- `../../references/design-system/diagram-type-selection.md` — 13-type selection guide
358- `../../references/design-system/diagram-density-rules.md` — Complexity budget per type
359- `../../references/design-system/taste-gate.md` — Pre-output quality checklist