melodic-software

claude-code-plugins

"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)."

melodic-software 28 5 Updated 3w ago

Resources

2
GitHub

Install

npx skillscat add melodic-software/claude-code-plugins/plugins-context-guard-skills-setup

Install via the SkillsCat registry.

SKILL.md

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 shim
bin/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)

  1. jq. Read the pre-computed jq value. FAIL when it is absent: without jq the wrapper
    cannot tee (it stays transparent and shows a visible notice), the standalone statusline
    degrades, and the zone resolver prints unknown. Remediation: install jq
    (https://jqlang.org/download/).

  2. node. Read the pre-computed node value. FAIL when it is absent: every hook row in
    ${CLAUDE_PLUGIN_ROOT}/hooks/hooks.json runs node hooks/exec-bash.mjs <script>, and Claude
    Code's native binary neither ships nor uses Node
    (https://code.claude.com/docs/en/setup), so without node on PATH the 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 use node.
    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.

  3. 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 read cmp: not identical as drift when the shipped path is the missing one.

  4. 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, where statusLine is 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-level statusLine shadows 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 statusLine configured, the wrapper is not running because nothing is. Print the
      standalone wiring from the template below (the shim is then the whole statusline).
    • statusLine present, command references neither the shim nor this plugin's
      statusline-tee.sh
      . Wrapper missing. Print the wrapped wiring below with the user's
      current command preserved as the wrapped command.
    • statusLine references a context-guard statusline-tee.sh under the plugin cache.
      LEGACY VERSION-PINNED WIRING: classify, report, and remediate per
      reference/legacy-statusline-detect.md "Legacy
      version-pinned wiring" (the fix's apply is step 3's).
    • statusLine invokes ~/.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 a claude -p run, a configured
      statusLine was 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 under claude -p observes a configured
      statusLine being 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 that unknown is
      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.md records 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.md carries the consumer rule.
    • The status line is turned off with a statusLine still configured. Claude Code
      disables it entirely when managed settings set disableAllHooks or the folder is not
      trusted, and narrows the source to managed settings when allowManagedHooksOnly is 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".
  5. Live-session snapshot freshness. This session's id is ${CLAUDE_SESSION_ID}. Probe
    ~/.claude/context-guard/context/${CLAUDE_SESSION_ID}.json:

    • Exists and captured_at is 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_percentage or current_usage null → INFO: documented early-session or
      post-/compact statusline state; the resolver correctly answers unknown. Not a defect.
    • Absent or stale while step 4 found no statusLine in 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: unknown is 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 reason check is 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.
    • 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.
  6. zones.json state, a read-only report over the pre-computed zones.json value: 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 and token_bands independently and falls back
    per shape with a stderr notice; a percentage-only file without token_bands is valid, with
    shipped token bands silently in effect; remediation: apply). A (present but unreadable)
    token, or a cat: 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 to apply. Note the hooks resolve zones through this same data: a machine with no snapshots gets silent hooks, not errors.

  7. 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.json is 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_enabled kill switch. Read its configured value,
      not the plugin's enablement: the value substituted here is
      ${user_config.context_guard_hooks_enabled}. Interpret it as
      • false → 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's
        pluginConfigs options block in the user settings.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 declared default field is not delivered to hook processes).
    • Gate posture. zone_hook_mode is ${user_config.zone_hook_mode}, read and interpreted
      the same way. Only blocking makes 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.
  8. 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 running
    bash "${CLAUDE_PLUGIN_ROOT}/scripts/compose-statusline-wiring.sh" over the effective
    statusLine value 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 --explain

    Read `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 another sh -c layer on every re-run.

  9. 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 the settings.json change 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; the
defaults 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 makes check step 3 a plain cmp.
  • 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.json edit puts it on the statusline path. That edit is step 8 of check, which
    this skill never applies. Say that explicitly when reporting the write.
  • After installing, print the wiring edit (check step 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-class
token_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):

  1. 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 }
      }
    }
  2. 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; for token_bands, invalid per the reader contract's per-shape validity rules) are set to the
      shipped defaults. An absent token_bands is 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
      bare apply.
    • apply defaults: set all recognized band keys (both percentage keys and token_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: a jq merge 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. Use jq to merge so the result stays valid JSON. If
      jq is 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.)
  3. Idempotent, a second identical apply produces no content change; say so.

  4. 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:

  1. Unwrap the statusLine command first, restoring the operator's own renderer (or removing
    the field entirely if the shim was the whole statusline).
  2. 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). Nor settings.json (user or project) or any other Claude Code settings surface; the printed edit is the operator's to apply.
  • Install jq or 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 sibling rate-limit-guard
    directory, whose own setup skill installs that plugin's shim.