mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
fix(autoresearch): pass claude prompt via stdin (#2184)
* fix(autoresearch): pass Claude prompt via stdin to prevent shell injection The modify() function in autoresearch/commands/run.ts interpolated a prompt string — built from git log messages and scope file names — directly into a shell command executed by execSync. The double-quote escaping only handled literal quotes, leaving $(...), backticks and backslashes able to trigger command substitution. Switch to the same pattern already used in autoresearch/commands/fix.ts: pass the prompt via the execSync 'input' option so it is delivered on stdin and never parsed by the shell. * fix(autoresearch): invoke Claude without a shell --------- Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
* Engine handles commit, verify, guard, keep/discard, and logging.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync, type ExecFileSyncOptionsWithStringEncoding } from 'node:child_process';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseArgs, type AutoResearchConfig } from '../config.js';
|
||||
@@ -20,6 +20,31 @@ import { PRESETS } from '../presets/index.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..', '..');
|
||||
const CLAUDE_ALLOWED_TOOLS = 'Bash(npm:*),Bash(npx:*),Bash(git:*),Read,Edit,Write,Glob,Grep';
|
||||
|
||||
export function buildClaudeModifyInvocation(prompt: string) {
|
||||
const options: ExecFileSyncOptionsWithStringEncoding = {
|
||||
cwd: ROOT,
|
||||
timeout: 300_000,
|
||||
encoding: 'utf-8',
|
||||
input: prompt,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: process.env,
|
||||
};
|
||||
return {
|
||||
command: 'claude',
|
||||
args: [
|
||||
'-p',
|
||||
'--dangerously-skip-permissions',
|
||||
'--allowedTools',
|
||||
CLAUDE_ALLOWED_TOOLS,
|
||||
'--output-format',
|
||||
'text',
|
||||
'--no-session-persistence',
|
||||
],
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
function buildModifyPrompt(ctx: ModifyContext, config: AutoResearchConfig): string {
|
||||
const recent = ctx.recentLog.slice(-10).map(r =>
|
||||
@@ -60,15 +85,13 @@ async function modify(ctx: ModifyContext, config: AutoResearchConfig): Promise<s
|
||||
|
||||
console.log(' Claude Code making a change...');
|
||||
try {
|
||||
const result = execSync(
|
||||
`claude -p --dangerously-skip-permissions --allowedTools "Bash(npm:*),Bash(npx:*),Bash(git:*),Read,Edit,Write,Glob,Grep" --output-format text --no-session-persistence "${prompt.replace(/"/g, '\\"')}"`,
|
||||
{
|
||||
cwd: ROOT,
|
||||
timeout: 300_000,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: process.env,
|
||||
}
|
||||
// Keep command structure and the repository-derived prompt out of a shell.
|
||||
// Claude reads the prompt from stdin when -p has no positional prompt.
|
||||
const invocation = buildClaudeModifyInvocation(prompt);
|
||||
const result = execFileSync(
|
||||
invocation.command,
|
||||
invocation.args,
|
||||
invocation.options
|
||||
).trim();
|
||||
|
||||
// Extract description from Claude's response (last non-empty line or summary)
|
||||
@@ -135,4 +158,6 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
main();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildClaudeModifyInvocation } from '../autoresearch/commands/run.js';
|
||||
|
||||
describe('autoresearch Claude invocation', () => {
|
||||
it('passes repository-derived prompts through stdin instead of shell arguments', () => {
|
||||
const prompt = 'try $(touch /tmp/opencli-pwned) and `id` and "quotes"';
|
||||
const invocation = buildClaudeModifyInvocation(prompt);
|
||||
|
||||
expect(invocation.command).toBe('claude');
|
||||
expect(invocation.args).toEqual([
|
||||
'-p',
|
||||
'--dangerously-skip-permissions',
|
||||
'--allowedTools',
|
||||
'Bash(npm:*),Bash(npx:*),Bash(git:*),Read,Edit,Write,Glob,Grep',
|
||||
'--output-format',
|
||||
'text',
|
||||
'--no-session-persistence',
|
||||
]);
|
||||
expect(invocation.args.join(' ')).not.toContain(prompt);
|
||||
expect(invocation.options.input).toBe(prompt);
|
||||
expect(invocation.options).not.toHaveProperty('shell');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user