claude-usage-limits 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ # Where the numbers come from
2
+
3
+ Everything is read from local files. Nothing is sent anywhere, and no API key
4
+ or token is read.
5
+
6
+ ## Sources
7
+
8
+ ### The limits
9
+
10
+ `~/.claude.json`, key `cachedUsageUtilization`. Claude Code refreshes this
11
+ during normal use, so it is usually a few minutes old at most. The shape:
12
+
13
+ ```json
14
+ {
15
+ "fetchedAtMs": 1787522765060,
16
+ "utilization": {
17
+ "five_hour": { "utilization": 2, "resets_at": "2026-08-24T02:59:59Z" },
18
+ "seven_day": { "utilization": 75, "resets_at": "2026-08-23T22:59:59Z" },
19
+ "extra_usage": { "is_enabled": false }
20
+ }
21
+ }
22
+ ```
23
+
24
+ `utilization` is a whole-number percentage. `resets_at` is when that window
25
+ rolls over. Some plans also carry `seven_day_opus` and `seven_day_sonnet`;
26
+ the report includes them when they are present.
27
+
28
+ If the key is missing, run `/usage` once inside Claude Code. That is what
29
+ populates it.
30
+
31
+ The plan name comes from `oauthAccount.organizationType` in the same file.
32
+ Current effort and model come from `settings.json` in the config directory.
33
+
34
+ `CLAUDE_CONFIG_DIR` is honoured if set.
35
+
36
+ Every surface writes here: the terminal CLI, the VS Code and JetBrains
37
+ extensions, and the desktop app all share one config directory, and their
38
+ sessions are counted together. Entries carry an `entrypoint` field (`cli`,
39
+ `claude-vscode`) if you want to tell them apart, but the report does not
40
+ filter on it.
41
+
42
+ ### The pace
43
+
44
+ `~/.claude/projects/<project>/<session>.jsonl`. One JSON object per line.
45
+ Assistant turns carry a usage record:
46
+
47
+ ```json
48
+ {
49
+ "type": "assistant",
50
+ "timestamp": "2026-08-23T22:04:56.858Z",
51
+ "requestId": "req_011...",
52
+ "effort": "xhigh",
53
+ "message": {
54
+ "id": "msg_01...",
55
+ "model": "claude-opus-5",
56
+ "usage": {
57
+ "input_tokens": 2,
58
+ "cache_creation_input_tokens": 8049,
59
+ "cache_read_input_tokens": 24780,
60
+ "output_tokens": 2144,
61
+ "cache_creation": { "ephemeral_1h_input_tokens": 8049 }
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ Files whose modification time predates the window are skipped. Turns are keyed
68
+ by `message.id` plus `requestId` and counted once, because resuming or forking
69
+ a session copies earlier turns into a new file.
70
+
71
+ ## The arithmetic
72
+
73
+ Each turn is priced at published API rates. Cache traffic is a multiple of the
74
+ input rate: 1.25x for a five-minute write, 2x for a one-hour write, 0.1x for a
75
+ read.
76
+
77
+ ```
78
+ turn = (input + 0.1*cache_read + 1.25*write5m + 2*write1h) * input_rate
79
+ + output * output_rate
80
+
81
+ spent = sum of turns inside the window
82
+ per_percent = spent / utilization
83
+ left = per_percent * (100 - utilization)
84
+ turns_left = (100 - utilization) / (recent_cost_per_turn / per_percent)
85
+ headroom = (100 - utilization) / (recent_dollars_per_hour / per_percent)
86
+ ```
87
+
88
+ The window opens at `resets_at` minus its span: five hours, or seven days.
89
+ Recent pace is measured over the last hour, or since the window opened if that
90
+ is more recent.
91
+
92
+ The self-calibration is the point. Nobody outside Anthropic knows what a
93
+ subscription limit is worth in tokens, and it differs by plan. But if 75
94
+ percent of the week corresponds to a measurable amount of local traffic, the
95
+ remaining 25 percent is worth a quarter of that. The absolute dollar figures
96
+ are an internal unit for that ratio. On a subscription plan you are not billed
97
+ them, and they should not be read as a bill.
98
+
99
+ ## Where it is soft
100
+
101
+ **Whole percent granularity.** The meter reports integers, so 2 percent is
102
+ really somewhere in 1.5 to 2.5. At low readings the projection can be off by
103
+ a quarter or more in either direction. The report flags this below 5 percent.
104
+ Above about 20 percent it tightens up considerably.
105
+
106
+ **One machine only.** Transcripts are local. Usage from another machine, from
107
+ claude.ai, or from a cloud session counts against the same limit but leaves no
108
+ local record. The percentages stay correct; the calibration reads low, which
109
+ makes the remaining headroom look smaller than it is.
110
+
111
+ **Deleted transcripts.** Same effect. Old session files get cleaned up, and
112
+ anything cleaned up inside the seven-day window is invisible to the pace
113
+ calculation.
114
+
115
+ **List prices are a proxy.** The rate table is first-party API pricing. How a
116
+ subscription plan actually meters usage is not published, and the weighting
117
+ almost certainly is not exactly this. It is close enough for ratios, which is
118
+ all it is used for.
119
+
120
+ **A reset time can be in the past.** The cache refreshes when Claude Code
121
+ talks to the API, so an idle spell leaves it behind. A window whose `resets_at`
122
+ has passed has already turned over, and its cached percentage describes a
123
+ window that no longer exists. Those are marked stale, excluded from the
124
+ binding choice, and never used for projections, because treating one as
125
+ current would report an empty budget at the exact moment the budget came back.
126
+
127
+ **Pace is not a promise.** Turns left assumes the next turns look like the last
128
+ hour's. A debugging spiral or a large file read breaks that assumption
129
+ immediately. Re-run the report if the shape of the work changes.
130
+
131
+ ## Keeping it accurate
132
+
133
+ The rate table in `scripts/usage.js` is a plain object at the top of the file.
134
+ When new models ship, add a row. An unknown id falls back to the family it
135
+ names (`opus`, `sonnet`, `haiku`, `fable`) and then to Opus rates, so a missing
136
+ row degrades to an estimate rather than a crash.
@@ -0,0 +1,170 @@
1
+ # Spending less per turn
2
+
3
+ Everything here works by shrinking one of three quantities: the tokens you
4
+ resend, the tokens you generate, or the number of times you do either.
5
+
6
+ ## The part people get wrong
7
+
8
+ A conversation is stateless. Every turn resends the entire conversation so
9
+ far. Nothing is stored server-side between requests, so a file you read on
10
+ turn 5 is re-sent on turns 6 through 60.
11
+
12
+ That resend is cheap per token but not free. Cached prefix tokens bill at a
13
+ tenth of the input rate, fresh input at the full rate, cache writes at 1.25x
14
+ (five-minute) or 2x (one-hour), and output at five times the input rate. On
15
+ Opus rates, a 100k-token context costs roughly five cents a turn just to be
16
+ re-read, before the model generates anything.
17
+
18
+ Two consequences follow, and they drive most of the list below:
19
+
20
+ - **Context length is a recurring tax, not a one-off cost.** Dumping a large
21
+ file into context on turn 5 is not one expensive turn, it is a surcharge on
22
+ every turn after it.
23
+ - **Output is the expensive direction.** Reasoning tokens are output tokens.
24
+ A turn that thinks for 4,000 tokens and edits one line costs more than a
25
+ turn that reads 20,000 tokens of cached context.
26
+
27
+ ## Levers, largest first
28
+
29
+ ### 1. Drop the effort level
30
+
31
+ Effort controls how much the model reasons, and reasoning is output. Going
32
+ from `xhigh` to `low` cuts the expensive half of the turn several times over.
33
+ This is the biggest saving available that does not change what gets built.
34
+
35
+ ```
36
+ node scripts/lowpower.js on # writes effortLevel, remembers the old one
37
+ /effort low # same change, applied to the running session
38
+ ```
39
+
40
+ Keep high effort for the decisions that are hard to undo: schema changes,
41
+ migrations, anything touching auth. Mechanical work does not need it.
42
+
43
+ ### 2. Move mechanical work to a cheaper model
44
+
45
+ Per million tokens, input and output:
46
+
47
+ | Model | Input | Output |
48
+ | --- | --- | --- |
49
+ | Opus 5 | $5 | $25 |
50
+ | Sonnet 5 | $3 | $15 |
51
+ | Haiku 4.5 | $1 | $5 |
52
+
53
+ Renaming symbols, writing boilerplate tests, formatting, mechanical
54
+ translation between two known formats: none of that needs the top model.
55
+ Switch at a task boundary rather than mid-task, because a model switch
56
+ invalidates the prompt cache and the rebuild can cost more than the saving on
57
+ a short remaining task.
58
+
59
+ ### 3. Read less, and read it once
60
+
61
+ - Line ranges instead of whole files. Grep with a result limit instead of
62
+ opening candidates one by one.
63
+ - Pipe noisy commands through `head`, `tail`, or `grep`. A full `npm test`
64
+ dump or an untrimmed `git log` lands in context and stays there.
65
+ - Never re-read a file to confirm an edit applied. The edit already failed
66
+ loudly if it did not.
67
+ - Prefer `git diff --stat` before `git diff`. Usually the stat is the answer.
68
+
69
+ ### 4. Cut the tool surface
70
+
71
+ Every connected MCP server's tool definitions sit in the system prompt of
72
+ every single request. A handful of large servers can add tens of thousands of
73
+ tokens to the prefix, on every turn, whether or not you use them. Disconnect
74
+ the ones this project does not need with `/mcp` or `claude mcp`.
75
+
76
+ `DISABLE_BUNDLED_SKILLS=1` trims the bundled skill catalogue for the same
77
+ reason, if none of them are in use.
78
+
79
+ ### 5. Protect the cache prefix
80
+
81
+ Cache reads are ten times cheaper than fresh input, and the cache matches on
82
+ an exact prefix. Any byte that changes early invalidates everything after it.
83
+ Things that invalidate it mid-session:
84
+
85
+ - switching models
86
+ - editing `CLAUDE.md` or project settings
87
+ - connecting or disconnecting an MCP server
88
+ - changing the tool set
89
+
90
+ None of these are forbidden. Just do them at a session boundary instead of in
91
+ the middle of a long run.
92
+
93
+ ### 6. Batch tool calls
94
+
95
+ Independent calls belong in one message. Three greps in one turn cost one
96
+ context resend. Three greps in three turns cost three, plus three rounds of
97
+ reasoning and narration.
98
+
99
+ ### 7. Skip subagents when the context already exists
100
+
101
+ A subagent starts with an empty context and re-derives what the main session
102
+ already knows. That is worth paying for genuine fan-out across more material
103
+ than one context can hold. It is not worth paying to answer a question the
104
+ main session could answer directly.
105
+
106
+ ### 8. Stop failing loops early
107
+
108
+ The single most expensive pattern is retrying a fix that does not work.
109
+ Three blind attempts cost more than one turn spent reading the actual error.
110
+ If the second attempt fails for a new reason, stop and re-read.
111
+
112
+ ### 9. End sessions at task boundaries
113
+
114
+ Compaction reads the whole conversation and writes a summary, which is a
115
+ full-price pass over everything. It is worth it compared to dragging a bloated
116
+ context through another twenty turns, but a fresh session started from a short
117
+ handoff note is cheaper than either.
118
+
119
+ Write the handoff, exit, start clean.
120
+
121
+ ### 10. Specify the work properly the first time
122
+
123
+ Underspecifying is the most expensive habit in the list, and it looks like
124
+ saving. A vague instruction that produces the wrong thing costs the build, the
125
+ review, the revert, and the rebuild. Two hundred tokens of precise
126
+ instructions routinely save several thousand.
127
+
128
+ ## Environment knobs
129
+
130
+ | Variable | Effect |
131
+ | --- | --- |
132
+ | `MAX_THINKING_TOKENS` | Hard ceiling on reasoning per turn. |
133
+ | `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | Hard ceiling on response length. Too low truncates mid-answer. |
134
+ | `DISABLE_BUNDLED_SKILLS` | Drops the bundled skill catalogue from the prompt. |
135
+ | `DISABLE_NONESSENTIAL_TRAFFIC` | Suppresses background requests. Small effect. |
136
+
137
+ Ceilings are blunt. Effort level gets you most of the same saving while
138
+ letting the model still finish its sentence.
139
+
140
+ ## What does not save money
141
+
142
+ **Clearing between related tasks.** A fresh session pays a fresh cache write
143
+ and re-reads the same files. Clear at real boundaries, not every few turns.
144
+
145
+ **Terse prompts.** See lever 10. The tokens saved on the instruction come back
146
+ multiplied in rework.
147
+
148
+ **Turning thinking off entirely.** On Opus 5 this has known failure modes: the
149
+ model can write a tool call into its visible text instead of actually calling
150
+ the tool, which fails silently and pollutes later turns. Lower the effort level
151
+ instead. It is cheaper *and* it still works.
152
+
153
+ **Asking for shorter answers when the cost is elsewhere.** If the spend is in
154
+ tool results, prose length is rounding error. Check where it actually went
155
+ before optimising the visible part.
156
+
157
+ ## Rough arithmetic
158
+
159
+ Illustrative, not measured. A 60-turn session with about 80k tokens of cached
160
+ context, on Opus rates:
161
+
162
+ | | Per turn | Over 60 turns |
163
+ | --- | --- | --- |
164
+ | Re-reading cached context | $0.04 | $2.40 |
165
+ | Output at high effort, ~2,500 tokens | $0.063 | $3.75 |
166
+ | Output at low effort, ~700 tokens | $0.018 | $1.05 |
167
+
168
+ Same work, same context, roughly 44 percent cheaper. Halving the context on
169
+ top of that takes another $1.20 off. Neither change removes a single feature
170
+ from what gets built.
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Turns the expensive knobs down and remembers what they were.
5
+ //
6
+ // effortLevel is the setting behind the /effort picker. It drives how much
7
+ // reasoning the model does per turn, and reasoning is billed as output
8
+ // tokens, which are the priciest tokens in the request. Dropping xhigh to
9
+ // low is the single largest per-turn saving available without changing
10
+ // model or scope.
11
+ //
12
+ // node scripts/lowpower.js status
13
+ // node scripts/lowpower.js on
14
+ // node scripts/lowpower.js on --effort medium --model sonnet
15
+ // node scripts/lowpower.js off
16
+ //
17
+ // The saved values live in usage-limits-lowpower.json inside the config
18
+ // directory, so "off" puts back exactly what was there, including keys
19
+ // that were not set in the first place.
20
+
21
+ const fs = require('fs');
22
+ const os = require('os');
23
+ const path = require('path');
24
+
25
+ const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
26
+ const MANAGED_KEYS = ['effortLevel', 'model'];
27
+ const DEFAULTS = { effortLevel: 'low' };
28
+
29
+ function configDir() {
30
+ return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
31
+ }
32
+
33
+ function settingsFile() {
34
+ return path.join(configDir(), 'settings.json');
35
+ }
36
+
37
+ function stateFile() {
38
+ return path.join(configDir(), 'usage-limits-lowpower.json');
39
+ }
40
+
41
+ function readJson(file) {
42
+ try {
43
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
44
+ } catch (err) {
45
+ if (err.code === 'ENOENT') return null;
46
+ throw err;
47
+ }
48
+ }
49
+
50
+ // Replace through a temporary file so an interrupted run cannot leave
51
+ // settings.json half written.
52
+ function writeJson(file, value) {
53
+ fs.mkdirSync(path.dirname(file), { recursive: true });
54
+ const temp = file + '.usage-limits-tmp';
55
+ fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n', 'utf8');
56
+ fs.renameSync(temp, file);
57
+ }
58
+
59
+ function parseArgs(argv) {
60
+ const args = { command: null, effort: null, model: null, dryRun: false };
61
+ for (let i = 0; i < argv.length; i += 1) {
62
+ const arg = argv[i];
63
+ if (arg === '--dry-run') args.dryRun = true;
64
+ else if (arg === '--effort') args.effort = argv[++i];
65
+ else if (arg === '--model') args.model = argv[++i];
66
+ else if (arg.startsWith('--effort=')) args.effort = arg.slice('--effort='.length);
67
+ else if (arg.startsWith('--model=')) args.model = arg.slice('--model='.length);
68
+ else if (!args.command) args.command = arg;
69
+ }
70
+ if (!args.command) args.command = 'status';
71
+ return args;
72
+ }
73
+
74
+ // Work out the new settings without touching disk. Returns the object to
75
+ // write, the state to remember, and a readable list of what moved.
76
+ function planApply(settings, options, existingState) {
77
+ const current = settings || {};
78
+ const wanted = {};
79
+ const effort = options.effort || DEFAULTS.effortLevel;
80
+
81
+ if (EFFORT_LEVELS.indexOf(effort) === -1) {
82
+ throw new Error('unknown effort "' + effort + '", expected one of ' + EFFORT_LEVELS.join(', '));
83
+ }
84
+ wanted.effortLevel = effort;
85
+ if (options.model) wanted.model = options.model;
86
+
87
+ // A second "on" must not record the already-lowered values as the
88
+ // originals, or "off" would restore low power forever.
89
+ const saved =
90
+ existingState && existingState.previous ? existingState.previous : capture(current);
91
+
92
+ const next = Object.assign({}, current, wanted);
93
+ const changes = [];
94
+ for (const key of Object.keys(wanted)) {
95
+ const before = current[key] === undefined ? '(unset)' : current[key];
96
+ if (String(before) !== String(wanted[key])) {
97
+ changes.push(key + ': ' + before + ' -> ' + wanted[key]);
98
+ }
99
+ }
100
+
101
+ return {
102
+ settings: next,
103
+ state: { savedAt: new Date().toISOString(), previous: saved, applied: wanted },
104
+ changes,
105
+ };
106
+ }
107
+
108
+ // Snapshot only the keys this tool is allowed to touch. A key that was not
109
+ // present is recorded as null so it can be removed again on restore.
110
+ function capture(settings) {
111
+ const previous = {};
112
+ for (const key of MANAGED_KEYS) {
113
+ previous[key] = Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : null;
114
+ }
115
+ return previous;
116
+ }
117
+
118
+ function planRestore(settings, state) {
119
+ if (!state || !state.previous) {
120
+ return { settings: settings || {}, changes: [], restored: false };
121
+ }
122
+ const next = Object.assign({}, settings || {});
123
+ const changes = [];
124
+
125
+ for (const key of MANAGED_KEYS) {
126
+ const before = next[key] === undefined ? '(unset)' : next[key];
127
+ const target = state.previous[key];
128
+ if (target === null || target === undefined) {
129
+ if (Object.prototype.hasOwnProperty.call(next, key)) {
130
+ delete next[key];
131
+ changes.push(key + ': ' + before + ' -> (unset)');
132
+ }
133
+ } else if (String(before) !== String(target)) {
134
+ next[key] = target;
135
+ changes.push(key + ': ' + before + ' -> ' + target);
136
+ }
137
+ }
138
+
139
+ return { settings: next, changes, restored: true };
140
+ }
141
+
142
+ function describe(settings, state) {
143
+ const lines = [];
144
+ const current = settings || {};
145
+ lines.push('Low power ' + (state ? 'on since ' + state.savedAt : 'off'));
146
+ lines.push('effortLevel ' + (current.effortLevel || '(unset, defaults to xhigh)'));
147
+ lines.push('model ' + (current.model || '(unset)'));
148
+ if (state && state.previous) {
149
+ const previous = state.previous;
150
+ const parts = MANAGED_KEYS.map(
151
+ (key) => key + '=' + (previous[key] === null ? '(unset)' : previous[key])
152
+ );
153
+ lines.push('will restore ' + parts.join(' '));
154
+ }
155
+ return lines.join('\n');
156
+ }
157
+
158
+ function main(argv) {
159
+ const args = parseArgs(argv);
160
+ const file = settingsFile();
161
+ const settings = readJson(file) || {};
162
+ const state = readJson(stateFile());
163
+
164
+ if (args.command === 'status') {
165
+ process.stdout.write(describe(settings, state) + '\n');
166
+ return 0;
167
+ }
168
+
169
+ if (args.command === 'on') {
170
+ const plan = planApply(settings, args, state);
171
+ if (!plan.changes.length && state) {
172
+ process.stdout.write('Already in low power. Nothing to change.\n');
173
+ return 0;
174
+ }
175
+ if (args.dryRun) {
176
+ process.stdout.write('Would change:\n ' + (plan.changes.join('\n ') || '(nothing)') + '\n');
177
+ return 0;
178
+ }
179
+ if (!state && fs.existsSync(file)) fs.copyFileSync(file, file + '.usage-limits-backup');
180
+ writeJson(file, plan.settings);
181
+ writeJson(stateFile(), plan.state);
182
+ process.stdout.write(
183
+ 'Low power on.\n ' + (plan.changes.join('\n ') || '(nothing to change)') + '\n' +
184
+ 'Applies to new sessions. For the session you are in, run /effort ' +
185
+ plan.state.applied.effortLevel + '.\n'
186
+ );
187
+ return 0;
188
+ }
189
+
190
+ if (args.command === 'off') {
191
+ if (!state) {
192
+ process.stdout.write('Low power is not on. Nothing to restore.\n');
193
+ return 0;
194
+ }
195
+ const plan = planRestore(settings, state);
196
+ if (args.dryRun) {
197
+ process.stdout.write('Would restore:\n ' + (plan.changes.join('\n ') || '(nothing)') + '\n');
198
+ return 0;
199
+ }
200
+ writeJson(file, plan.settings);
201
+ fs.unlinkSync(stateFile());
202
+ process.stdout.write(
203
+ 'Low power off.\n ' + (plan.changes.join('\n ') || '(nothing to change)') + '\n'
204
+ );
205
+ return 0;
206
+ }
207
+
208
+ process.stderr.write('usage: lowpower.js [status|on|off] [--effort level] [--model name] [--dry-run]\n');
209
+ return 1;
210
+ }
211
+
212
+ if (require.main === module) {
213
+ try {
214
+ process.exitCode = main(process.argv.slice(2));
215
+ } catch (err) {
216
+ process.stderr.write('lowpower: ' + (err && err.message ? err.message : String(err)) + '\n');
217
+ process.exitCode = 1;
218
+ }
219
+ }
220
+
221
+ module.exports = {
222
+ main,
223
+ EFFORT_LEVELS,
224
+ MANAGED_KEYS,
225
+ parseArgs,
226
+ capture,
227
+ planApply,
228
+ planRestore,
229
+ describe,
230
+ };