"Verify the context-guard plugin's wiring on this machine — jq, the installed statusline shim, statusline wiring (including legacy version-pinned plugin-cache paths), 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
1Install
npx skillscat add melodic-software/claude-code-plugins/plugins-context-guard-skills-setup Install via the SkillsCat registry.
This skill checks or applies context-guard's statusline wiring and shim, verifying system dependencies like jq and plugin cache paths, and ensures live-session snapshots are fresh. It addresses inconsistent statusline behavior after updates or misconfigured zones by validating wiring and reinstalling the shim with default zones. Use it during setup, post-update, or when context tees fail in live sessions.
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—command -v jq. FAIL if absent: without it 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/).Installed shim state — the shim is the wiring target, so check it before the wiring. Compare
~/.claude/context-guard/bin/statusline-shim.shagainst${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh(the installed copy is byte-identical by
contract, socmp -sis the test):- Absent — FAIL when the statusline is wired to it (that wiring cannot run), INFO otherwise.
Remediation:apply. - Present and identical — PASS. Nothing about it needs revisiting on a plugin update; that
is the whole point of the shim. - Present but differing — classify by what the installed revision can still do, not by the
fact that it differs. Report the shipped# shim-revision:marker against the installed one
either way, say which of the two behaviors the installed copy has, and offerapplyas the
refresh.- Installed revision >= 3 — INFO: an older-but-adequate or hand-edited copy that still
resolves the newest tee correctly. A refresh is housekeeping. - Installed revision < 3, or unmarked — FAIL. Such a copy picks the newest tee by mtime
alone, so it also resolves one left behind by an UNINSTALLED plugin and keeps teeing for
the whole orphan grace window. The statusline keeps rendering, which is why this reads as
harmless and is not: it is a behavior defect in what the operator is running, and INFO
files it under a heading operators are told they can defer. - The migration matters more than the classification. The durable copy at
~/.claude/context-guard/bin/statusline-shim.shis what the statusline actually runs; a
plugin update never overwrites it. An operator who ranapplybefore revision 3 shipped
therefore keeps running the old shim until they re-runapply— and if they uninstall the
plugin first, this skill is gone and the stale shim keeps teeing with no remaining way to
reach the remediation. Say that in the finding, so the reason to act now is on screen.
- Installed revision >= 3 — INFO: an older-but-adequate or hand-edited copy that still
- The SHIPPED source is absent (no
${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh) —
INFO, and skip the comparison entirely: this installed plugin version predates the shim
(< 0.2.0). Never report the operator's installed copy as drifted on this branch. Remediation:/plugin update context-guard, then re-runcheck. Until then the legacy version-pinned
wiring in step 3 is the only wiring this version can offer.
- Absent — FAIL when the statusline is wired to it (that wiring cannot run), INFO otherwise.
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 determine which one owns the EFFECTIVE command (the most
specific scope wins). All wiring states below are evaluated against that effective command,
and the printed edit in step 7 targets THAT scope's file — wiring the user file while a
project-levelstatusLineshadows it would apply cleanly and never run; when a 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
(.../plugins/cache/<marketplace>/context-guard/<version>/scripts/…) — LEGACY
VERSION-PINNED WIRING, regardless of whether that file currently exists. It is running
today only until the next version bump, and it breaks the whole statusline once the old
version directory is pruned (~14 days after an update). Report it as the failure mode this
plugin's shim exists to remove, and print the shim wiring as the fix (step 2'sapplyfirst
if the shim is not installed). An interim[ -f … ]existence guard around such a path is
the same state: it survives pruning but still stops teeing on a version bump.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.
- 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 3 reported correct wiring → FAIL: the wrapper is wired but not
running (the statusline refreshes only in interactive sessions; also re-check steps 2 and 3 —
a shim that is wired but not installed produces exactly this). Note the file only updates
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 — read-only report: 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 v1 file withouttoken_bandsis valid, with shipped token bands
silently in effect; remediation:apply). 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
(docs/conventions/hook-config-deliveryowns 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 — always print the applicable statusline edit for the settings
file that owns the effective command (step 3), marked clearly as the operator's to apply. The
wiring target is the SHIM's fixed path — never${CLAUDE_PLUGIN_ROOT}, which is version-pinned
and belongs in no operator file:Unwrap before you compose.
<current statusline command>below means the operator's OWN
renderer, never the raw effectivecommandstring. Recover it by peeling off the wrapping this
skill itself prints, applying BOTH rules repeatedly until a pass strips nothing:Guard-shim prefixes — every leading
bash <path>/context-guard/bin/statusline-shim.shandbash <path>/rate-limit-guard/bin/statusline-shim.sh, in whatever order they appear, plus any
legacybash <plugin-cache>/…/statusline-tee.shprefix.A generated
sh -cadapter — when what remains is EXACTLYsh -c '<single-quoted string>'
with nothing after the closing quote, AND, once that string is unescaped, ANY of the following
holds, that is an adapter a previous run printed, not the renderer. Unescape it back: drop the
leadingsh -cand the outer quotes, then replace every'\''with'.- A — it is itself EXACTLY
sh -c '<single-quoted string>', nothing after the closing
quote. A nestedsh -cis always a layer some run added: an operator's own renderer is at
most onesh -cdeep. Apply the same strictness here as to the outer shape, so two readers
peel the same number of layers. - B — it begins with a guard-shim prefix from rule 1. This skill never puts a shim inside
an adapter, and an operator would not write one inside their ownsh -c. Leaving it sealed
there hides it from rule 1, which strips only LEADING prefixes, and the composed wiring then
names that shim a second time. - C — it is a command the guard below would send for wrapping. That is the only shape this
skill's own adapter ever carries.
Branches A and B must NOT inherit the guard's top-level scoping — their evidence is the shape
of the carried string, not the syntax in it. Absent all three, thesh -cwas written by the
operator and must be preserved: peelingsh -c 'ulimit -n'toulimit -nwould leave the
shimexec-ing a shell builtin that no longer has a shell, and the statusline would exit 127
instead of rendering. A trailing word (sh -c '…' extra) makes it a real command, not an
adapter — leave that alone too.One shape stays ambiguous on purpose: a single
sh -cover a merely-quoted command, which a
version of this skill that wrongly counted quoting as a trigger also emitted. Nothing in it
distinguishes that from an operator's own, so it is preserved. The cost is one spurious shell
per refresh; peeling on a guess costs a broken statusline.- A — it is itself EXACTLY
One pass is not enough: an operator may already carry several layers from earlier reruns, and a
single peel over three layers leaves two.Substituting the raw string instead is what produces
context → rate → rate → rendererwhen the
sibling plugin was configured first, or a doubled self-wrap on a re-run: each duplicated tee runs
and writes on EVERY refresh and costs another 0.6–0.9 s (below). Skipping rule 2 compounds the
shell-syntax guard instead — the leftover adapter still contains shell syntax, so it is wrapped in
ANOTHERsh -clayer, one more on every run. Unwrapping both makes the printed edit idempotent:
re-runningcheckon already-correct wiring prints byte-identical wiring, with exactly one shim
invocation per plugin and at most onesh -clayer.Wrapping an existing statusline command (preserve the user's unwrapped command verbatim as the
trailing arguments):{ "statusLine": { "type": "command", "command": "bash ~/.claude/context-guard/bin/statusline-shim.sh <current statusline command>" } }No statusline configured (standalone minimal statusline):
{ "statusLine": { "type": "command", "command": "bash ~/.claude/context-guard/bin/statusline-shim.sh" } }Shell-syntax guard: the wrapped form passes the user's command as ARGV — the shell that runs
thestatusLinecommand splits the whole line into words and consumes its quotes, and the shimexecs those words unchanged. It therefore only works for plainexecutable arg…commands. Test
the UNWRAPPED renderer, never the raw effectivecommandstring — the rules above run first.
Print the shell-wrapped variant instead when EITHER of these holds:It carries — UNQUOTED, at the top level — shell syntax no ARGV word can express: an inline env
assignment likeTHEME=dark my-statusline, a redirection, or any control operator (|,|&,&&,||,;,&, a newline).Its command word is not an executable — a shell builtin, function, or alias, which
exec
cannot run because there is no file to exec.ulimit '-n'is the standing example:ulimit
exists only as a builtin, so the plain wrapped form reachesexec ulimit -nand the statusline
dies with exit 127 on every refresh. Resolve it the way the shim will —type -P <word>finding nothing whiletype -t <word>reportsbuiltin,function, oraliasis the test — not by matching a hardcoded list of builtin names.This trigger is load-bearing precisely because it is not about syntax. Such a renderer often
carries none at all, and before the guard was scoped to real syntax the bare presence of quotes
wrapped it by accident. That accident was doing real work, and dropping it without this
replacement is what turns a working statusline into exit 127.
{ "statusLine": { "type": "command", "command": "bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c '<escaped renderer>'" } }<escaped renderer>is that same unwrapped renderer, POSIX-escaped for single-quote
embedding: replace every'in it with'\''before substituting (then JSON-escape the wholecommandstring as usual). Show the final, fully escaped line — never hand the operator a
template with raw quotes left to fix. Verify your printed edit round-trips: runprintf '%s\n' '<escaped renderer>'and confirm the output matches the renderer
byte-for-byte. The single-quoted argument reproduces exactly the quoting context the emittedsh -c '<escaped renderer>'uses; a double-quoted wrapper would instead let the outer shell
expand any$(...)or backticks in the operator's own renderer before the check ever ran.Syntax inside a quoted argument does not count, and bare quoting is never itself a trigger.
The quotes make it one ordinary ARGV word that reaches the renderer intact through the plain
wrapped form. So an operator's ownsh -c '<string>'— the one shape rule 2 preserves — is
ALREADY a plainexecutable arg…command:shis the executable,-cand the carried string
are two ordinary ARGV words. Substitute it VERBATIM.For an input that is ITSELF
sh -c '<string>', rule 2 and this guard therefore never both wrap
it, and leave exactly one layer: rule 2 peels every generated layer before the guard runs, and
what rule 2 preserves is a renderer this guard declines. Do not generalize that to a layer count
for every input — the guard adds whatever the renderer genuinely needs, which is NONE for a plain
command and ONE for top-level syntax, and that one is a layer more when the operator's ownsh -csits inside it.sh -c 'ulimit -n' && echo okcorrectly prints TWO: the&&cannot be an
ARGV word, so the adapter is mandatory, and peeling the innersh -cwould strand the builtin.
What is invariant is that peel and wrap are inverses, which is what makes a re-run byte-identical
at whatever count the renderer needs. Firing on the quotes instead is what turned an operator'ssh -c 'ulimit -n'intosh -c 'sh -c '\''ulimit -n'\''', one more shell on every refresh and
the same compounding rule 2 exists to prevent.Sibling tees compose by nesting, each through its OWN shim — the tees are transparent wrappers,
so the innermost command still owns stdout and the exit code. Print this form only whenrate-limit-guardis installed AND its shim is already present at~/.claude/rate-limit-guard/bin/statusline-shim.sh. The sibling shim is written by/rate-limit-guard:setup apply, which the operator may not have run yet — naming a path that
does not exist reintroduces exactly the failure this wiring exists to remove, becausebash <missing-path>exits 127 before the operator's renderer ever runs. When the sibling plugin is
installed but its shim is absent, print the single-shim form above and say that/rate-limit-guard:setup applyfollowed by a re-run of this check yields the combined wiring:{ "statusLine": { "type": "command", "command": "bash ~/.claude/context-guard/bin/statusline-shim.sh bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh <current statusline command>" } }The shell-syntax guard applies UNCHANGED to this form:
<current statusline command>is the
innermost ARGV here too, so run the same test above on the same unwrapped renderer and substitute
whichever of the two forms it selects — never the raw string. SubstitutingTHEME=dark my-statuslineraw
makesTHEME=darkthe executable, which failscommand not found(127) instead of setting the
variable. The shim paths are the only part that nests; the innermost substitution rule never
changes:{ "statusLine": { "type": "command", "command": "bash ~/.claude/context-guard/bin/statusline-shim.sh bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c '<escaped renderer>'" } }State the measured cost with the combined form: each tee adds roughly 0.6–0.9 s per statusline
refresh on Windows/Git Bash (process-spawn bound), on top of the operator's own statusline
command.refreshIntervalsets how often that runs; the statusline is not on the input path, so
the cost is display latency, not typing latency.Windows note: the command must run under Git Bash —
bashis invoked explicitly for exactly
that reason (the script's stated shell requirement); with Git Bash absent Claude Code routes
statusline commands through PowerShell and this wiring does not apply (statusline reference,
"Windows configuration"). State this with the printed edit: the wiring is applied ONCE and
survives every later plugin update, because the shim — not the version-pinned cache path — is
what the settings file names.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 2 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 — step 7 ofcheck, which this skill never applies — puts it on the
statusline path. Say that explicitly when reporting the write. - After installing, print the wiring edit (
checkstep 7) so the operator's next action is in
front of them, 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. A v1 file's 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 (owner-approved execution-shape
decision).
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
- Edit
settings.json(user or project), writepluginConfigs, or touch any 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.