Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cli/commands/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export async function send(
// Spawn agent — spawnAgent handles --resume vs --session-id internally
const result = await spawnAgent(agent.toolType, agent.cwd, message, options.session, {
readOnlyMode: options.readOnly,
customModel: agent.customModel,
});
const response = buildResponse(agentId, agent.name, result, agent.toolType);

Expand Down
35 changes: 31 additions & 4 deletions src/cli/services/agent-spawner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ const CLAUDE_ARGS = [
'--verbose',
'--output-format',
'stream-json',
'--dangerously-skip-permissions',
];

// Permission bypass arg for Claude — skipped in read-only mode
const CLAUDE_YOLO_ARGS = ['--dangerously-skip-permissions'];
Comment on lines +27 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing test asserts YOLO flag is still present in read-only mode

The removal of --dangerously-skip-permissions from CLAUDE_ARGS and its relegation to CLAUDE_YOLO_ARGS (only added in non-read-only mode) is the correct core change of this PR. However, the existing test at src/__tests__/cli/services/agent-spawner.test.ts:1179 explicitly asserts that --dangerously-skip-permissions is still present when readOnlyMode: true:

// From agent-spawner.test.ts, line ~1178-1179
// Should still have base args
expect(args).toContain('--print');
expect(args).toContain('--dangerously-skip-permissions'); // ← contradicts this PR's intent

This assertion was written against the old behaviour where --dangerously-skip-permissions lived in CLAUDE_ARGS and was therefore always included. After this PR it will no longer be present in read-only mode, so the test will fail. The assertion should be inverted:

expect(args).not.toContain('--dangerously-skip-permissions');

Additionally, a new assertion checking it is present in normal (non-read-only) mode would close the gap in coverage.


// Cached paths per agent type (resolved once at startup)
const cachedPaths: Map<string, string> = new Map();

Expand Down Expand Up @@ -192,6 +194,9 @@ async function spawnClaudeAgent(
if (def?.readOnlyEnvOverrides) {
Object.assign(env, def.readOnlyEnvOverrides);
}
} else {
// Only bypass permissions in non-read-only mode
args.push(...CLAUDE_YOLO_ARGS);
}

if (agentSessionId) {
Expand Down Expand Up @@ -355,7 +360,8 @@ async function spawnJsonLineAgent(
cwd: string,
prompt: string,
agentSessionId?: string,
_readOnlyMode?: boolean
readOnlyMode?: boolean,
customModel?: string
): Promise<AgentResult> {
return new Promise((resolve) => {
const env = buildExpandedEnv();
Expand All @@ -368,11 +374,29 @@ async function spawnJsonLineAgent(
}
}

// Apply read-only mode env overrides from agent definition
if (readOnlyMode && def?.readOnlyEnvOverrides) {
Object.assign(env, def.readOnlyEnvOverrides);
}

// Build args from agent definition
const args: string[] = [];
if (def?.batchModePrefix) args.push(...def.batchModePrefix);
if (def?.batchModeArgs) args.push(...def.batchModeArgs);

// In read-only mode, filter out YOLO/bypass args from batchModeArgs
// (they override read-only flags). In normal mode, apply all batchModeArgs.
if (def?.batchModeArgs) {
if (readOnlyMode && def.yoloModeArgs?.length) {
const yoloSet = new Set(def.yoloModeArgs);
args.push(...def.batchModeArgs.filter((a) => !yoloSet.has(a)));
} else {
args.push(...def.batchModeArgs);
}
}
Comment on lines +386 to +397
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini CLI may hang in read-only batch mode after -y is stripped

For the Gemini CLI agent, batchModeArgs: ['-y'] and yoloModeArgs: ['-y'] are the same flag. In read-only mode, the new filtering logic strips -y from batchModeArgs:

if (readOnlyMode && def.yoloModeArgs?.length) {
    const yoloSet = new Set(def.yoloModeArgs);
    args.push(...def.batchModeArgs.filter((a) => !yoloSet.has(a)));
}

-y is Gemini's auto-confirm flag; without it Gemini may prompt for interactive confirmation on any tool use or file access. Since stdin is closed immediately after spawning (child.stdin?.end()), this would cause Gemini to receive EOF on its prompt input and either hang or exit with an error.

OpenCode faced the same problem and was fixed by keeping "*":"allow" in its readOnlyEnvOverrides. For Gemini there is no readOnlyEnvOverrides defined, and readOnlyArgs is intentionally empty (readOnlyCliEnforced: false).

One option is to add a Gemini-specific readOnlyEnvOverrides that sets GEMINI_YOLO=1 (or equivalent) to prevent interactive prompts while still relying on the system prompt for read-only enforcement. Alternatively, keep -y for Gemini even in read-only mode (similar to the OpenCode fix) and rely solely on the system prompt to prevent writes.


if (def?.jsonOutputArgs) args.push(...def.jsonOutputArgs);
if (readOnlyMode && def?.readOnlyArgs) args.push(...def.readOnlyArgs);
if (customModel && def?.modelArgs) args.push(...def.modelArgs(customModel));

if (agentSessionId && def?.resumeArgs) {
args.push(...def.resumeArgs(agentSessionId));
Expand Down Expand Up @@ -477,6 +501,8 @@ export interface SpawnAgentOptions {
agentSessionId?: string;
/** Run in read-only/plan mode (uses centralized agent definitions for provider-specific flags) */
readOnlyMode?: boolean;
/** Custom model ID from agent config (e.g., 'github-copilot/gpt-5-mini') */
customModel?: string;
}

/**
Expand All @@ -490,13 +516,14 @@ export async function spawnAgent(
options?: SpawnAgentOptions
): Promise<AgentResult> {
const readOnly = options?.readOnlyMode;
const customModel = options?.customModel;

if (toolType === 'claude-code') {
return spawnClaudeAgent(cwd, prompt, agentSessionId, readOnly);
}

if (hasCapability(toolType, 'usesJsonLineOutput')) {
return spawnJsonLineAgent(toolType, cwd, prompt, agentSessionId);
return spawnJsonLineAgent(toolType, cwd, prompt, agentSessionId, readOnly, customModel);
}

return {
Expand Down
4 changes: 3 additions & 1 deletion src/cli/services/batch-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,9 @@ export async function* runPlaybook(
}

// Spawn agent with combined prompt + document
const result = await spawnAgent(session.toolType, session.cwd, finalPrompt);
const result = await spawnAgent(session.toolType, session.cwd, finalPrompt, undefined, {
customModel: session.customModel,
});

const elapsedMs = Date.now() - taskStartTime;

Expand Down
6 changes: 4 additions & 2 deletions src/main/agents/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,12 @@ export const AGENT_DEFINITIONS: AgentDefinition[] = [
OPENCODE_CONFIG_CONTENT:
'{"permission":{"*":"allow","external_directory":"allow","question":"deny"},"tools":{"question":false}}',
},
// In read-only mode, strip blanket permission grants so the plan agent can't auto-approve file writes.
// In read-only mode, keep blanket permission grants to prevent stdin prompts that hang batch mode.
// Read-only enforcement comes from --agent plan (readOnlyArgs), not env config.
// Keep question tool disabled to prevent stdin hangs in batch mode.
readOnlyEnvOverrides: {
OPENCODE_CONFIG_CONTENT: '{"permission":{"question":"deny"},"tools":{"question":false}}',
OPENCODE_CONFIG_CONTENT:
'{"permission":{"*":"allow","external_directory":"allow","question":"deny"},"tools":{"question":false}}',
},
// Agent-specific configuration options shown in UI
configOptions: [
Expand Down
1 change: 1 addition & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface SessionInfo {
cwd: string;
projectRoot: string;
autoRunFolderPath?: string;
customModel?: string;
}

// Usage statistics from AI agent CLI (Claude Code, Codex, etc.)
Expand Down