claude-threads 1.7.0 → 1.8.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.
- package/CHANGELOG.md +18 -0
- package/README.md +1 -0
- package/dist/index.js +58 -24
- package/dist/mcp/permission-server.js +32 -19
- package/docs/CONFIGURATION.md +40 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.8.0] - 2026-04-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Pass `MCP_CONNECTION_NONBLOCKING=true` to the Claude child** — caps `--mcp-config` connects at 5s so a slow MCP server never delays session startup. Requires Claude CLI 2.1.89+. Set it explicitly in the bot's own env to override.
|
|
12
|
+
- **Pass `ENABLE_PROMPT_CACHING_1H=true` to the Claude child** — opts into the 1-hour prompt cache TTL, meaningfully reducing re-caching cost on long-lived threads that idle past the default 5-minute window. Requires Claude CLI 2.1.108+. Set it explicitly in the bot's own env to override.
|
|
13
|
+
- **Documented `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1`** as an opt-in hardening flag. When set on the bot's env it passes through to Claude, which strips the specific credential env vars `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_BEARER_TOKEN_BEDROCK`, and `GOOGLE_APPLICATION_CREDENTIALS` from any Bash, hook, or stdio MCP subprocess it spawns (empirically verified on CLI 2.1.116). Bot-specific vars like `PLATFORM_TOKEN` / `MATTERMOST_TOKEN` / `SLACK_BOT_TOKEN` pass through untouched. **Side effect:** the flag also forces Claude's permission mode to `default` and rejects `--dangerously-skip-permissions`; the bot now warns at startup if the flag is set alongside any platform configured with `skipPermissions: true`. Requires Claude CLI 2.1.83+.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Stale Claude CLI version range in `CLAUDE.md`** — the docs said `>=2.0.74 <=2.0.76`, but the actual pin in `version-check.ts` has been `>=2.0.74 <2.2.0` since v1.x. Also bumped the "install a compatible version" hint in the runtime error from `@2.1.1` to `@2.1.116`.
|
|
17
|
+
|
|
18
|
+
## [1.7.1] - 2026-04-21
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **`maxSessions` cap could be exceeded under concurrent session starts** — `startSession()` passed the cap check synchronously but then awaited `createPost()` before committing the session to the map, so concurrent starts all read the same stale count and over-admitted. The integration test `"should reject new session when at capacity"` flaked at ~40% for weeks because of this. Pending starts are now tracked alongside committed sessions in the cap check. (#331)
|
|
22
|
+
- **Reject Claude accounts with both `home` and `apiKey` set** — the two are documented as mutually exclusive, but `AccountPool` silently preferred `home` when both were configured. Now the account is dropped with a warning so misconfiguration surfaces. (#330, thanks @shaders)
|
|
23
|
+
- **Tighten `reset_at` epoch regex in rate-limit detection** — the old pattern matched `"preset": N` and (more importantly) `reset_after=N`, a *relative* retry-after hint that would have been misread as an absolute epoch and pushed cooldown decades into the future. Added word-boundary anchors and regression tests. (#330)
|
|
24
|
+
- **Rate-limit emit guard now allows deadline extensions** — the boolean latch in `ClaudeCli` fired at most once per process, so a second rate-limit hit with a longer reset time couldn't widen `AccountPool.markCooling`'s extend-only window. Replaced with a numeric last-deadline tracker: repeat hits at the same severity still dedupe, but a later deadline re-emits. (#330)
|
|
25
|
+
|
|
8
26
|
## [1.7.0] - 2026-04-21
|
|
9
27
|
|
|
10
28
|
### Added
|
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
- **Git worktrees** - Isolate changes in separate branches
|
|
35
35
|
- **File attachments** - Attach images, PDFs, and files for Claude to analyze
|
|
36
36
|
- **Chrome automation** - Control Chrome browser for web tasks
|
|
37
|
+
- **Multi-account Claude (opt-in)** - Round-robin sessions across multiple Claude subscriptions or API keys with automatic rate-limit cooldown — see [Configuration](docs/CONFIGURATION.md#claude-accounts-optional-multi-account-mode)
|
|
37
38
|
|
|
38
39
|
## Quick Start
|
|
39
40
|
|
package/dist/index.js
CHANGED
|
@@ -50038,7 +50038,7 @@ function validateClaudeCli() {
|
|
|
50038
50038
|
version: result.version,
|
|
50039
50039
|
compatible: false,
|
|
50040
50040
|
message: `Claude CLI version ${result.version} is not compatible. Required: ${CLAUDE_CLI_VERSION_RANGE}
|
|
50041
|
-
` + `Install a compatible version: npm install -g @anthropic-ai/claude-code@2.1.
|
|
50041
|
+
` + `Install a compatible version: npm install -g @anthropic-ai/claude-code@2.1.116`,
|
|
50042
50042
|
rawOutput: result.rawOutput ?? undefined
|
|
50043
50043
|
};
|
|
50044
50044
|
}
|
|
@@ -53561,8 +53561,13 @@ class AccountPool {
|
|
|
53561
53561
|
const hasAuth = !!acc.home || !!acc.apiKey;
|
|
53562
53562
|
if (!hasAuth) {
|
|
53563
53563
|
log6.warn(`Claude account ${acc.id} has neither home nor apiKey — ignoring`);
|
|
53564
|
+
return false;
|
|
53565
|
+
}
|
|
53566
|
+
if (acc.home && acc.apiKey) {
|
|
53567
|
+
log6.warn(`Claude account ${acc.id} has both home and apiKey set — must choose one; ignoring`);
|
|
53568
|
+
return false;
|
|
53564
53569
|
}
|
|
53565
|
-
return
|
|
53570
|
+
return true;
|
|
53566
53571
|
});
|
|
53567
53572
|
this.byId = new Map(this.accounts.map((acc) => [acc.id, acc]));
|
|
53568
53573
|
for (const acc of this.accounts) {
|
|
@@ -54215,7 +54220,7 @@ function extractResetAt(text, now) {
|
|
|
54215
54220
|
};
|
|
54216
54221
|
return now + value * unitMs[unit];
|
|
54217
54222
|
}
|
|
54218
|
-
const unix = text.match(
|
|
54223
|
+
const unix = text.match(/\breset(?:_at)?\b\s*["']?\s*[:=]\s*(\d{10,13})/);
|
|
54219
54224
|
if (unix) {
|
|
54220
54225
|
const raw = parseInt(unix[1], 10);
|
|
54221
54226
|
return unix[1].length === 13 ? raw : raw * 1000;
|
|
@@ -54255,6 +54260,25 @@ function cleanupBrowserBridgeSockets() {
|
|
|
54255
54260
|
log10.debug(`Browser bridge cleanup failed: ${err}`);
|
|
54256
54261
|
}
|
|
54257
54262
|
}
|
|
54263
|
+
function buildClaudeChildEnv(parentEnv, account) {
|
|
54264
|
+
const env = { ...parentEnv };
|
|
54265
|
+
if (env.MCP_CONNECTION_NONBLOCKING === undefined) {
|
|
54266
|
+
env.MCP_CONNECTION_NONBLOCKING = "true";
|
|
54267
|
+
}
|
|
54268
|
+
if (env.ENABLE_PROMPT_CACHING_1H === undefined) {
|
|
54269
|
+
env.ENABLE_PROMPT_CACHING_1H = "true";
|
|
54270
|
+
}
|
|
54271
|
+
if (account?.home) {
|
|
54272
|
+
env.HOME = account.home;
|
|
54273
|
+
env.USERPROFILE = account.home;
|
|
54274
|
+
delete env.ANTHROPIC_API_KEY;
|
|
54275
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
54276
|
+
} else if (account?.apiKey) {
|
|
54277
|
+
env.ANTHROPIC_API_KEY = account.apiKey;
|
|
54278
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
54279
|
+
}
|
|
54280
|
+
return env;
|
|
54281
|
+
}
|
|
54258
54282
|
function isErrorResultEvent(event) {
|
|
54259
54283
|
const ev = event;
|
|
54260
54284
|
if (typeof ev.subtype === "string" && ev.subtype.startsWith("error"))
|
|
@@ -54272,7 +54296,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
54272
54296
|
statusFilePath = null;
|
|
54273
54297
|
lastStatusData = null;
|
|
54274
54298
|
stderrBuffer = "";
|
|
54275
|
-
|
|
54299
|
+
lastEmittedRateLimitDeadline = 0;
|
|
54276
54300
|
log;
|
|
54277
54301
|
constructor(options2) {
|
|
54278
54302
|
super();
|
|
@@ -54324,7 +54348,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
54324
54348
|
if (this.process)
|
|
54325
54349
|
throw new Error("Already running");
|
|
54326
54350
|
this.stderrBuffer = "";
|
|
54327
|
-
this.
|
|
54351
|
+
this.lastEmittedRateLimitDeadline = 0;
|
|
54328
54352
|
cleanupBrowserBridgeSockets();
|
|
54329
54353
|
const claudePath = getClaudePath();
|
|
54330
54354
|
const args = [
|
|
@@ -54479,9 +54503,11 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
54479
54503
|
const hit = detectRateLimit(text);
|
|
54480
54504
|
if (!hit.detected)
|
|
54481
54505
|
return;
|
|
54482
|
-
|
|
54506
|
+
const newDeadline = cooldownDeadline(hit);
|
|
54507
|
+
const MIN_ADVANCE_MS = 60000;
|
|
54508
|
+
if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS)
|
|
54483
54509
|
return;
|
|
54484
|
-
this.
|
|
54510
|
+
this.lastEmittedRateLimitDeadline = newDeadline;
|
|
54485
54511
|
this.log.warn(`Rate limit detected: ${hit.matched ?? "(no match text)"}`);
|
|
54486
54512
|
this.emit("rate-limit", hit);
|
|
54487
54513
|
}
|
|
@@ -54554,20 +54580,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
54554
54580
|
return true;
|
|
54555
54581
|
}
|
|
54556
54582
|
buildChildEnv() {
|
|
54557
|
-
|
|
54558
|
-
if (!account)
|
|
54559
|
-
return process.env;
|
|
54560
|
-
const env = { ...process.env };
|
|
54561
|
-
if (account.home) {
|
|
54562
|
-
env.HOME = account.home;
|
|
54563
|
-
env.USERPROFILE = account.home;
|
|
54564
|
-
delete env.ANTHROPIC_API_KEY;
|
|
54565
|
-
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
54566
|
-
} else if (account.apiKey) {
|
|
54567
|
-
env.ANTHROPIC_API_KEY = account.apiKey;
|
|
54568
|
-
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
54569
|
-
}
|
|
54570
|
-
return env;
|
|
54583
|
+
return buildClaudeChildEnv(process.env, this.options.account);
|
|
54571
54584
|
}
|
|
54572
54585
|
getMcpServerPath() {
|
|
54573
54586
|
const __filename2 = fileURLToPath3(import.meta.url);
|
|
@@ -67216,6 +67229,11 @@ var sessionLog6 = createSessionLog(log26);
|
|
|
67216
67229
|
function mutableSessions(ctx) {
|
|
67217
67230
|
return ctx.state.sessions;
|
|
67218
67231
|
}
|
|
67232
|
+
var pendingStartsCount = 0;
|
|
67233
|
+
function releasePendingStart() {
|
|
67234
|
+
if (pendingStartsCount > 0)
|
|
67235
|
+
pendingStartsCount--;
|
|
67236
|
+
}
|
|
67219
67237
|
function mutablePostIndex(ctx) {
|
|
67220
67238
|
return ctx.state.postIndex;
|
|
67221
67239
|
}
|
|
@@ -67548,20 +67566,24 @@ async function startSession(options2, username, displayName, replyToPostId, plat
|
|
|
67548
67566
|
if (!platform) {
|
|
67549
67567
|
throw new Error(`Platform '${platformId}' not found. Call addPlatform() first.`);
|
|
67550
67568
|
}
|
|
67551
|
-
|
|
67569
|
+
const activeOrPending = ctx.state.sessions.size + pendingStartsCount;
|
|
67570
|
+
if (activeOrPending >= ctx.config.maxSessions) {
|
|
67552
67571
|
const formatter2 = platform.getFormatter();
|
|
67553
67572
|
const tempSession = {
|
|
67554
67573
|
platform,
|
|
67555
67574
|
threadId: replyToPostId || "",
|
|
67556
67575
|
sessionId: "temp"
|
|
67557
67576
|
};
|
|
67558
|
-
await post(tempSession, "warning", `${formatter2.formatBold("Too busy")} - ${
|
|
67577
|
+
await post(tempSession, "warning", `${formatter2.formatBold("Too busy")} - ${activeOrPending} sessions active. Please try again later.`);
|
|
67559
67578
|
return;
|
|
67560
67579
|
}
|
|
67580
|
+
pendingStartsCount++;
|
|
67561
67581
|
const startFormatter = platform.getFormatter();
|
|
67562
67582
|
const startPost = await withErrorHandling(() => platform.createPost(startFormatter.formatItalic("Claude Threads session starting..."), replyToPostId), { action: "Create session post" });
|
|
67563
|
-
if (!startPost)
|
|
67583
|
+
if (!startPost) {
|
|
67584
|
+
releasePendingStart();
|
|
67564
67585
|
return;
|
|
67586
|
+
}
|
|
67565
67587
|
const actualThreadId = replyToPostId || startPost.id;
|
|
67566
67588
|
const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
|
|
67567
67589
|
platform.sendTyping(actualThreadId);
|
|
@@ -67576,11 +67598,13 @@ async function startSession(options2, username, displayName, replyToPostId, plat
|
|
|
67576
67598
|
const resolvedDir = resolve6(requestedDir);
|
|
67577
67599
|
if (!existsSync11(resolvedDir)) {
|
|
67578
67600
|
await platform.updatePost(startPost.id, `❌ Directory does not exist: ${formatter.formatCode(initialOptions.workingDir)}`);
|
|
67601
|
+
releasePendingStart();
|
|
67579
67602
|
return;
|
|
67580
67603
|
}
|
|
67581
67604
|
const { statSync: statSync4 } = await import("fs");
|
|
67582
67605
|
if (!statSync4(resolvedDir).isDirectory()) {
|
|
67583
67606
|
await platform.updatePost(startPost.id, `❌ Not a directory: ${formatter.formatCode(initialOptions.workingDir)}`);
|
|
67607
|
+
releasePendingStart();
|
|
67584
67608
|
return;
|
|
67585
67609
|
}
|
|
67586
67610
|
workingDir = resolvedDir;
|
|
@@ -67649,6 +67673,7 @@ ${CHAT_PLATFORM_PROMPT}`;
|
|
|
67649
67673
|
workingDir: ctx.config.workingDir
|
|
67650
67674
|
});
|
|
67651
67675
|
mutableSessions(ctx).set(sessionId, session);
|
|
67676
|
+
releasePendingStart();
|
|
67652
67677
|
ctx.ops.registerPost(startPost.id, actualThreadId);
|
|
67653
67678
|
ctx.ops.emitSessionAdd(session);
|
|
67654
67679
|
sessionLog6(session).info(`▶ Session started by @${username}`);
|
|
@@ -79986,6 +80011,15 @@ async function startWithoutDaemon() {
|
|
|
79986
80011
|
console.error("");
|
|
79987
80012
|
process.exit(1);
|
|
79988
80013
|
}
|
|
80014
|
+
if (process.env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB === "1") {
|
|
80015
|
+
const hasSkipPermissionPlatform = config.platforms.some((p) => p.skipPermissions === true);
|
|
80016
|
+
if (hasSkipPermissionPlatform) {
|
|
80017
|
+
console.error(red(" ⚠️ CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 is set but a platform has skipPermissions: true."));
|
|
80018
|
+
console.error(dim(" Claude CLI will force permissionMode: default and reject --dangerously-skip-permissions."));
|
|
80019
|
+
console.error(dim(" Either unset the env var or set skipPermissions: false on all platforms."));
|
|
80020
|
+
console.error("");
|
|
80021
|
+
}
|
|
80022
|
+
}
|
|
79989
80023
|
let triggerShutdown = null;
|
|
79990
80024
|
const updateState = loadUpdateState();
|
|
79991
80025
|
const restoredSettings = updateState.justUpdated ? updateState.runtimeSettings : undefined;
|
|
@@ -53526,6 +53526,11 @@ function detectRateLimit(text, now = Date.now()) {
|
|
|
53526
53526
|
const resetAtEpochMs = extractResetAt(text, now);
|
|
53527
53527
|
return { detected: true, matched, resetAtEpochMs };
|
|
53528
53528
|
}
|
|
53529
|
+
function cooldownDeadline(hit, now = Date.now()) {
|
|
53530
|
+
if (!hit.detected)
|
|
53531
|
+
return now;
|
|
53532
|
+
return hit.resetAtEpochMs ?? now + DEFAULT_COOLDOWN_MS;
|
|
53533
|
+
}
|
|
53529
53534
|
function extractResetAt(text, now) {
|
|
53530
53535
|
const relative = text.match(/(?:retry[_\s-]?after|resets?\s+in)\s+(\d+)\s*(second|minute|hour|day)s?/i);
|
|
53531
53536
|
if (relative) {
|
|
@@ -53539,7 +53544,7 @@ function extractResetAt(text, now) {
|
|
|
53539
53544
|
};
|
|
53540
53545
|
return now + value * unitMs[unit];
|
|
53541
53546
|
}
|
|
53542
|
-
const unix = text.match(
|
|
53547
|
+
const unix = text.match(/\breset(?:_at)?\b\s*["']?\s*[:=]\s*(\d{10,13})/);
|
|
53543
53548
|
if (unix) {
|
|
53544
53549
|
const raw = parseInt(unix[1], 10);
|
|
53545
53550
|
return unix[1].length === 13 ? raw : raw * 1000;
|
|
@@ -53579,6 +53584,25 @@ function cleanupBrowserBridgeSockets() {
|
|
|
53579
53584
|
log7.debug(`Browser bridge cleanup failed: ${err}`);
|
|
53580
53585
|
}
|
|
53581
53586
|
}
|
|
53587
|
+
function buildClaudeChildEnv(parentEnv, account) {
|
|
53588
|
+
const env = { ...parentEnv };
|
|
53589
|
+
if (env.MCP_CONNECTION_NONBLOCKING === undefined) {
|
|
53590
|
+
env.MCP_CONNECTION_NONBLOCKING = "true";
|
|
53591
|
+
}
|
|
53592
|
+
if (env.ENABLE_PROMPT_CACHING_1H === undefined) {
|
|
53593
|
+
env.ENABLE_PROMPT_CACHING_1H = "true";
|
|
53594
|
+
}
|
|
53595
|
+
if (account?.home) {
|
|
53596
|
+
env.HOME = account.home;
|
|
53597
|
+
env.USERPROFILE = account.home;
|
|
53598
|
+
delete env.ANTHROPIC_API_KEY;
|
|
53599
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
53600
|
+
} else if (account?.apiKey) {
|
|
53601
|
+
env.ANTHROPIC_API_KEY = account.apiKey;
|
|
53602
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
53603
|
+
}
|
|
53604
|
+
return env;
|
|
53605
|
+
}
|
|
53582
53606
|
function isErrorResultEvent(event) {
|
|
53583
53607
|
const ev = event;
|
|
53584
53608
|
if (typeof ev.subtype === "string" && ev.subtype.startsWith("error"))
|
|
@@ -53596,7 +53620,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
53596
53620
|
statusFilePath = null;
|
|
53597
53621
|
lastStatusData = null;
|
|
53598
53622
|
stderrBuffer = "";
|
|
53599
|
-
|
|
53623
|
+
lastEmittedRateLimitDeadline = 0;
|
|
53600
53624
|
log;
|
|
53601
53625
|
constructor(options2) {
|
|
53602
53626
|
super();
|
|
@@ -53648,7 +53672,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
53648
53672
|
if (this.process)
|
|
53649
53673
|
throw new Error("Already running");
|
|
53650
53674
|
this.stderrBuffer = "";
|
|
53651
|
-
this.
|
|
53675
|
+
this.lastEmittedRateLimitDeadline = 0;
|
|
53652
53676
|
cleanupBrowserBridgeSockets();
|
|
53653
53677
|
const claudePath = getClaudePath();
|
|
53654
53678
|
const args = [
|
|
@@ -53803,9 +53827,11 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
53803
53827
|
const hit = detectRateLimit(text);
|
|
53804
53828
|
if (!hit.detected)
|
|
53805
53829
|
return;
|
|
53806
|
-
|
|
53830
|
+
const newDeadline = cooldownDeadline(hit);
|
|
53831
|
+
const MIN_ADVANCE_MS = 60000;
|
|
53832
|
+
if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS)
|
|
53807
53833
|
return;
|
|
53808
|
-
this.
|
|
53834
|
+
this.lastEmittedRateLimitDeadline = newDeadline;
|
|
53809
53835
|
this.log.warn(`Rate limit detected: ${hit.matched ?? "(no match text)"}`);
|
|
53810
53836
|
this.emit("rate-limit", hit);
|
|
53811
53837
|
}
|
|
@@ -53878,20 +53904,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
53878
53904
|
return true;
|
|
53879
53905
|
}
|
|
53880
53906
|
buildChildEnv() {
|
|
53881
|
-
|
|
53882
|
-
if (!account)
|
|
53883
|
-
return process.env;
|
|
53884
|
-
const env = { ...process.env };
|
|
53885
|
-
if (account.home) {
|
|
53886
|
-
env.HOME = account.home;
|
|
53887
|
-
env.USERPROFILE = account.home;
|
|
53888
|
-
delete env.ANTHROPIC_API_KEY;
|
|
53889
|
-
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
53890
|
-
} else if (account.apiKey) {
|
|
53891
|
-
env.ANTHROPIC_API_KEY = account.apiKey;
|
|
53892
|
-
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
53893
|
-
}
|
|
53894
|
-
return env;
|
|
53907
|
+
return buildClaudeChildEnv(process.env, this.options.account);
|
|
53895
53908
|
}
|
|
53896
53909
|
getMcpServerPath() {
|
|
53897
53910
|
const __filename2 = fileURLToPath3(import.meta.url);
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -72,6 +72,33 @@ platforms:
|
|
|
72
72
|
| `allowedUsers` | No | List of Slack usernames |
|
|
73
73
|
| `skipPermissions` | No | Auto-approve actions (default: `false`) |
|
|
74
74
|
|
|
75
|
+
## Claude Accounts (optional, multi-account mode)
|
|
76
|
+
|
|
77
|
+
By default every session spawns `claude` with the bot's own `process.env`, so they all share one subscription's token budget. Add a `claudeAccounts` block to spread load across multiple accounts — the bot round-robins new sessions across the pool and automatically skips accounts in rate-limit cooldown. Omit the block entirely to stay in single-account mode (unchanged behavior).
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
claudeAccounts:
|
|
81
|
+
# OAuth accounts — prepare each HOME first with `HOME=<path> claude login`
|
|
82
|
+
- id: primary
|
|
83
|
+
home: /home/bot/.claude-accounts/primary
|
|
84
|
+
- id: backup
|
|
85
|
+
displayName: Backup (Pro)
|
|
86
|
+
home: /home/bot/.claude-accounts/backup
|
|
87
|
+
|
|
88
|
+
# API-key billed
|
|
89
|
+
- id: shared-api
|
|
90
|
+
apiKey: sk-ant-api03-xxxxxxxx...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Setting | Required | Description |
|
|
94
|
+
|---------|----------|-------------|
|
|
95
|
+
| `id` | Yes | Stable identifier used in logs, UI, and persisted session state |
|
|
96
|
+
| `home` | One of | Alternate `$HOME` containing `.claude/.credentials.json` from a prior `HOME=<path> claude login`. For OAuth Pro/Max subscriptions. Session history also lives here, so resumed sessions pick the same account. |
|
|
97
|
+
| `apiKey` | One of | Anthropic API key. Billed against that key; session history stays under the bot's default `HOME`. |
|
|
98
|
+
| `displayName` | No | Human-readable label in UI (defaults to `id`) |
|
|
99
|
+
|
|
100
|
+
Exactly one of `home` or `apiKey` should be set per account. Persisted sessions record which account they ran under and resume on the same one.
|
|
101
|
+
|
|
75
102
|
## Environment Variables
|
|
76
103
|
|
|
77
104
|
| Variable | Description | Default |
|
|
@@ -80,6 +107,19 @@ platforms:
|
|
|
80
107
|
| `SESSION_TIMEOUT_MS` | Idle timeout in milliseconds | `1800000` (30 min) |
|
|
81
108
|
| `NO_UPDATE_NOTIFIER` | Disable update checks | - |
|
|
82
109
|
| `DEBUG` | Enable verbose logging | - |
|
|
110
|
+
| `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` | Strip `ANTHROPIC_*` / `AWS_*_TOKEN` / `CLAUDE_CODE_OAUTH_TOKEN` / `GOOGLE_APPLICATION_CREDENTIALS` etc. from Bash, hook, and stdio-MCP subprocesses Claude spawns. Bot-specific vars like `PLATFORM_TOKEN` pass through. **Also forces permission mode to `default`** — `--dangerously-skip-permissions` will be rejected. Requires Claude CLI 2.1.83+. | - |
|
|
111
|
+
|
|
112
|
+
### Forwarded to Claude CLI automatically
|
|
113
|
+
|
|
114
|
+
The bot sets two tuning flags on the Claude child process when they aren't
|
|
115
|
+
already present in the bot's environment:
|
|
116
|
+
|
|
117
|
+
| Variable | Effect | Requires |
|
|
118
|
+
|----------|--------|----------|
|
|
119
|
+
| `MCP_CONNECTION_NONBLOCKING=true` | Caps `--mcp-config` connects at 5s so a slow MCP server never delays startup | Claude CLI 2.1.89+ |
|
|
120
|
+
| `ENABLE_PROMPT_CACHING_1H=true` | Opts into 1-hour prompt cache TTL, cutting re-caching cost on long-lived threads | Claude CLI 2.1.108+ |
|
|
121
|
+
|
|
122
|
+
Export either with a different value in the bot's own env to disable.
|
|
83
123
|
|
|
84
124
|
## CLI Options
|
|
85
125
|
|