fix(claude-local): pipe print prompt via stdin (#9500)

Fixes #2444.
Refs #4947.

The `claude_local` adapter launched Claude Code as
`claude --print - --output-format stream-json --verbose`. Paperclip writes
the rendered task prompt to Claude's stdin, but current Claude Code releases
can treat the stale `-` positional marker as the prompt itself, so Claude
received the literal string `"-"` instead of the issue body. The customer's
task ran against no content at all.

The fix keeps `--print` mode and stdin delivery, and removes the stale `-`.

Adds regression coverage on both sides of the delivery path: a `claude_local`
assertion that `--print` is present, `"-"` is absent and the prompt still
reaches stdin, and an adapter-utils case proving the sandbox run-log command
wrapper preserves stdin while streaming logs.

Authored by @elJayAdvisor, whose commit is included unchanged with their
authorship. The branch had gone stale and was showing CONFLICTING; the
conflict was in `execution-target-sandbox.test.ts`, where their new test was
added at the same point as master's `creates the process session directories
only in the launch exec` case and git interleaved the two into one hunk.
Resolved by taking master's file and re-inserting their test whole, after
checking every helper it needs still exists there.

Verified: the bug was still live on master at `execute.ts:838`; the
regression test genuinely catches it — restoring the stale `-` fails
`expect(captured.argv).not.toContain("-")`; `@paperclipai/adapter-claude-local`
and `@paperclipai/adapter-utils` typecheck clean; 67 pass across the two test
files. All CI gates green; Greptile 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
LeeJ 2026-08-14 19:34:15 +01:00 committed by GitHub
parent 8cb0ce0de5
commit a53cc8819b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 1 deletions

View File

@ -237,6 +237,48 @@ describe("sandbox adapter execution targets", () => {
});
});
it("preserves stdin when wrapping sandbox adapter commands for run-log streaming", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-run-log-stdin-"));
cleanupDirs.push(rootDir);
const target: AdapterSandboxExecutionTarget = {
kind: "remote",
transport: "sandbox",
providerKey: "local-test",
remoteCwd: rootDir,
timeoutMs: 30_000,
streamRunLogs: true,
runner: createLocalSandboxRunner(),
};
const logsDir = path.posix.join(rootDir, ".paperclip-runtime", "bridge", "logs");
const runLogTail = createSandboxRunLogTailFactory({
runner: target.runner!,
remoteCwd: rootDir,
logsDir,
shellCommand: "bash",
}).create();
const events: Array<{ stream: "stdout" | "stderr"; chunk: string }> = [];
const result = await runAdapterExecutionTargetProcess(
"run-log-stdin",
target,
process.execPath,
["-e", "process.stdin.setEncoding('utf8'); let s=''; process.stdin.on('data', c => s += c); process.stdin.on('end', () => process.stdout.write('stdin=' + s));"],
{
cwd: rootDir,
env: {},
stdin: "hello-through-wrapper",
timeoutSec: 5,
graceSec: 1,
runLogTail: { create: () => runLogTail },
onLog: async (stream, chunk) => { events.push({ stream, chunk }); },
},
);
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe("stdin=hello-through-wrapper");
expect(combinedStream(events, "stdout")).toContain("stdin=hello-through-wrapper");
});
it("creates the process session directories only in the launch exec, not in upfront makeDir execs", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-makedir-"));
cleanupDirs.push(rootDir);

View File

@ -835,7 +835,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
resumeSessionId: string | null,
attemptInstructionsFilePath: string | undefined,
) => {
const args = ["--print", "-", "--output-format", "stream-json", "--verbose"];
const args = ["--print", "--output-format", "stream-json", "--verbose"];
if (resumeSessionId) args.push("--resume", resumeSessionId);
args.push(...buildClaudeExecutionPermissionArgs({
dangerouslySkipPermissions,

View File

@ -434,6 +434,9 @@ describe("claude execute", () => {
onMeta: async () => {},
});
const captured = JSON.parse(await fs.readFile(capturePath, "utf-8"));
expect(captured.argv).toContain("--print");
expect(captured.argv).not.toContain("-");
expect(captured.prompt).toContain("Do work.");
expect(captured.argv).toContain("--append-system-prompt-file");
} finally {
restore();