"Verify the context-guard plugin's wiring on this machine: jq, node, the installed statusline shim, statusline wiring (including legacy version-pinned plugin-cache paths), and live-session snapshot freshness. Print the exact statusline edit for the operator, and install the shim plus seed ~/.claude/context-guard/zones.json from the shipped defaults. Use when: 'set up context-guard', 'is the context tee working', 'wire the context statusline', a consumer reports zone unknown in a live session, or after a plugin update. Actions: check (read-only; never edits settings), apply (writes ONLY inside ~/.claude/context-guard/, the shim and zones.json, on explicit request)."
Resources
2Install
npx skillscat add melodic-software/claude-code-plugins/plugins-context-guard-skills-setup Install via the SkillsCat registry.
Pre-computed context
Four of check's read-only probes run at load time. Read the values below; do not re-issue them.
jq (a path = present, absent = missing): !command -v jq 2>/dev/null || echo "absent"node (a path = present, absent = missing): !command -v node 2>/dev/null || echo "absent"
Installed shim (first path) against the shipped source (second path), with both # shim-revision: markers; a No such file line names the side that is missing: !{ grep -H "^# shim-revision:" "$HOME/.claude/context-guard/bin/statusline-shim.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh" 2>&1; cmp -s "$HOME/.claude/context-guard/bin/statusline-shim.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh" && echo "cmp: identical" || echo "cmp: not identical, or a file is absent"; }zones.json contents, capped at 40 lines, or one token distinguishing an absent file from an unreadable one: !{ if [ -e "$HOME/.claude/context-guard/zones.json" ]; then cat "$HOME/.claude/context-guard/zones.json" 2>&1 || echo "(present but unreadable)"; else echo "(absent)"; fi; } | head -40
Purpose
Narrow-write setup, because this plugin's surface splits in two. The statusline wiring lives in the
user's own settings.json and the jq prerequisite is a system tool: neither is something
plugin setup may write, so check inspects, reports PASS/FAIL/INFO with one remediation line per
FAIL, and prints the exact statusline edit for the operator to apply by hand. But this plugin
also owns its operator-home directory ~/.claude/context-guard/, the machine file zones.json,
whose schema it defines and whose values the operator may edit, and the statusline shimbin/statusline-shim.sh, the durable path the operator's wiring names, and those owned writable
artifacts are what oblige an apply. apply is scoped to that directory and touches nothing else.
Why the shim exists (the durable-wiring rule). ${CLAUDE_PLUGIN_ROOT} is version-pinned and
changes on every plugin update, and the old version directory is pruned about 14 days later
(plugins reference, "Plugin cache and file access"). A statusline wired straight to<plugin-root>/scripts/statusline-tee.sh therefore stops teeing at the next version bump and, once
the old directory is pruned, bash <missing-path> exits 127 and takes the operator's whole
statusline down with it. So the operator wires the shim, never the tee: the shim lives at a
path that never changes, resolves the newest installed tee at run time, and degrades to running the
wrapped command alone when no tee is installed. Read${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh for its actual resolution rule rather than
reciting this paragraph.
The scripts are the source of truth for their own behavior. Read${CLAUDE_PLUGIN_ROOT}/scripts/statusline-tee.sh and${CLAUDE_PLUGIN_ROOT}/scripts/context-zone.sh first; probe what they actually do rather than
reciting this file. The consumer-facing constants (snapshot path pattern, staleness rule, default
zone bands, zones.json shape) are owned by${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.md.
check (read-only)
jq. Read the pre-computedjqvalue. FAIL when it isabsent: without jq the wrapper
cannot tee (it stays transparent and shows a visible notice), the standalone statusline
degrades, and the zone resolver printsunknown. Remediation: install jq
(https://jqlang.org/download/).node. Read the pre-computednodevalue. FAIL when it isabsent: every hook row in${CLAUDE_PLUGIN_ROOT}/hooks/hooks.jsonrunsnode hooks/exec-bash.mjs <script>, and Claude
Code's native binary neither ships nor uses Node
(https://code.claude.com/docs/en/setup), so withoutnodeonPATHthe injection, the gate
and the PostCompact marker do not launch and are not enforced. Report that a hook that fails to
launch is non-blocking, so nothing else says so. The statusline tee and shim do not usenode.
Remediation: install Node.js (https://nodejs.org/en/download) and restart Claude Code.
The Node claim was verified 2026-09-29 against the setup page above; recheck when a Claude Code
release note says the native binary bundles Node or runs hooks without it, or when that page
stops saying the native binary needs no Node.Installed shim state, the shim is the wiring target, so check it before the wiring. The
pre-computed shim value compares~/.claude/context-guard/bin/statusline-shim.sh(the durable
shim copy) against${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh(the shipped source) and
carries the# shim-revision:marker of each file that exists. Classify it per
reference/legacy-statusline-detect.md "Installed shim
state", shared with the sibling guard plugin and synced byte-identical. An absent shipped
source takes that reference's own branch and ends the comparison; never readcmp: not identicalas drift when the shipped path is the missing one.Statusline wiring state. Read (never write) every settings scope that can carry a
statusLine(user~/.claude/settings.json, project.claude/settings.json, local.claude/settings.local.json, and managed settings, wherestatusLineis also a valid key)
and determine which one owns the effective command (the most specific scope wins among the
three non-managed scopes; a managed value outranks all of them). All wiring states below are
evaluated against that effective command. The printed edit in step 8 targets that scope's
file except when the owning scope is managed: that file is administrator-controlled, the
operator running this skill generally cannot change it, and no lower-scope edit can override
it. In that case name the managed source, say the operator cannot change it from here, and
route to the policy administrator. Do not print an operator edit for the managed file.
Wiring the user file while a project-levelstatusLineshadows it would apply cleanly and
never run; when a non-managed shadow exists, say so explicitly and print the edit for the
shadowing file (or note that removing the override is the alternative). Distinguish four
states:- No
statusLineconfigured, the wrapper is not running because nothing is. Print the
standalone wiring from the template below (the shim is then the whole statusline). statusLinepresent, command references neither the shim nor this plugin'sstatusline-tee.sh. Wrapper missing. Print the wrapped wiring below with the user's
current command preserved as the wrapped command.statusLinereferences acontext-guardstatusline-tee.shunder the plugin cache.
LEGACY VERSION-PINNED WIRING: classify, report, and remediate per
reference/legacy-statusline-detect.md "Legacy
version-pinned wiring" (the fix'sapplyis step 3's).statusLineinvokes~/.claude/context-guard/bin/statusline-shim.sh. PASS. No path
comparison against${CLAUDE_PLUGIN_ROOT}applies or is meaningful here; the shim resolves
the tee at run time.
Orthogonal to all four, and checked before reporting any of them as working, two
environment-side states that make a configured command inert:- The session is terminal-less. The statusline is a terminal-interface surface, so a
session with no terminal interface does not run a statusline even when one is wired.
Measured 2026-08-21 for Claude Code on the web and for aclaude -prun, a configuredstatusLinewas never invoked in either, and expected on the same reasoning, though not
measured, for other non-terminal environments such as a self-hosted cloud runner. Recheck
when a release note or the statusline docs describe a statusline running without a terminal
interface, or when a re-measurement on the web or underclaude -pobserves a configuredstatusLinebeing invoked. Where
you can tell you are in such a session, report this as INFO: no capture channel in this
environment regardless of which of the four wiring states applies, say thatunknownis
the correct and permanent zone here, and do not print wiring the operator cannot make run.
A correctly-wired shim in a cloud or headless session is still never invoked; classifying
that wiring as PASS and the missing snapshot as a wiring FAIL is the defect this exception
exists to prevent.${CLAUDE_PLUGIN_ROOT}/reference/cloud-headless-capture.mdrecords why no substitute
channel exists (every channel checked, with sources and dates) and the cloud-and-headless
section of${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.mdcarries the consumer rule. - The status line is turned off with a
statusLinestill configured. Claude Code
disables it entirely when managed settings setdisableAllHooksor the folder is not
trusted, and narrows the source to managed settings whenallowManagedHooksOnlyis set;
under narrowing it runs a managed value if one is deployed and otherwise skips your value
without warning, leaving the status line disabled. Report that state as INFO: the status
line is disabled by policy or workspace trust, name which of the three conditions
applies, and route the operator to policy or trust. It is not a wiring defect, and
printing wiring will not fix it. The dated record for both settings keys is${CLAUDE_PLUGIN_ROOT}/reference/cloud-headless-capture.md, branch 3 of "Distinguishing
structural absence from breakage".
- No
Live-session snapshot freshness. This session's id is
${CLAUDE_SESSION_ID}. Probe~/.claude/context-guard/context/${CLAUDE_SESSION_ID}.json:- Exists and
captured_atis within the reader contract's 10-minute staleness window → PASS
(zone-informed consumers get real data). Also report the zone:bash "${CLAUDE_PLUGIN_ROOT}/scripts/context-zone.sh" ${CLAUDE_SESSION_ID}. - Fresh but
used_percentageorcurrent_usagenull → INFO: documented early-session or
post-/compactstatusline state; the resolver correctly answersunknown. Not a defect. - Absent or stale while step 4 found no
statusLinein any scope → INFO, not FAIL:
nothing is writing snapshots because nothing is configured to, whether the file is missing
or a leftover from an earlier session has gone stale. Which INFO depends on the same
condition step 4 branched on, and the two reports must agree, never print step 4's wiring
and then say nothing is broken.- If step 4 took the terminal-less exception (you could tell this session refreshes no
statusline) this is structural:unknownis correct and permanent here, no other channel
can supply one, and there is nothing to fix. Do not report a defect and do not send the
operator to fix an install that is not broken. - Otherwise this is the not-yet-wired state, the ordinary state of a fresh local
install, and the single most common reasoncheckis run. The remediation is the wiring
step 4 just printed; point at it, say snapshots start on the next statusline refresh once
it is applied, and do not call this structural.
- If step 4 took the terminal-less exception (you could tell this session refreshes no
- Absent or stale while step 4 reported correct wiring, did not find the status line
disabled, and did not take the terminal-less exception → FAIL: the wrapper is wired but
not running. Re-check steps 3 and 4; a shim that is wired but not installed produces
exactly this. The file updates only while this session is interactive. - If the literal string
${CLAUDE_SESSION_ID}appears unexpanded above, report that this
Claude Code version lacks the substitution and consumers will take the conservative path; probe the newest file in~/.claude/context-guard/context/instead, labeled as such.
- Exists and
zones.json state, a read-only report over the pre-computed
zones.jsonvalue: absent
(shipped defaults in effect, percentage 50/75 plus the window-class token bands; valid
zero-config state, not a defect), present and valid
(report the bands in effect, both shapes), or present with a malformed shape (report per
shape: the resolver validates percentage keys andtoken_bandsindependently and falls back
per shape with a stderr notice; a percentage-only file withouttoken_bandsis valid, with
shipped token bands silently in effect; remediation:apply). A(present but unreadable)
token, or acat:error in place of the contents, is the fourth state: the file exists and
cannot be read, which is a defect the absent branch would hide. Report the read error and route
the operator to the file's permissions, not toapply. Note the hooks resolve zones through this same data: a machine with no snapshots gets silent hooks, not errors.Hook registration vs hook activation. Three separate facts, never collapsed into one
status. A registered hook set that every hook exits out of immediately is the exact state an
operator is diagnosing when injections or gating are missing, and reporting "active" because the
plugin is enabled tells them the opposite of the runtime state.- Registered, the plugin is enabled, so
hooks/hooks.jsonis loaded and the matchers fire.
This follows from the plugin being enabled and says nothing about what the hooks then do. - Hook set armed, the
context_guard_hooks_enabledkill switch. Read its configured value,
not the plugin's enablement: the value substituted here is${user_config.context_guard_hooks_enabled}. Interpret it asfalse→ INERT: registered but every hook (injection, gate, PostCompact marker) exits
immediately without acting. Remediation: re-enable the option via/plugin.true→ armed.- anything else, including the literal
${user_config.context_guard_hooks_enabled}surviving
unexpanded (unset key, or a Claude Code without the substitution) → UNKNOWN, never
"armed". Say which source was read and that an unset key falls back to the hooks' in-script
default (armed); the operator-inspectable source of truth is this plugin'spluginConfigsoptions block in the usersettings.json(the hook-config-delivery
convention,
https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/hook-config-delivery/README.md,
owns why the declareddefaultfield is not delivered to hook processes).
- Gate posture.
zone_hook_modeis${user_config.zone_hook_mode}, read and interpreted
the same way. Onlyblockingmakes the PreToolUse gate do anything;advisory(the in-script
default) leaves it inert while the injection hook still runs. Report it separately: an armed
hook set with an advisory posture is a different runtime state from an inert hook set, and
only one of the two is a defect.
- Registered, the plugin is enabled, so
Print the operator edit, except when step 4 took the terminal-less exception, found the
status line disabled by policy or trust, or found the effective command owned by managed
settings. Those branches already forbade printing wiring the operator cannot make run. When
this step does print, the wiring target is the shim's fixed path, never${CLAUDE_PLUGIN_ROOT}. Compose the value by runningbash "${CLAUDE_PLUGIN_ROOT}/scripts/compose-statusline-wiring.sh"over the effectivestatusLinevalue from step 4, never by peeling and wrapping the string yourself:jq '.statusLine' <the settings file that owns the effective command> | bash "${CLAUDE_PLUGIN_ROOT}/scripts/compose-statusline-wiring.sh" \ --wrap 'bash ~/.claude/context-guard/bin/statusline-shim.sh' --block --explainRead `reference/unwrap-before-compose.md` for that
script's argument and exit-code contract and the judgments it leaves to you (shared with
rate-limit-guard), then `reference/statusline-edit.md` for this
plugin's JSON edit blocks, the combined sibling-shim invocation, and the Windows note. Composing
by hand double-wraps a sibling tee and stacks anothersh -clayer on every re-run.Dotfiles tracking proposal, the printed edit changes a durable user-scope file the operator
maintains. When the operator's home directory is managed by a dotfiles system (chezmoi, yadm, a
bare-repo setup, ...), surface the reminder to capture thesettings.jsonchange through that
system's own add/track flow so the wiring survives machine rebuilds. This skill only surfaces
the reminder; it runs no dotfiles command.
apply (writes only inside ~/.claude/context-guard/, on explicit request)
Two files, both in this plugin's own operator-home directory. Every apply mode does both; thedefaults argument affects only the zones bands.
A. Install the statusline shim
Copy ${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh to~/.claude/context-guard/bin/statusline-shim.sh, creating bin/ if needed, and chmod +x the
result (a no-op on Windows ACL volumes; the wiring invokes it through bash anyway):
- The installed copy is byte-identical to the shipped source, never a rewrite, never a
templated variant. That is what makescheckstep 3 a plaincmp. - Idempotent: if the file already exists and compares equal, write nothing and say so.
Otherwise overwrite it (this is the update path after a plugin version bump changes the shim)
and report the# shim-revision:values, old → new. - The shim is inert until wired: installing it starts nothing. Only the operator's
settings.jsonedit puts it on the statusline path. That edit is step 8 ofcheck, which
this skill never applies. Say that explicitly when reporting the write. - After installing, print the wiring edit (
checkstep 8), honoring that step's exceptions,
so the operator's next action is in front of them when there is one, and note that a
statusline already wired to the shim needs no change now or on any future plugin update.
B. Seed or refresh the zones SSOT
Seed or refresh ~/.claude/context-guard/zones.json from the shipped defaults
(smart_max_used_percentage: 50, acceptable_max_used_percentage: 75, and the window-classtoken_bands, the reader contract owns these numbers; read them from${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.md rather than this file if they ever disagree):
File absent. Create the directory if needed and write exactly:
{ "smart_max_used_percentage": 50, "acceptable_max_used_percentage": 75, "token_bands": { "200000": { "smart_max_tokens": 100000, "acceptable_max_tokens": 160000 }, "1000000": { "smart_max_tokens": 200000, "acceptable_max_tokens": 400000 } } }File present. Behavior is mode-explicit, never ambiguous:
apply(no argument): repair-only. Valid recognized band values are left untouched and
reported; recognized keys that are missing or invalid (non-numeric, inverted, out of range; fortoken_bands, invalid per the reader contract's per-shape validity rules) are set to the
shipped defaults. An absenttoken_bandsis repaired by adding the shipped token bands
(absence is valid zero-config for the resolver, but the seeded SSOT should carry the full
tunable surface). An operator's custom-but-valid thresholds are never overwritten by a
bareapply.apply defaults: set all recognized band keys (both percentage keys andtoken_bands) to
the shipped defaults explicitly. This converges forward to a known state; it is not teardown,
and it never removes the file or any key it does not recognize.- Both modes preserve every unrecognized key semantically: same keys, same JSON values
(the file is a shared SSOT the operator's own statusline may extend). Preservation is
value-level, not lexical: ajqmerge reserializes the document, so formatting and escape
spellings may normalize ("blue"→"blue"); consumers of this file must parse it as
JSON, never depend on its raw bytes. Usejqto merge so the result stays valid JSON. Ifjqis absent while the file exists, FAIL with the jq install remediation
(https://jqlang.org/download/) instead of attempting a merge, never risk clobbering the
operator's keys with a jq-less rewrite. (Step 1's template write needs no jq.)
Idempotent, a second identical
applyproduces no content change; say so.Report exactly what was written (old bands → new bands, unrecognized keys preserved), and
remind that consumers re-read the file on their next zone decision. No restart needed.
apply never touches settings.json, the snapshot directory, or anything outside~/.claude/context-guard/. Statusline wiring stays print-only.
Uninstalling
Uninstalling the plugin removes the cache directory, not the operator's files. Nothing breaks: the
shim finds no tee and passes the wrapped statusline through unchanged (a wired-standalone shim
prints one notice line instead). Two operator cleanup steps remain, and their order matters. Report both together, in this order, when asked how to back this out:
- Unwrap the
statusLinecommand first, restoring the operator's own renderer (or removing
the field entirely if the shim was the whole statusline). - Then remove
~/.claude/context-guard/.
Deleting the directory while the wiring still names the shim leaves settings.json invoking a
missing file: bash <missing-path> exits 127 and takes the whole statusline down, the exact
failure the shim exists to prevent. The shim's own no-tee fallback cannot cover this, because the
fallback lives in the file that was just deleted.
What this skill does not do
- Write the plugin cache, Claude Code user settings, or
pluginConfigs, per the uniform setup
contract (docs/plugin-philosophy.md"Setup is explicit and repeatable" in the marketplace
repository). Norsettings.json(user or project) or any other Claude Code settings surface; the printed edit is the operator's to apply. - Install
jqor any system package. - Write to the snapshot directory
~/.claude/context-guard/context/, the tee owns those files. - Write anywhere outside
~/.claude/context-guard/, including the siblingrate-limit-guard
directory, whose own setup skill installs that plugin's shim.