fix: pass Claude prompt via stdin to avoid ENAMETOOLONG on Windows

The Claude Code adapter passed the full prompt (~44 KB with skills,
context, and memory) as a command-line argument via `-p <prompt>`.
On Windows, CreateProcessW has a 32,767-character command-line limit,
causing `spawn ENAMETOOLONG` for any agent with multiple skills or
substantial context (e.g. Wiki Keeper).

Claude Code supports reading the prompt from stdin when `-p` is used
without a positional prompt argument — the same pattern the Codex
adapter already uses with `-`.

Changes:
- src/adapters/claudeCodeAdapter.ts: remove opts.prompt from args,
  add stdinPayload to the return value
- src/adapters/types.ts: update ExecInvocation.stdinPayload comment
- src/adapters/claudeCodeAdapter.test.ts: update buildExec test
  expectations (prompt via stdinPayload, not in args)

Tested: all 252 tests pass. Manually verified on Windows 10 with
Claude Code CLI.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zhaoming-mike 2026-06-29 10:12:23 +08:00
parent 0251d8bcb9
commit 18742fd60e
4 changed files with 9 additions and 10 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-agent-fleet",
"version": "0.13.6",
"version": "0.15.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-agent-fleet",
"version": "0.13.6",
"version": "0.15.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View file

@ -77,13 +77,14 @@ function makeBuildOptions(overrides: Partial<ExecBuildOptions> = {}): ExecBuildO
}
describe("claudeCodeAdapter.buildExec", () => {
it("builds the streaming invocation with -p prompt and verbose stream-json", async () => {
it("builds the streaming invocation with -p (prompt via stdin) and verbose stream-json", async () => {
const inv = await claudeCodeAdapter.buildExec(makeBuildOptions());
expect(inv.cliPath).toBe("claude");
expect(inv.args.slice(0, 2)).toEqual(["-p", "do the thing"]);
expect(inv.args[0]).toBe("-p");
expect(inv.args).toContain("stream-json");
expect(inv.args).toContain("--verbose");
expect(inv.stdinPayload).toBeUndefined();
expect(inv.args).not.toContain("do the thing");
expect(inv.stdinPayload).toBe("do the thing");
});
it("uses plain json without --verbose when not streaming", async () => {

View file

@ -226,7 +226,6 @@ export const claudeCodeAdapter: CliAdapter = {
buildExec(opts: ExecBuildOptions): Promise<ExecInvocation> {
const args = [
"-p",
opts.prompt,
"--output-format",
opts.streaming ? "stream-json" : "json",
];
@ -256,7 +255,7 @@ export const claudeCodeAdapter: CliAdapter = {
args.push("--permission-mode", "bypassPermissions");
}
return Promise.resolve({ cliPath: opts.settings.claudeCliPath, args });
return Promise.resolve({ cliPath: opts.settings.claudeCliPath, args, stdinPayload: opts.prompt });
},
parseExecOutput(stdout: string, stderr: string, streaming: boolean): ExecParseResult {

View file

@ -27,9 +27,8 @@ export interface ExecBuildOptions {
export interface ExecInvocation {
cliPath: string;
args: string[];
/** Written to stdin right after spawn, then stdin is closed. Codex takes
* the prompt this way (positional `-`) to dodge argv length limits;
* Claude takes the prompt via `-p` and needs nothing on stdin. */
/** Written to stdin right after spawn, then stdin is closed. Both adapters
* use this to dodge OS argv length limits (Windows: 32767 chars). */
stdinPayload?: string;
}