fix: unblock claude-cli live runs

This commit is contained in:
Peter Steinberger
2026-01-11 00:22:48 +00:00
parent d8f1124d59
commit 24c3ab6fe0
5 changed files with 51 additions and 13 deletions

View File

@@ -177,7 +177,8 @@ CLAWDBOT_LIVE_TEST=1 CLAWDBOT_LIVE_SETUP_TOKEN=1 CLAWDBOT_LIVE_SETUP_TOKEN_PROFI
- `CLAWDBOT_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-opus-4-5"`
- `CLAWDBOT_LIVE_CLI_BACKEND_COMMAND="/full/path/to/claude"`
- `CLAWDBOT_LIVE_CLI_BACKEND_ARGS='["-p","--output-format","json","--permission-mode","bypassPermissions"]'`
- `CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV='["ANTHROPIC_API_KEY"]'`
- `CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV='["ANTHROPIC_API_KEY","ANTHROPIC_API_KEY_OLD"]'`
- `CLAWDBOT_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG=0` to keep Claude CLI MCP config enabled (default disables MCP config with a temporary empty file).
Example:

View File

@@ -43,7 +43,7 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
systemPromptArg: "--append-system-prompt",
systemPromptMode: "append",
systemPromptWhen: "first",
clearEnv: ["ANTHROPIC_API_KEY"],
clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"],
serialize: true,
};

View File

@@ -455,6 +455,7 @@ export async function runCliAgent(params: {
backend,
prompt: params.prompt,
});
const stdinPayload = stdin ?? "";
const args = buildCliArgs({
backend,
modelId: normalizedModel,
@@ -526,7 +527,7 @@ export async function runCliAgent(params: {
timeoutMs: params.timeoutMs,
cwd: workspaceDir,
env,
...(stdin ? { input: stdin } : {}),
input: stdinPayload,
});
const stdout = result.stdout.trim();

View File

@@ -21,7 +21,7 @@ const DEFAULT_ARGS = [
"json",
"--dangerously-skip-permissions",
];
const DEFAULT_CLEAR_ENV: string[] = [];
const DEFAULT_CLEAR_ENV = ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"];
function extractPayloadText(result: unknown): string {
const record = result as Record<string, unknown>;
@@ -52,6 +52,20 @@ function parseJsonStringArray(
return parsed;
}
function withMcpConfigOverrides(
args: string[],
mcpConfigPath: string,
): string[] {
const next = [...args];
if (!next.includes("--strict-mcp-config")) {
next.push("--strict-mcp-config");
}
if (!next.includes("--mcp-config")) {
next.push("--mcp-config", mcpConfigPath);
}
return next;
}
async function getFreePort(): Promise<number> {
return await new Promise((resolve, reject) => {
const srv = createServer();
@@ -134,12 +148,16 @@ describeLive("gateway live (cli backend)", () => {
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
skipCron: process.env.CLAWDBOT_SKIP_CRON,
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
anthropicApiKeyOld: process.env.ANTHROPIC_API_KEY_OLD,
};
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
process.env.CLAWDBOT_SKIP_CRON = "1";
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
delete process.env.ANTHROPIC_API_KEY;
delete process.env.ANTHROPIC_API_KEY_OLD;
const token = `test-${randomUUID()}`;
process.env.CLAWDBOT_GATEWAY_TOKEN = token;
@@ -156,7 +174,7 @@ describeLive("gateway live (cli backend)", () => {
const cliCommand =
process.env.CLAWDBOT_LIVE_CLI_BACKEND_COMMAND ?? "claude";
const cliArgs =
const baseCliArgs =
parseJsonStringArray(
"CLAWDBOT_LIVE_CLI_BACKEND_ARGS",
process.env.CLAWDBOT_LIVE_CLI_BACKEND_ARGS,
@@ -167,6 +185,20 @@ describeLive("gateway live (cli backend)", () => {
process.env.CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV,
) ?? DEFAULT_CLEAR_ENV;
const tempDir = await fs.mkdtemp(
path.join(os.tmpdir(), "clawdbot-live-cli-"),
);
const mcpConfigPath = path.join(tempDir, "claude-mcp.json");
await fs.writeFile(
mcpConfigPath,
`${JSON.stringify({ mcpServers: {} }, null, 2)}\n`,
);
const disableMcpConfig =
process.env.CLAWDBOT_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG !== "0";
const cliArgs = disableMcpConfig
? withMcpConfigOverrides(baseCliArgs, mcpConfigPath)
: baseCliArgs;
const cfg = loadConfig();
const existingBackends = cfg.agents?.defaults?.cliBackends ?? {};
const nextCfg = {
@@ -185,16 +217,13 @@ describeLive("gateway live (cli backend)", () => {
command: cliCommand,
args: cliArgs,
clearEnv: cliClearEnv,
systemPromptWhen: "never",
},
},
sandbox: { mode: "off" },
},
},
};
const tempDir = await fs.mkdtemp(
path.join(os.tmpdir(), "clawdbot-live-cli-"),
);
const tempConfigPath = path.join(tempDir, "clawdbot.json");
await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`);
process.env.CLAWDBOT_CONFIG_PATH = tempConfigPath;
@@ -252,6 +281,12 @@ describeLive("gateway live (cli backend)", () => {
if (previous.skipCanvas === undefined)
delete process.env.CLAWDBOT_SKIP_CANVAS_HOST;
else process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas;
if (previous.anthropicApiKey === undefined)
delete process.env.ANTHROPIC_API_KEY;
else process.env.ANTHROPIC_API_KEY = previous.anthropicApiKey;
if (previous.anthropicApiKeyOld === undefined)
delete process.env.ANTHROPIC_API_KEY_OLD;
else process.env.ANTHROPIC_API_KEY_OLD = previous.anthropicApiKeyOld;
}
}, 60_000);
});

View File

@@ -59,13 +59,14 @@ export async function runCommandWithTimeout(
? { timeoutMs: optionsOrTimeout }
: optionsOrTimeout;
const { timeoutMs, cwd, input, env } = options;
const hasInput = input !== undefined;
// Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed.
return await new Promise((resolve, reject) => {
const child = spawn(argv[0], argv.slice(1), {
stdio: [input ? "pipe" : "inherit", "pipe", "pipe"],
stdio: [hasInput ? "pipe" : "inherit", "pipe", "pipe"],
cwd,
env: env ? { ...process.env, ...env } : process.env,
env: env ?? process.env,
});
let stdout = "";
let stderr = "";
@@ -74,8 +75,8 @@ export async function runCommandWithTimeout(
child.kill("SIGKILL");
}, timeoutMs);
if (input && child.stdin) {
child.stdin.write(input);
if (hasInput && child.stdin) {
child.stdin.write(input ?? "");
child.stdin.end();
}