@rex_koh/subagent-budget-guard 0.1.3 → 0.1.4
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -5
- package/bin/setup.js +7 -0
- package/lib/guard.js +56 -2
- package/lib/verifier.js +51 -1
- package/package.json +1 -1
- package/skills/setup/SKILL.md +13 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "subagent-budget-guard",
|
|
3
3
|
"displayName": "Subagent Budget Guard",
|
|
4
4
|
"description": "Hard-deny subagent launches, record verified subagent usage, and enforce a session budget against Claude Code's 5-hour rate-limit percentage.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.4",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ClaudeSubAgentSuppressor"
|
|
8
8
|
},
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Subagent Budget Guard
|
|
2
2
|
|
|
3
|
-
Claude Code plugin that blocks subagents
|
|
3
|
+
Claude Code plugin that blocks subagents before setup, records verified subagent usage, and enforces a session budget against Claude Code's 5-hour usage percentage.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -10,13 +10,14 @@ Recommended Claude Code install:
|
|
|
10
10
|
/plugin marketplace add rexkoh425/ClaudeSubAgentSuppressor
|
|
11
11
|
/plugin install subagent-budget-guard@subagent-budget-tools
|
|
12
12
|
/reload-plugins
|
|
13
|
+
/subagent-budget-guard:setup
|
|
14
|
+
/reload-plugins
|
|
15
|
+
/subagent-budget-guard:verify
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
Useful after install:
|
|
16
19
|
|
|
17
20
|
```text
|
|
18
|
-
/subagent-budget-guard:setup
|
|
19
|
-
/subagent-budget-guard:verify
|
|
20
21
|
/subagent-budget-guard:report
|
|
21
22
|
```
|
|
22
23
|
|
|
@@ -43,6 +44,17 @@ Offline verification:
|
|
|
43
44
|
node bin/verify.js --offline
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
The plugin is strict
|
|
47
|
+
The plugin is strict before setup: `max_concurrent_subagents` defaults to `0`, so normal subagent launches are blocked unless raised. Run `/subagent-budget-guard:setup` to replace the long `--config ...` install command with the recommended config:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
max_concurrent_subagents=1
|
|
51
|
+
max_subagent_tokens_per_session=100000
|
|
52
|
+
subagent_token_warning_threshold_percent=95
|
|
53
|
+
session_five_hour_budget_percent=25
|
|
54
|
+
absolute_five_hour_ceiling_percent=95
|
|
55
|
+
enforcement_enabled=true
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For existing installs, setup also removes obsolete `max_subagents_per_session` and `max_agent_team_tasks_per_session` options from this plugin's Claude settings.
|
|
47
59
|
|
|
48
60
|
`max_subagent_tokens_per_session` is enforced from verified `Agent.totalTokens` values after each completed subagent. `subagent_token_warning_threshold_percent` defaults to `95`; once verified subagent usage reaches that percentage, the plugin tells Claude to stop using subagents and blocks future subagent launches. Claude Code does not expose mid-run per-token subagent streaming to hooks, so a single running subagent can only be evaluated when it reports its final token total.
|
package/bin/setup.js
CHANGED
|
@@ -11,6 +11,13 @@ async function main() {
|
|
|
11
11
|
process.stdout.write(
|
|
12
12
|
[
|
|
13
13
|
'Subagent Budget Guard statusLine bridge installed.',
|
|
14
|
+
'Recommended plugin config applied:',
|
|
15
|
+
` max_concurrent_subagents=${result.pluginConfigOptions.max_concurrent_subagents}`,
|
|
16
|
+
` max_subagent_tokens_per_session=${result.pluginConfigOptions.max_subagent_tokens_per_session}`,
|
|
17
|
+
` subagent_token_warning_threshold_percent=${result.pluginConfigOptions.subagent_token_warning_threshold_percent}`,
|
|
18
|
+
` session_five_hour_budget_percent=${result.pluginConfigOptions.session_five_hour_budget_percent}`,
|
|
19
|
+
` absolute_five_hour_ceiling_percent=${result.pluginConfigOptions.absolute_five_hour_ceiling_percent}`,
|
|
20
|
+
` enforcement_enabled=${result.pluginConfigOptions.enforcement_enabled}`,
|
|
14
21
|
`Settings: ${result.settingsPath}`,
|
|
15
22
|
`Bridge state: ${result.bridgePath}`,
|
|
16
23
|
result.previousStatusLine
|
package/lib/guard.js
CHANGED
|
@@ -13,7 +13,12 @@ import {
|
|
|
13
13
|
} from 'node:fs/promises';
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
|
|
16
20
|
export const PLUGIN_NAME = 'subagent-budget-guard';
|
|
21
|
+
export const PLUGIN_ID = 'subagent-budget-guard@subagent-budget-tools';
|
|
17
22
|
|
|
18
23
|
export const DEFAULT_CONFIG = Object.freeze({
|
|
19
24
|
max_concurrent_subagents: 0,
|
|
@@ -24,7 +29,17 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
24
29
|
enforcement_enabled: true
|
|
25
30
|
});
|
|
26
31
|
|
|
32
|
+
export const SETUP_CONFIG = Object.freeze({
|
|
33
|
+
...DEFAULT_CONFIG,
|
|
34
|
+
max_concurrent_subagents: 1,
|
|
35
|
+
max_subagent_tokens_per_session: 100000
|
|
36
|
+
});
|
|
37
|
+
|
|
27
38
|
export const CONFIG_KEYS = Object.freeze(Object.keys(DEFAULT_CONFIG));
|
|
39
|
+
export const REMOVED_CONFIG_KEYS = Object.freeze([
|
|
40
|
+
'max_subagents_per_session',
|
|
41
|
+
'max_agent_team_tasks_per_session'
|
|
42
|
+
]);
|
|
28
43
|
|
|
29
44
|
const NUMBER_KEYS = new Set(
|
|
30
45
|
CONFIG_KEYS.filter((key) => typeof DEFAULT_CONFIG[key] === 'number')
|
|
@@ -92,7 +107,7 @@ export function getHomeDir(env = process.env) {
|
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
export function getPluginRoot(env = process.env) {
|
|
95
|
-
return env.CLAUDE_PLUGIN_ROOT ||
|
|
110
|
+
return env.CLAUDE_PLUGIN_ROOT || PACKAGE_ROOT;
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
export function getDataDir(env = process.env) {
|
|
@@ -757,6 +772,42 @@ function isBridgeStatusLine(statusLine) {
|
|
|
757
772
|
);
|
|
758
773
|
}
|
|
759
774
|
|
|
775
|
+
function isPlainObject(value) {
|
|
776
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function applySetupPluginConfig(
|
|
780
|
+
settings,
|
|
781
|
+
{ pluginId = PLUGIN_ID, setupConfig = SETUP_CONFIG } = {}
|
|
782
|
+
) {
|
|
783
|
+
if (!isPlainObject(settings.pluginConfigs)) {
|
|
784
|
+
settings.pluginConfigs = {};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const currentEntry = isPlainObject(settings.pluginConfigs[pluginId])
|
|
788
|
+
? settings.pluginConfigs[pluginId]
|
|
789
|
+
: {};
|
|
790
|
+
const currentOptions = isPlainObject(currentEntry.options)
|
|
791
|
+
? currentEntry.options
|
|
792
|
+
: {};
|
|
793
|
+
const nextOptions = { ...currentOptions };
|
|
794
|
+
|
|
795
|
+
for (const key of REMOVED_CONFIG_KEYS) {
|
|
796
|
+
delete nextOptions[key];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
for (const key of CONFIG_KEYS) {
|
|
800
|
+
nextOptions[key] = setupConfig[key];
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
settings.pluginConfigs[pluginId] = {
|
|
804
|
+
...currentEntry,
|
|
805
|
+
options: nextOptions
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
return nextOptions;
|
|
809
|
+
}
|
|
810
|
+
|
|
760
811
|
export async function installStatusLineBridge({
|
|
761
812
|
homeDir = getHomeDir(),
|
|
762
813
|
pluginRoot = getPluginRoot(),
|
|
@@ -778,6 +829,7 @@ export async function installStatusLineBridge({
|
|
|
778
829
|
padding: existing?.padding ?? previousStatusLine?.padding ?? 0,
|
|
779
830
|
refreshInterval: existing?.refreshInterval ?? 5
|
|
780
831
|
};
|
|
832
|
+
const pluginConfigOptions = applySetupPluginConfig(settings);
|
|
781
833
|
|
|
782
834
|
settings.statusLine = nextStatusLine;
|
|
783
835
|
await writeJsonAtomic(settingsPath, settings);
|
|
@@ -794,7 +846,9 @@ export async function installStatusLineBridge({
|
|
|
794
846
|
settingsPath,
|
|
795
847
|
bridgePath,
|
|
796
848
|
command,
|
|
797
|
-
previousStatusLine
|
|
849
|
+
previousStatusLine,
|
|
850
|
+
pluginConfigApplied: true,
|
|
851
|
+
pluginConfigOptions
|
|
798
852
|
};
|
|
799
853
|
}
|
|
800
854
|
|
package/lib/verifier.js
CHANGED
|
@@ -5,6 +5,9 @@ import path from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
CONFIG_KEYS,
|
|
8
|
+
PLUGIN_ID,
|
|
9
|
+
REMOVED_CONFIG_KEYS,
|
|
10
|
+
SETUP_CONFIG,
|
|
8
11
|
buildReport,
|
|
9
12
|
handlePostToolUseAgent,
|
|
10
13
|
handlePreToolUseAgent,
|
|
@@ -87,7 +90,7 @@ export async function runOfflineVerification({
|
|
|
87
90
|
entry.source?.package === '@rex_koh/subagent-budget-guard',
|
|
88
91
|
'marketplace npm package mismatch'
|
|
89
92
|
);
|
|
90
|
-
assert(entry.source?.version === '0.1.
|
|
93
|
+
assert(entry.source?.version === '0.1.4', 'marketplace npm version mismatch');
|
|
91
94
|
return marketplacePath;
|
|
92
95
|
});
|
|
93
96
|
} else {
|
|
@@ -287,6 +290,53 @@ export async function runOfflineVerification({
|
|
|
287
290
|
}
|
|
288
291
|
});
|
|
289
292
|
|
|
293
|
+
await withCheck(result, 'setup-applies-plugin-config', async () => {
|
|
294
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-data-'));
|
|
295
|
+
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-home-'));
|
|
296
|
+
try {
|
|
297
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
298
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
299
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
300
|
+
await mkdir(claudeDir, { recursive: true });
|
|
301
|
+
await writeFile(
|
|
302
|
+
settingsPath,
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
pluginConfigs: {
|
|
305
|
+
[PLUGIN_ID]: {
|
|
306
|
+
options: {
|
|
307
|
+
max_subagents_per_session: 9,
|
|
308
|
+
max_concurrent_subagents: 0,
|
|
309
|
+
max_agent_team_tasks_per_session: 4,
|
|
310
|
+
max_subagent_tokens_per_session: 0,
|
|
311
|
+
enforcement_enabled: false
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
await installStatusLineBridge({
|
|
319
|
+
homeDir,
|
|
320
|
+
pluginRoot: root,
|
|
321
|
+
pluginData: dataDir
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const settings = await readJson(settingsPath);
|
|
325
|
+
const options = settings.pluginConfigs?.[PLUGIN_ID]?.options;
|
|
326
|
+
assert(options, `missing pluginConfigs.${PLUGIN_ID}.options`);
|
|
327
|
+
for (const key of CONFIG_KEYS) {
|
|
328
|
+
assert(options[key] === SETUP_CONFIG[key], `setup config ${key} mismatch`);
|
|
329
|
+
}
|
|
330
|
+
for (const key of REMOVED_CONFIG_KEYS) {
|
|
331
|
+
assert(!(key in options), `obsolete option ${key} was not removed`);
|
|
332
|
+
}
|
|
333
|
+
return `${PLUGIN_ID} recommended setup config applied`;
|
|
334
|
+
} finally {
|
|
335
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
336
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
290
340
|
result.ok = result.failures.length === 0;
|
|
291
341
|
return result;
|
|
292
342
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rex_koh/subagent-budget-guard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Claude Code plugin that blocks subagents by default, records verified subagent usage, and enforces 5-hour usage budgets.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ClaudeSubAgentSuppressor",
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Install or refresh the Subagent Budget Guard statusLine bridge
|
|
2
|
+
description: Install or refresh the Subagent Budget Guard statusLine bridge and apply the recommended plugin config.
|
|
3
3
|
disable-model-invocation: true
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -11,7 +11,18 @@ Run this command:
|
|
|
11
11
|
node "${CLAUDE_PLUGIN_ROOT}/bin/setup.js"
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
This applies the recommended config in Claude settings:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
max_concurrent_subagents=1
|
|
18
|
+
max_subagent_tokens_per_session=100000
|
|
19
|
+
subagent_token_warning_threshold_percent=95
|
|
20
|
+
session_five_hour_budget_percent=25
|
|
21
|
+
absolute_five_hour_ceiling_percent=95
|
|
22
|
+
enforcement_enabled=true
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then tell the user to run `/reload-plugins`, interact with Claude Code once so the statusLine bridge receives fresh session JSON, and run:
|
|
15
26
|
|
|
16
27
|
```bash
|
|
17
28
|
node "${CLAUDE_PLUGIN_ROOT}/bin/verify.js" --live
|