Pre-computed context
Three 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"
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)
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/).
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.
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 7 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 2'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. 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".
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 3 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 3 branched on, and the two reports must agree, never print step 3's wiring
and then say nothing is broken.
- If step 3 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 3 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 3 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 2 and 3; 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.
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.
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.
Print the operator edit, except when step 3 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 3, 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.
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 2 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 — step 7 of check, which this skill never applies — puts it on the
statusline path. Say that explicitly when reporting the write.
- After installing, print the wiring edit (
check step 7) — 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):
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; 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.)
Idempotent, a second identical apply produces 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
statusLine command 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). 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.
1---2name: setup-403description: 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).4---56## Pre-computed context78Three of `check`'s read-only probes run at load time. Read the values below; do not re-issue them.910`jq` (a path = present, `absent` = missing): !`command -v jq 2>/dev/null || echo "absent"`11Installed 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"; }`12`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`1314## Purpose1516Narrow-write setup, because this plugin's surface splits in two. The statusline wiring lives in the17**user's own** `settings.json` and the `jq` prerequisite is a system tool: neither is something18plugin setup may write, so `check` inspects, reports PASS/FAIL/INFO with one remediation line per19FAIL, and **prints the exact statusline edit for the operator to apply by hand**. But this plugin20also owns its operator-home directory `~/.claude/context-guard/`, the machine file `zones.json`,21whose schema it defines and whose values the operator may edit, and the statusline shim22`bin/statusline-shim.sh`, the durable path the operator's wiring names, and those owned writable23artifacts are what oblige an `apply`. `apply` is scoped to that directory and touches nothing else.2425**Why the shim exists (the durable-wiring rule).** `${CLAUDE_PLUGIN_ROOT}` is version-pinned and26changes on every plugin update, and the old version directory is pruned about 14 days later27(plugins reference, "Plugin cache and file access"). A statusline wired straight to28`<plugin-root>/scripts/statusline-tee.sh` therefore stops teeing at the next version bump and, once29the old directory is pruned, `bash <missing-path>` exits 127 and takes the operator's whole30statusline down with it. So the operator wires the **shim**, never the tee: the shim lives at a31path that never changes, resolves the newest installed tee at run time, and degrades to running the32wrapped command alone when no tee is installed. Read33`${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh` for its actual resolution rule rather than34reciting this paragraph.3536The scripts are the source of truth for their own behavior. Read37`${CLAUDE_PLUGIN_ROOT}/scripts/statusline-tee.sh` and38`${CLAUDE_PLUGIN_ROOT}/scripts/context-zone.sh` first; probe what they actually do rather than39reciting this file. The consumer-facing constants (snapshot path pattern, staleness rule, default40zone bands, zones.json shape) are owned by41`${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.md`.4243## `check` (read-only)44451. **`jq`**. Read the pre-computed `jq` value. FAIL when it is `absent`: without jq the wrapper46 cannot tee (it stays transparent and shows a visible notice), the standalone statusline47 degrades, and the zone resolver prints `unknown`. Remediation: install jq48 (<https://jqlang.org/download/>).492. **Installed shim state**, the shim is the wiring target, so check it before the wiring. The50 pre-computed shim value compares `~/.claude/context-guard/bin/statusline-shim.sh` (the durable51 shim copy) against `${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh` (the shipped source) and52 carries the `# shim-revision:` marker of each file that exists. Classify it per53 [reference/legacy-statusline-detect.md](reference/legacy-statusline-detect.md) "Installed shim54 state", shared with the sibling guard plugin and synced byte-identical. An absent shipped55 source takes that reference's own branch and ends the comparison; never read `cmp: not56 identical` as drift when the shipped path is the missing one.573. **Statusline wiring state**. Read (never write) every settings scope that can carry a58 `statusLine` (user `~/.claude/settings.json`, project `.claude/settings.json`, local59 `.claude/settings.local.json`, and managed settings, where `statusLine` is also a valid key)60 and determine which one owns the effective command (the most specific scope wins among the61 three non-managed scopes; a managed value outranks all of them). All wiring states below are62 evaluated against that effective command. The printed edit in step 7 targets that scope's63 file **except** when the owning scope is managed: that file is administrator-controlled, the64 operator running this skill generally cannot change it, and no lower-scope edit can override65 it. In that case name the managed source, say the operator cannot change it from here, and66 route to the policy administrator. Do not print an operator edit for the managed file.67 Wiring the user file while a project-level `statusLine` shadows it would apply cleanly and68 never run; when a non-managed shadow exists, say so explicitly and print the edit for the69 shadowing file (or note that removing the override is the alternative). Distinguish four70 states:71 - **No `statusLine` configured**, the wrapper is not running because nothing is. Print the72 standalone wiring from the template below (the shim is then the whole statusline).73 - **`statusLine` present, command references neither the shim nor this plugin's74 `statusline-tee.sh`**. Wrapper missing. Print the wrapped wiring below with the user's75 current command preserved as the wrapped command.76 - **`statusLine` references a `context-guard` `statusline-tee.sh` under the plugin cache**.77 LEGACY VERSION-PINNED WIRING: classify, report, and remediate per78 [reference/legacy-statusline-detect.md](reference/legacy-statusline-detect.md) "Legacy79 version-pinned wiring" (the fix's `apply` is step 2's).80 - **`statusLine` invokes `~/.claude/context-guard/bin/statusline-shim.sh`**. PASS. No path81 comparison against `${CLAUDE_PLUGIN_ROOT}` applies or is meaningful here; the shim resolves82 the tee at run time.8384 Orthogonal to all four, and checked before reporting any of them as working, two85 environment-side states that make a configured command inert:8687 - **The session is terminal-less.** The statusline is a terminal-interface surface, so a88 session with no terminal interface does not run a statusline even when one is wired.89 Measured 2026-08-21 for Claude Code on the web and for a `claude -p` run, a configured90 `statusLine` was never invoked in either, and expected on the same reasoning, though not91 measured, for other non-terminal environments such as a self-hosted cloud runner. Where92 you can tell you are in such a session, report this as **INFO: no capture channel in this93 environment** regardless of which of the four wiring states applies, say that `unknown` is94 the correct and permanent zone here, and do not print wiring the operator cannot make run.95 A correctly-wired shim in a cloud or headless session is still never invoked; classifying96 that wiring as PASS and the missing snapshot as a wiring FAIL is the defect this exception97 exists to prevent.98 `${CLAUDE_PLUGIN_ROOT}/reference/cloud-headless-capture.md` records why no substitute99 channel exists (every channel checked, with sources and dates) and the cloud-and-headless100 section of `${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.md` carries the consumer rule.101 - **The status line is turned off with a `statusLine` still configured.** Claude Code102 disables it entirely when managed settings set `disableAllHooks` or the folder is not103 trusted, and narrows the source to managed settings when `allowManagedHooksOnly` is set;104 under narrowing it runs a managed value if one is deployed and otherwise skips your value105 without warning, leaving the status line disabled. Report that state as **INFO: the status106 line is disabled by policy or workspace trust**, name which of the three conditions107 applies, and route the operator to policy or trust. It is not a wiring defect, and108 printing wiring will not fix it. The dated record for both settings keys is109 `${CLAUDE_PLUGIN_ROOT}/reference/cloud-headless-capture.md`, branch 3 of "Distinguishing110 structural absence from breakage".1114. **Live-session snapshot freshness**. This session's id is `${CLAUDE_SESSION_ID}`. Probe112 `~/.claude/context-guard/context/${CLAUDE_SESSION_ID}.json`:113 - Exists and `captured_at` is within the reader contract's 10-minute staleness window → PASS114 (zone-informed consumers get real data). Also report the zone:115 `bash "${CLAUDE_PLUGIN_ROOT}/scripts/context-zone.sh" ${CLAUDE_SESSION_ID}`.116 - Fresh but `used_percentage` or `current_usage` null → INFO: documented early-session or117 post-`/compact` statusline state; the resolver correctly answers `unknown`. Not a defect.118 - Absent or stale while step 3 found **no `statusLine` in any scope** → INFO, not FAIL:119 nothing is writing snapshots because nothing is configured to, whether the file is missing120 or a leftover from an earlier session has gone stale. Which INFO depends on the same121 condition step 3 branched on, and the two reports must agree, never print step 3's wiring122 and then say nothing is broken.123 - **If step 3 took the terminal-less exception** (you could tell this session refreshes no124 statusline) this is structural: `unknown` is correct and permanent here, no other channel125 can supply one, and there is nothing to fix. Do not report a defect and do not send the126 operator to fix an install that is not broken.127 - **Otherwise** this is the not-yet-wired state, the ordinary state of a fresh local128 install, and the single most common reason `check` is run. The remediation is the wiring129 step 3 just printed; point at it, say snapshots start on the next statusline refresh once130 it is applied, and do not call this structural.131 - Absent or stale while step 3 reported correct wiring, did not find the status line132 disabled, and did not take the terminal-less exception → FAIL: the wrapper is wired but133 not running. Re-check steps 2 and 3; a shim that is wired but not installed produces134 exactly this. The file updates only while this session is interactive.135 - If the literal string `${CLAUDE_SESSION_ID}` appears unexpanded above, report that this136 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.1375. **zones.json state**, a read-only report over the pre-computed `zones.json` value: absent138 (shipped defaults in effect, percentage 50/75 plus the window-class token bands; valid139 zero-config state, not a defect), present and valid140 (report the bands in effect, both shapes), or present with a malformed shape (report per shape141 — the resolver validates percentage keys and `token_bands` independently and falls back per142 shape with a stderr notice; a percentage-only file without `token_bands` is valid, with143 shipped token bands silently in effect; remediation: `apply`). A `(present but unreadable)`144 token, or a `cat:` error in place of the contents, is the fourth state: the file exists and145 cannot be read, which is a defect the absent branch would hide. Report the read error and route146 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.1476. **Hook registration vs hook activation**. Three separate facts, never collapsed into one148 status. A registered hook set that every hook exits out of immediately is the exact state an149 operator is diagnosing when injections or gating are missing, and reporting "active" because the150 plugin is enabled tells them the opposite of the runtime state.151 - **Registered**, the plugin is enabled, so `hooks/hooks.json` is loaded and the matchers fire.152 This follows from the plugin being enabled and says nothing about what the hooks then do.153 - **Hook set armed**, the `context_guard_hooks_enabled` kill switch. Read its configured value,154 not the plugin's enablement: the value substituted here is155 `${user_config.context_guard_hooks_enabled}`. Interpret it as156 - `false` → **INERT**: registered but every hook (injection, gate, PostCompact marker) exits157 immediately without acting. Remediation: re-enable the option via `/plugin`.158 - `true` → armed.159 - anything else, including the literal `${user_config.context_guard_hooks_enabled}` surviving160 unexpanded (unset key, or a Claude Code without the substitution) → **UNKNOWN**, never161 "armed". Say which source was read and that an unset key falls back to the hooks' in-script162 default (armed); the operator-inspectable source of truth is this plugin's163 `pluginConfigs` options block in the user `settings.json` (the hook-config-delivery164 convention,165 <https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/hook-config-delivery/README.md>,166 owns why the declared `default` field is not delivered to hook processes).167 - **Gate posture**. `zone_hook_mode` is `${user_config.zone_hook_mode}`, read and interpreted168 the same way. Only `blocking` makes the PreToolUse gate do anything; `advisory` (the in-script169 default) leaves it inert while the injection hook still runs. Report it separately: an armed170 hook set with an advisory posture is a different runtime state from an inert hook set, and171 only one of the two is a defect.1727. **Print the operator edit**, except when step 3 took the terminal-less exception, found the173 status line disabled by policy or trust, or found the effective command owned by managed174 settings. Those branches already forbade printing wiring the operator cannot make run. When175 this step does print, the wiring target is the shim's fixed path, never176 `${CLAUDE_PLUGIN_ROOT}`. Compose the value by running177 `bash "${CLAUDE_PLUGIN_ROOT}/scripts/compose-statusline-wiring.sh"` over the effective178 `statusLine` value from step 3, never by peeling and wrapping the string yourself:179180 ```bash181 jq '.statusLine' <the settings file that owns the effective command> |182 bash "${CLAUDE_PLUGIN_ROOT}/scripts/compose-statusline-wiring.sh" \183 --wrap 'bash ~/.claude/context-guard/bin/statusline-shim.sh' --block --explain184 ```185186 Read [`reference/unwrap-before-compose.md`](reference/unwrap-before-compose.md) for that187 script's argument and exit-code contract and the judgments it leaves to you (shared with188 rate-limit-guard), then [`reference/statusline-edit.md`](reference/statusline-edit.md) for this189 plugin's JSON edit blocks, the combined sibling-shim invocation, and the Windows note. Composing190 by hand double-wraps a sibling tee and stacks another `sh -c` layer on every re-run.1918. **Dotfiles tracking proposal**, the printed edit changes a durable user-scope file the operator192 maintains. When the operator's home directory is managed by a dotfiles system (chezmoi, yadm, a193 bare-repo setup, ...), surface the reminder to capture the `settings.json` change through that194 system's own add/track flow so the wiring survives machine rebuilds. This skill only surfaces195 the reminder; it runs no dotfiles command.196197## `apply` (writes only inside `~/.claude/context-guard/`, on explicit request)198199Two files, both in this plugin's own operator-home directory. Every `apply` mode does both; the200`defaults` argument affects only the zones bands.201202### A. Install the statusline shim203204Copy `${CLAUDE_PLUGIN_ROOT}/scripts/statusline-shim.sh` to205`~/.claude/context-guard/bin/statusline-shim.sh`, creating `bin/` if needed, and `chmod +x` the206result (a no-op on Windows ACL volumes; the wiring invokes it through `bash` anyway):207208- The installed copy is **byte-identical** to the shipped source, never a rewrite, never a209 templated variant. That is what makes `check` step 2 a plain `cmp`.210- **Idempotent**: if the file already exists and compares equal, write nothing and say so.211 Otherwise overwrite it (this is the update path after a plugin version bump changes the shim)212 and report the `# shim-revision:` values, old → new.213- The shim is **inert until wired**: installing it starts nothing. Only the operator's214 `settings.json` edit — step 7 of `check`, which this skill never applies — puts it on the215 statusline path. Say that explicitly when reporting the write.216- After installing, print the wiring edit (`check` step 7) — honoring that step's exceptions —217 so the operator's next action is in front of them when there is one, and note that a218 statusline already wired to the shim needs no change now or on any future plugin update.219220### B. Seed or refresh the zones SSOT221222Seed or refresh `~/.claude/context-guard/zones.json` from the shipped defaults223(`smart_max_used_percentage: 50`, `acceptable_max_used_percentage: 75`, and the window-class224`token_bands`, the reader contract owns these numbers; read them from225`${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.md` rather than this file if they ever disagree):2262271. **File absent**. Create the directory if needed and write exactly:228229 ```json230 {231 "smart_max_used_percentage": 50,232 "acceptable_max_used_percentage": 75,233 "token_bands": {234 "200000": { "smart_max_tokens": 100000, "acceptable_max_tokens": 160000 },235 "1000000": { "smart_max_tokens": 200000, "acceptable_max_tokens": 400000 }236 }237 }238 ```2392402. **File present**. Behavior is mode-explicit, never ambiguous:241 - `apply` (no argument): repair-only. Valid recognized band values are left untouched and242 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 the243 shipped defaults. An absent `token_bands` is repaired by adding the shipped token bands244 (absence is valid zero-config for the resolver, but the seeded SSOT should carry the full245 tunable surface). An operator's custom-but-valid thresholds are never overwritten by a246 bare `apply`.247 - `apply defaults`: set all recognized band keys (both percentage keys and `token_bands`) to248 the shipped defaults explicitly. This converges forward to a known state; it is not teardown,249 and it never removes the file or any key it does not recognize.250 - Both modes **preserve every unrecognized key semantically** — same keys, same JSON values —251 (the file is a shared SSOT the operator's own statusline may extend). Preservation is252 value-level, not lexical: a `jq` merge reserializes the document, so formatting and escape253 spellings may normalize (`"blue"` → `"blue"`); consumers of this file must parse it as254 JSON, never depend on its raw bytes. Use `jq` to merge so the result stays valid JSON. If255 `jq` is absent while the file exists, FAIL with the jq install remediation256 (<https://jqlang.org/download/>) instead of attempting a merge, never risk clobbering the257 operator's keys with a jq-less rewrite. (Step 1's template write needs no jq.)2583. **Idempotent**, a second identical `apply` produces no content change; say so.2594. **Report exactly what was written** (old bands → new bands, unrecognized keys preserved), and260 remind that consumers re-read the file on their next zone decision. No restart needed.261262`apply` never touches `settings.json`, the snapshot directory, or anything outside263`~/.claude/context-guard/`. Statusline wiring stays print-only.264265## Uninstalling266267Uninstalling the plugin removes the cache directory, not the operator's files. Nothing breaks: the268shim finds no tee and passes the wrapped statusline through unchanged (a wired-standalone shim269prints 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:2702711. **Unwrap the `statusLine` command first**, restoring the operator's own renderer (or removing272 the field entirely if the shim was the whole statusline).2732. **Then remove `~/.claude/context-guard/`.**274275Deleting the directory while the wiring still names the shim leaves `settings.json` invoking a276missing file: `bash <missing-path>` exits 127 and takes the whole statusline down, the exact277failure the shim exists to prevent. The shim's own no-tee fallback cannot cover this, because the278fallback lives in the file that was just deleted.279280## What this skill does not do281282- Write the plugin cache, Claude Code user settings, or `pluginConfigs`, per the uniform setup283 contract (`docs/PLUGIN-PHILOSOPHY.md` "Setup is explicit and repeatable" in the marketplace284 repository). Nor `settings.json` (user or project) or any other Claude Code settings surface; the printed edit is the operator's to apply.285- Install `jq` or any system package.286- Write to the snapshot directory `~/.claude/context-guard/context/`, the tee owns those files.287- Write anywhere outside `~/.claude/context-guard/`, including the sibling `rate-limit-guard`288 directory, whose own setup skill installs that plugin's shim.