Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/context-guard/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "context-guard",
"version": "0.5.2",
"version": "0.5.3",
"description": "Per-session context-window observability plus the first shipped consumer: a statusline wrapper tees each session's context_window fields to a per-session snapshot file, a zone resolver classifies usage into smart/acceptable/dumb bands (percentage bands plus window-class token bands, conservative-min combination, zones.json SSOT with shipped defaults), a reader contract fixes how consuming sessions interpret the snapshots, and zone-crossing hooks report once per transition into a worse zone across two channels — the continuation menu to the operator, who owns that choice, and to the model only the zone determination plus the counter-steer that a zone word is not a decay signal (advisory by default; an optional blocking mode gates new mutating work on a fresh dumb-zone snapshot with handoff-writing exempt), with a PostCompact hook persisting an evidence-degraded marker.",
"author": {
"name": "Melodic Software",
Expand Down
20 changes: 20 additions & 0 deletions plugins/context-guard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to the `context-guard` plugin.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.3]

### Fixed

- **`setup check` FAILs a pre-revision-3 installed shim instead of reporting INFO.** The rule
already described the defect accurately — a copy predating `# shim-revision: 3` picks the newest
tee by mtime alone and keeps teeing from an UNINSTALLED plugin for the whole orphan window — but
still classified it INFO because the statusline keeps rendering. Rendering is not the property
that matters; what the operator is running has a behavior defect, and INFO files it under a
heading operators are told they can defer. Classification now turns on the installed revision:
`>= 3` stays INFO, below 3 or unmarked is FAIL.

**Existing installs need one `apply`.** The statusline runs the durable copy at
`~/.claude/context-guard/bin/statusline-shim.sh`, which a plugin update never overwrites, so an
operator who ran `apply` before revision 3 shipped keeps running the old shim until they re-run
it. Uninstalling first is the trap worth naming: the setup skill goes with the plugin while the
stale shim stays behind, leaving no in-product path to the remediation. Kept in step with the
identical `rate-limit-guard` change (#1866) — the two shims are a deliberate byte-identical
cluster, so their setup contracts must not drift apart.

## [0.5.2]

### Fixed
Expand Down
23 changes: 17 additions & 6 deletions plugins/context-guard/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,23 @@ zone bands, zones.json shape) are owned by
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** — INFO, not FAIL: the installed copy is an older (or hand-edited)
revision, and it still resolves a tee, so the statusline keeps rendering. Do NOT report the
difference as harmless: a copy predating `# shim-revision: 3` 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. Report the shipped `# shim-revision:` marker against the installed
one, say which of the two behaviors the installed copy has, and offer `apply` as the refresh.
- **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 offer `apply` as 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.sh` is what the statusline actually runs; a
plugin update never overwrites it. An operator who ran `apply` before revision 3 shipped
therefore keeps running the old shim until they re-run `apply` — 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.
- **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:
Expand Down
2 changes: 1 addition & 1 deletion plugins/rate-limit-guard/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "rate-limit-guard",
"version": "0.4.2",
"version": "0.4.3",
"description": "Shared rate-limit guard for loop lanes: a statusline wrapper tees the subscription rate-limit windows to a fixed machine-scope file, a StopFailure hook records rate-limit stops reactively, and a reader contract fixes how consuming sessions pause and resume.",
"author": {
"name": "Melodic Software",
Expand Down
27 changes: 27 additions & 0 deletions plugins/rate-limit-guard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
All notable changes to the `rate-limit-guard` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.4.3]

### Fixed

- **The shim no longer runs an uninstalled plugin's tee (#1849).** `claude plugin uninstall` does
not delete the version directory: the plugins reference documents that updating or uninstalling
marks the previous version directory orphaned and removes it automatically 14 days later, so the
files — `scripts/statusline-tee.sh` included — stay on disk for that whole window. `resolve_tee()`
matched on the glob and mtime alone, so a removed plugin kept teeing and kept writing snapshots
with no signal to the operator. A candidate whose version directory carries the orphan marker is
now skipped, so uninstalling stops the tee at the next statusline refresh. The marking is
documented; the marker's on-disk spelling was measured (Claude Code 2.1.220, against a relocated
`CLAUDE_CONFIG_DIR`) and the shim's header records both, along with the fallback: should upstream
rename or drop the marker, resolution degrades to exactly what it does today — a stale tee, never
a broken statusline. The undocumented `installed_plugins.json` the header previously rejected
stays rejected. Port of the context-guard fix from #1787 / PR #1844; the two shims remain
deliberately unregistered as a byte-identical cluster (plugin name and header prose differ).

**Existing installs need one `apply`.** The statusline runs the durable copy at
`~/.claude/rate-limit-guard/bin/statusline-shim.sh`, which a plugin update never overwrites, so
an operator who ran `apply` before this release keeps running the old shim — and keeps selecting
orphaned tees — until they re-run it. `setup check` previously reported any installed-vs-shipped
difference as INFO on the premise that an older revision "still resolves the newest tee"; that
premise is what this fix falsifies, so a copy below revision 3 is now a FAIL with the migration
stated in the finding. Uninstalling first is the trap worth naming: the setup skill goes with the
plugin while the stale shim stays behind, leaving no in-product path to the remediation.

## [0.4.2]

### Changed
Expand Down
48 changes: 39 additions & 9 deletions plugins/rate-limit-guard/scripts/statusline-shim.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
# dir (the documented multi-account alias), so the shim would silently take the
# no-tee path forever after they wired it.
#
# RESOLUTION: the newest installed tee by MTIME across marketplaces, which is
# the most recently installed one — deliberately not a version sort, since
# RESOLUTION: the newest NON-ORPHANED tee by MTIME across marketplaces, which
# is the most recently installed one — deliberately not a version sort, since
# version directory names sort lexically ("0.9.0" > "0.10.0") and carry no
# guarantee of being semver at all. Marketplace directories named temp_* are
# skipped: the cache holds transient temp_git_*/temp_local_* clones during
Expand All @@ -47,12 +47,39 @@
# Verified empirically 2026-07-24: the cache copy does NOT preserve the source
# file's timestamps — an installed tee carries its INSTALL time (measured: a
# source committed at 03:08 installed at 12:38 carried 12:38), so "newest
# mtime" is "most recently installed" even against an orphaned older version
# directory lingering from a previous install. An alternative authoritative
# source exists — ~/.claude/plugins/installed_plugins.json maps
# <plugin>@<marketplace> to the current installPath — but it is an UNDOCUMENTED
# internal file carrying its own schema version, and reading it would put a jq
# spawn on every statusline refresh. Revisit only if upstream documents it.
# mtime" is "most recently installed".
#
# ORPHAN SKIP — why mtime alone is not enough: "When you update or uninstall a
# plugin, the previous version directory is marked as orphaned and removed
# automatically 14 days later. The grace period lets concurrent Claude Code
# sessions that already loaded the old version keep running without errors"
# (plugins reference, "Plugin caching and file resolution",
# https://code.claude.com/docs/en/plugins-reference, fetched 2026-07-30).
# UNINSTALL therefore leaves the tee on disk for ~14 days, and an mtime-only
# shim keeps executing it for that whole window: the operator removes the
# plugin and it keeps writing snapshots, with no signal that it is still
# running. A candidate whose version directory carries the orphan marker is
# skipped, so uninstalling stops the tee at the next statusline refresh.
#
# The MARKING is documented; the marker's on-disk spelling is not. Measured on
# Claude Code 2.1.220, 2026-07-30, against a relocated CLAUDE_CONFIG_DIR: an
# uninstall writes `<version-dir>/.orphaned_at` (epoch-ms) and leaves
# scripts/statusline-tee.sh in place. Reproducible on any live cache: every
# superseded version directory of a plugin carries the marker and the currently
# installed one does not. A directory can also be marker-less while merely
# STAGED — a newer version fetched for a pending update — so the marker's
# absence is not itself a claim of installation; mtime still picks the winner
# among unmarked candidates, as it did before. If upstream renames or drops the
# marker, the test finds nothing and resolution falls back to today's
# mtime-only behavior — a stale tee, never a broken statusline.
#
# An alternative authoritative source exists — ~/.claude/plugins/
# installed_plugins.json maps <plugin>@<marketplace> to the current installPath
# — but it is an UNDOCUMENTED internal file carrying its own schema version,
# and reading it would put a jq spawn on every statusline refresh. The orphan
# marker is preferred over it on both counts: the behavior it reports is
# documented, and the test is a builtin. Revisit only if upstream documents the
# file.
#
# Pure builtins — glob + `-nt` tests, no subprocesses — because the statusline
# command runs on every session event and on the refresh interval.
Expand All @@ -61,7 +88,7 @@ set -uo pipefail

PLUGIN_NAME="rate-limit-guard"

# shim-revision: 2
# shim-revision: 3
Comment thread
kyle-sexton marked this conversation as resolved.
# Bumped whenever this file's content changes. The installed copy is a
# BYTE-IDENTICAL copy of this file, so /rate-limit-guard:setup check compares
# the two directly; the marker is for humans reading the installed copy.
Expand All @@ -86,6 +113,9 @@ resolve_tee() {
rest="${cand#"$cache"/}"
mkt="${rest%%/*}"
[[ "$mkt" == temp_* ]] && continue
# Uninstalled or superseded: the version directory is marked orphaned and
# lingers ~14 days. Running it would keep an uninstalled plugin writing.
[[ -e "${cand%/scripts/statusline-tee.sh}/.orphaned_at" ]] && continue
if [[ -z "$RESOLVED" || "$cand" -nt "$RESOLVED" ]]; then
RESOLVED="$cand"
fi
Expand Down
43 changes: 41 additions & 2 deletions plugins/rate-limit-guard/scripts/statusline-shim.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
#
# Proves: (a) RESOLUTION — the newest installed tee by mtime wins across
# version directories whose names do NOT sort lexically (0.10.0 vs 0.9.0),
# transient temp_* marketplace clones are skipped, the marketplace directory
# name is never assumed, and other plugins' trees are ignored; (b)
# transient temp_* marketplace clones are skipped, version directories marked
# orphaned by an update or an uninstall are skipped even when they are the
# newest, the marketplace directory name is never assumed, and other plugins'
# trees are ignored; (b)
# TRANSPARENCY — the wrapped statusline command receives the stdin bytes and
# its stdout and exit code pass through, whether the tee was found or not;
# (c) the NO-TEE paths — a missing tee degrades to running the wrapped command
Expand Down Expand Up @@ -80,6 +82,16 @@ EOF
printf '%s' "$dir/statusline-tee.sh"
}

# Mark a planted tee's VERSION directory the way Claude Code marks one on an
# update or an uninstall: `.orphaned_at` holding an epoch-ms timestamp
# (measured on Claude Code 2.1.220). The directory keeps its files for ~14 days
# afterwards, which is the window this marker exists to close.
# $1 = the tee path returned by plant_tee
orphan_tee() {
local tee="$1"
printf '1785448003467' >"${tee%/scripts/statusline-tee.sh}/.orphaned_at"
}

# Wrapped statusline stand-in: echoes the stdin it received, prints a fixed
# line, and exits with a chosen code.
make_wrapped() {
Expand Down Expand Up @@ -240,6 +252,33 @@ ERR="$(cat "$errfile")"
rm -f "$errfile"
assert_contains "$ERR" "TEE:home" "an empty CLAUDE_CONFIG_DIR falls back to \$HOME/.claude"

# --- 15. UNINSTALLED plugin: the orphaned tee is not executed ---------------
# `claude plugin uninstall` writes .orphaned_at into the version directory and
# leaves the files there for ~14 days. Without the marker check the shim keeps
# finding and running the removed plugin's tee for that whole window.
H8="$WORK/h8"
GONE="$(plant_tee "$H8" "melodic-software" "rate-limit-guard" "0.4.0" "uninstalled")"
orphan_tee "$GONE"
make_wrapped "$H8/render.sh" 0
run "$H8" bash "$H8/render.sh"
assert_eq "" "$ERR" "an uninstalled plugin's orphaned tee is not executed"
assert_contains "$OUT" "RENDER" "uninstalled plugin still leaves the statusline running"
assert_eq "0" "$RC" "uninstalled plugin preserves the wrapped exit code"

# --- 16. orphaned loses to an installed sibling even when it is newer -------
# The update path: the superseded directory is marked orphaned. Give it the
# newer mtime so only the marker can decide.
H9="$WORK/h9"
KEEP="$(plant_tee "$H9" "mkt" "rate-limit-guard" "0.4.0" "installed")"
DEAD="$(plant_tee "$H9" "mkt" "rate-limit-guard" "0.3.9" "orphaned")"
orphan_tee "$DEAD"
touch -t 202001010000 "$KEEP"
touch -t 203001010000 "$DEAD"
make_wrapped "$H9/render.sh" 0
run "$H9" bash "$H9/render.sh"
assert_contains "$ERR" "TEE:installed" "orphaned version directory skipped even when newest by mtime"
assert_not_contains "$ERR" "TEE:orphaned" "the orphaned tee does not also run"

echo
echo "passed: $PASS failed: $FAIL"
((FAIL == 0))
20 changes: 17 additions & 3 deletions plugins/rate-limit-guard/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,23 @@ owned by `${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.md`.
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** — INFO, not FAIL: the installed copy is an older (or hand-edited)
revision that still resolves the newest tee. Report the shipped `# shim-revision:` marker
against the installed one and offer `apply` as the refresh.
- **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, and offer `apply` as 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. Revision 3 is the first that skips a
candidate whose version directory carries the orphan marker; every earlier revision keeps
teeing from an UNINSTALLED plugin's directory for the ~14 days before Claude Code reaps it,
writing snapshots the operator has no reason to expect. That is a behavior defect in the
running statusline, not drift, and INFO would leave it sitting under a heading operators
are told they can defer.
- **The migration matters more than the classification.** The durable copy at
`~/.claude/rate-limit-guard/bin/statusline-shim.sh` is what the statusline actually runs;
Comment thread
kyle-sexton marked this conversation as resolved.
a plugin update never overwrites it. An operator who ran `apply` before 0.4.3 therefore
keeps running the old shim until they re-run `apply` — 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.
- **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:
Expand Down