@rex_koh/subagent-budget-guard 0.1.3 → 0.1.5

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.
@@ -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.3",
5
+ "version": "0.1.5",
6
6
  "author": {
7
7
  "name": "ClaudeSubAgentSuppressor"
8
8
  },
@@ -21,7 +21,7 @@
21
21
  "description": "Maximum simultaneously active subagents. The default 0 blocks all new subagents.",
22
22
  "default": 0,
23
23
  "min": 0,
24
- "required": true
24
+ "required": false
25
25
  },
26
26
  "max_subagent_tokens_per_session": {
27
27
  "type": "number",
@@ -29,7 +29,7 @@
29
29
  "description": "Maximum verified subagent tokens allowed in one session. Set 0 for no token cap.",
30
30
  "default": 0,
31
31
  "min": 0,
32
- "required": true
32
+ "required": false
33
33
  },
34
34
  "subagent_token_warning_threshold_percent": {
35
35
  "type": "number",
@@ -38,7 +38,7 @@
38
38
  "default": 95,
39
39
  "min": 1,
40
40
  "max": 100,
41
- "required": true
41
+ "required": false
42
42
  },
43
43
  "session_five_hour_budget_percent": {
44
44
  "type": "number",
@@ -47,7 +47,7 @@
47
47
  "default": 25,
48
48
  "min": 0,
49
49
  "max": 100,
50
- "required": true
50
+ "required": false
51
51
  },
52
52
  "absolute_five_hour_ceiling_percent": {
53
53
  "type": "number",
@@ -56,14 +56,14 @@
56
56
  "default": 95,
57
57
  "min": 0,
58
58
  "max": 100,
59
- "required": true
59
+ "required": false
60
60
  },
61
61
  "enforcement_enabled": {
62
62
  "type": "boolean",
63
63
  "title": "Enable enforcement",
64
64
  "description": "When true, hooks deny over-budget subagents and prompts. When false, the plugin records usage without blocking.",
65
65
  "default": true,
66
- "required": true
66
+ "required": false
67
67
  }
68
68
  }
69
69
  }
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Subagent Budget Guard
2
2
 
3
- Claude Code plugin that blocks subagents by default, records verified subagent usage, and enforces a session budget against Claude Code's 5-hour usage percentage.
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
- Run after install:
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 by default: `max_concurrent_subagents` defaults to `0`, so normal subagent launches are blocked unless raised.
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 || path.resolve('.');
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.3', 'marketplace npm version mismatch');
93
+ assert(entry.source?.version === '0.1.5', 'marketplace npm version mismatch');
91
94
  return marketplacePath;
92
95
  });
93
96
  } else {
@@ -116,6 +119,10 @@ export async function runOfflineVerification({
116
119
  );
117
120
  for (const key of CONFIG_KEYS) {
118
121
  assert(manifest.userConfig?.[key], `missing userConfig.${key}`);
122
+ assert(
123
+ manifest.userConfig[key].required !== true,
124
+ `userConfig.${key} must not be required at install time`
125
+ );
119
126
  }
120
127
  return manifestPath;
121
128
  });
@@ -287,6 +294,53 @@ export async function runOfflineVerification({
287
294
  }
288
295
  });
289
296
 
297
+ await withCheck(result, 'setup-applies-plugin-config', async () => {
298
+ const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-data-'));
299
+ const homeDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-home-'));
300
+ try {
301
+ const { mkdir, writeFile } = await import('node:fs/promises');
302
+ const claudeDir = path.join(homeDir, '.claude');
303
+ const settingsPath = path.join(claudeDir, 'settings.json');
304
+ await mkdir(claudeDir, { recursive: true });
305
+ await writeFile(
306
+ settingsPath,
307
+ JSON.stringify({
308
+ pluginConfigs: {
309
+ [PLUGIN_ID]: {
310
+ options: {
311
+ max_subagents_per_session: 9,
312
+ max_concurrent_subagents: 0,
313
+ max_agent_team_tasks_per_session: 4,
314
+ max_subagent_tokens_per_session: 0,
315
+ enforcement_enabled: false
316
+ }
317
+ }
318
+ }
319
+ })
320
+ );
321
+
322
+ await installStatusLineBridge({
323
+ homeDir,
324
+ pluginRoot: root,
325
+ pluginData: dataDir
326
+ });
327
+
328
+ const settings = await readJson(settingsPath);
329
+ const options = settings.pluginConfigs?.[PLUGIN_ID]?.options;
330
+ assert(options, `missing pluginConfigs.${PLUGIN_ID}.options`);
331
+ for (const key of CONFIG_KEYS) {
332
+ assert(options[key] === SETUP_CONFIG[key], `setup config ${key} mismatch`);
333
+ }
334
+ for (const key of REMOVED_CONFIG_KEYS) {
335
+ assert(!(key in options), `obsolete option ${key} was not removed`);
336
+ }
337
+ return `${PLUGIN_ID} recommended setup config applied`;
338
+ } finally {
339
+ await rm(dataDir, { recursive: true, force: true });
340
+ await rm(homeDir, { recursive: true, force: true });
341
+ }
342
+ });
343
+
290
344
  result.ok = result.failures.length === 0;
291
345
  return result;
292
346
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rex_koh/subagent-budget-guard",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Install or refresh the Subagent Budget Guard statusLine bridge so 5-hour rate-limit percentages can be captured for enforcement.
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
- Then tell the user to interact with Claude Code once so the statusLine bridge receives fresh session JSON. After that, run:
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