agentleFS
aquahittaquahitt/claude-code-limit-alerts/.claude/CLAUDE.md

claude-code-limit-alerts

CLAUDE.md1 starsChanged 4 months agoMITView on GitHub

Pure bash tooling for macOS: monitors Claude Code subscription usage limits and sends notifications. No framework, test runner, or build step — the whole project lives in shell scripts installed into ~/.claude/. - .claude/ (this file, agents, skills, hooks) — English. This is operational documentation for Claude Code itself; English gives the best model performance. - docs/ (user-facing documentation, e.g. docs/how-it-works.md) — the project's user-facing languages, currently RU + EN (mirrors README.md/README.en.md). Keep both in sync when either changes. -…

  • Installs packages

The file

We split this CLAUDE.md into the rules it gives an agent

1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Never add a new shared usage file without the same tag.

    line 113

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

claude-code-limit-alerts

Pure bash tooling for macOS: monitors Claude Code subscription usage limits and sends notifications. No framework, test runner, or build step — the whole project lives in shell scripts installed into ~/.claude/.

Documentation language

  • .claude/ (this file, agents, skills, hooks) — English. This is operational documentation for Claude Code itself; English gives the best model performance.
  • docs/ (user-facing documentation, e.g. docs/how-it-works.md) — the project's user-facing languages, currently RU + EN (mirrors README.md/README.en.md). Keep both in sync when either changes.

Structure

  • install.sh / uninstall.sh / update.sh — manage files in ~/.claude/scripts/, hooks in ~/.claude/settings.json, and the launchd agent (~/Library/LaunchAgents/com.claude.usage-monitor.plist).
  • lib/hooks.sh — shared hook-registration logic (add_hook, register_monitor_hooks, register_attention_hooks), sourced by install.sh and update.sh; never copied into ~/.claude/scripts. It does ship inside the plugin, though — source: "./" puts the whole repository, lib/ included, into the plugin cache.
  • lib/launchd.sh — shared launchd plist generation (generate_plist, print_proxy_status), including proxy-env passthrough for corporate proxy/VPN setups; sourced by install.sh and update.sh the same way as lib/hooks.sh.
  • scripts/usage-monitor.sh, scripts/notify-attention.sh, scripts/statusline-with-limits.sh, scripts/compact-advisor.sh — the files actually copied into ~/.claude/scripts/ and run by hooks/launchd/statusline. compact-advisor.sh is a Stop hook; fanout-guard.sh is a PreToolUse hook (matcher Agent|Task), off by default — install.sh installs it only with --guard-fanout.
  • Don't reintroduce auto-resume. 0.5.0 removed auto-resume.sh because Claude Code does it natively ("Continue automatically at usage limit" in /config, autoContinueAtUsageLimit in ~/.claude/settings.json). An external worker needs the session exited first and, alongside the built-in one, resumes one conversation twice. This project warns about limits; it does not manage sessions.
  • launchd/com.claude.usage-monitor.plist.template — plist template. __LABEL__, __SCRIPT__, __ERRLOG__, __INTERVAL__ (and legacy __HOME__) are substituted by generate_plist, whose 5th-8th parameters are optional — empty means the classic install's label, script and error log, and a 92-second interval. The interval is always passed explicitly (--interval / the interval option), never carried over from an older plist: that cannot tell a user's choice from an old version's default.
  • docs/how-it-works.md — data source (/api/oauth/usage endpoint, Keychain), hook logic, and anti-spam rules.
  • docs/superpowers/plans/, docs/superpowers/specs/ — plans and specs left behind by the superpowers:writing-plans / superpowers:brainstorming skills. Local working drafts, gitignored — not committed to the repository.

Plugin surface

The repository root is the plugin (source: "./"), so scripts/ is reused from the same files with no copy.

  • .claude-plugin/plugin.json — manifest and userConfig; .claude-plugin/marketplace.json — single-entry catalogue.
  • hooks/hooks.json — Stop / SessionStart / Notification / PreToolUse. Every command passes UM_STATE_DIR="${CLAUDE_PLUGIN_DATA}" explicitly.
  • skills/{status,setup,uninstall,doctor}/SKILL.md — thin triggers; logic stays in bash. (.claude/skills/ is a different thing entirely — those are development skills for this repository and are not shipped to plugin users.)
  • scripts/plugin-bootstrap.sh — SessionStart hook.
  • The one constraint to remember: ${CLAUDE_PLUGIN_ROOT} points into a version-stamped cache, so any path written outside the plugin (the statusline command, the launchd plist) must go through ${CLAUDE_PLUGIN_DATA}/bin/ instead. The bootstrap re-copies those scripts on a version change, but rewrites the generated wrappers on every session start — they carry the resolved options, which change whenever the user edits the configuration.
  • A bootstrap older than bin/.version must not touch anything. A session keeps the hooks of the version it started with and re-runs its bootstrap on resume, /clear and compaction; letting it copy or rewrite made old and new sessions flip the mirror and the plist back and forth. The STALE guard skips mirroring, wrappers and every "apply" step; only removal (an option set to false) still runs.
  • statusline and launchd are tri-state, not boolean: true applies, false removes, unset is a no-op. The skills run the bootstrap from a plain shell where CLAUDE_PLUGIN_OPTION_* is absent, and defaulting to false there would destroy the statusline the user just enabled. Options resolve environment-first, then from pluginConfigs in ~/.claude/settings.json.
  • Never add CLAUDE_PLUGIN_DATA to the state-directory chain. It is exported by whichever plugin owns the running hook and leaks into unrelated shells; a classic install would silently relocate its state into another plugin's data directory. UM_STATE_DIR is the only knob.
  • userConfig defaults are never materialised into the environment — an unset CLAUDE_PLUGIN_OPTION_* is the normal case, so every default is duplicated in bash. Booleans arrive as the literal strings true / false.
  • The statusline's stdin rate_limits freeze while a session idles — Claude Code only refreshes them from its own API responses. That is why each window takes the fresher of stdin and the account-wide cache (pick_window: later reset wins, same window → higher percent; usage only grows within a window), and why the statusline starts a detached usage-monitor.sh hook when the cache is stale. Never go back to "stdin always wins". Sessions share their freshest stdin reading through usage-monitor-observed (mkdir lock + retry), and a live request runs under usage-monitor.fetch.lock so concurrent callers share one request.
  • Usage data is per account. The key is accountUuid:organizationUuid (the user switches accounts; one person can hold a personal plan and a Team seat). usage-monitor.sh takes it from the Keychain token itself — one /api/oauth/profile request per token, remembered by token hash in usage-monitor-accounts.json — and falls back to oauthAccount in ~/.claude.json; the statusline, fan-out guard and mod read ~/.claude.json only. State and backoff are one file per account (*.<key>.json, key = first 8 chars of each); the cache and usage-monitor-observed carry the account and are ignored for any other. An untagged cache is only a no-sign-in install's, and an unparsable ~/.claude.json means "account unknown" — write nothing, trust no cache. Never add a new shared usage file without the same tag.
  • CLAUDE_CONFIG_DIR moves Claude Code's files, so ours follow. Every script reads ${CLAUDE_CONFIG_DIR:-$HOME}/.claude.json (a non-empty <config dir>/.config.json wins), ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/… for settings, sessions and tasks, and the Keychain entry Claude Code-credentials-<sha256(dir)[0:8]> — the variable as given, not resolved, exactly as Claude Code computes it. Never hard-code $HOME/.claude.json or ~/.claude/settings.json in a hook-side script. The launchd agent is the exception: one per macOS user, owned by the default configuration's plugin. A bootstrap under another CLAUDE_CONFIG_DIR leaves it alone (it only removes a plist pointing at its own data) — otherwise two installs rewrote the plist on every session start. install.sh and its state stay on ~/.claude.
  • Every hook script exits at once on UM_INTERNAL=1. usage-monitor.sh sets it on its headless claude -p /usage refresh, which fires this project's own hooks; without the early exit each refresh made one more request to the usage endpoint. A new hook script needs the same line.
  • CLAUDE_PLUGIN_OPTION_* leak just like CLAUDE_PLUGIN_DATA — the names are generic, not per plugin. A script reads them only behind the FROM_PLUGIN check (${BASH_SOURCE[0]} under ${CLAUDE_PLUGIN_ROOT}), so a classic install never sees them. In pluginConfigs lookups, test with has(), never jq's //: // treats a stored false as missing.
  • The plugin's launchd label is com.claude.usage-monitor.plugin, deliberately distinct from the classic one, and UM_NO_LAUNCHCTL=1 makes the bootstrap generate the plist without touching launchd (which ignores HOME, so this is the only way to verify that path safely).

Experimental mod

  • mods/limit-alerts-status/ — a separate plugin of TypeScript function hooks (a Claude Code "mod"), not part of the limit-alerts plugin and not listed in marketplace.json. It registers one command, /limits (immediate), answered by a command.run hook with { text } — no model turn, no tokens. 5h/7d take, per window, the fresher of $.session.usage().rateLimits and the monitor's cache (freshest, the same rule as the statusline's pick_window; same directory chain as the bash scripts, then ~/.claude/plugins/data/limit-alerts-*); the scoped weekly limit always from the cache; the 429 backoff from usage-monitor-backoff.json. Local time comes from the host's date +%z — the worker's own zone is not to be trusted.
  • It used to pin a permanent $.ui.status row. Dropped on purpose: the engine styles that row as a notice (yellow ▲, plugin-name prefix, no colour API), and since 0.7.0 the statusline shows the same live numbers in colour. Don't bring the row back as a duplicate.
  • The mod API is early access and changes between Claude Code releases — that is why it stays out of the shipped plugin. Load it with claude --plugin-dir mods/limit-alerts-status; check it with claude plugin validate and claude plugin test on that folder. .claude-plugin/types/ inside it is generated by the engine and gitignored.
  • It is the one place in the repository that is not bash; the "no test framework" rule below covers the shell scripts, the mod has its own *.test.ts.

Conventions

  • macOS only. Scripts may rely on launchctl, security, osascript, afplay. Linux/Windows are on the roadmap but not supported yet — don't add cross-platform shims unless that's the actual task.
  • set -euo pipefail at the top of every executable script — never remove it.
  • jq is required — resolved via resolve_jq() in lib/hooks.sh; if missing, a script must fail with a clear message (jq is required. Install it with: brew install jq), not continue silently.
  • install/update idempotency. install.sh, update.sh, and add_hook() must be safe to re-run — no duplicate hooks in settings.json, no duplicate launchd registrations. update.sh respects the flags used at install time (--no-statusline / --no-launchd / --no-attention) — don't enable something that wasn't installed.
  • The user's ~/.claude/settings.json is someone else's file. Any edit from install.sh must be preceded by a backup; existing hooks and statusline are preserved, never overwritten.
  • No test framework. Verification is manual, with real commands and expected output, plus bash -n for syntax. When manually verifying steps that touch ~/.claude/settings.json or the real launchd agent, do it with an overridden HOME (sandboxed), never against the user's real environment.
  • Versioning: VERSION (repo root) is the source of truth, CHANGELOG.md follows Keep a Changelog
    • SemVer. Every behavioral change in install.sh/uninstall.sh/update.sh/lib//scripts/ gets its own CHANGELOG.md entry and, if warranted, a VERSION bump. See the release skill.
  • README is bilingual. README.md (Russian, primary) and README.en.md (English) must stay structurally in sync — same sections, same order. See the sync-readmes skill.
  • Commits: conventional-style prefixes (feat:, fix:, refactor:, chore:, docs:) — matches the existing project history.

Before committing

Skill check-scripts — bash -n (+ shellcheck, if installed) over every changed *.sh.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.