Add Auto Review permission mode for Claude Code (#928)

* Add Auto Review permission mode

* Align Codex Auto Review with auto permissions

* Defer Codex Auto Review to dedicated PR

* fix(app): drop unconditional snapshot refresh on mode selector open

Opening the mode selector was reusing the model selector refresh path, which forced a provider snapshot refresh every time. That made the agent status bar briefly lose its selected provider data and flash empty while the picker opened.

Keep the stale model refresh on the model selector only, and let the mode selector open from the existing snapshot state.

* refactor(claude): rename Claude auto permission mode label to "Auto mode"

Claude Code itself calls this permission setting Auto mode in its CLI/TUI, so match that label in Paseo. The supporting test names and transport eligibility errors now use the same terminology.

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
c4605
2026-05-13 07:58:26 +02:00
committed by GitHub
parent b8a3eefd47
commit 16c27d7404
3 changed files with 172 additions and 5 deletions

View File

@@ -39,6 +39,13 @@ const CLAUDE_MODES: AgentProviderModeDefinition[] = [
icon: "ShieldCheck",
colorTier: "safe",
},
{
id: "auto",
label: "Auto mode",
description: "Uses a model classifier to review permission prompts automatically",
icon: "ShieldQuestionMark",
colorTier: "moderate",
},
{
id: "acceptEdits",
label: "Accept File Edits",

View File

@@ -140,6 +140,14 @@ function extractStringLogArgs(calls: unknown[][]): string[] {
return calls.flatMap((args) => args.filter((arg): arg is string => typeof arg === "string"));
}
function restoreEnvValue(key: string, previousValue: string | undefined): void {
if (previousValue === undefined) {
delete process.env[key];
return;
}
process.env[key] = previousValue;
}
async function collectUntilTerminal(
stream: AsyncGenerator<AgentStreamEvent>,
): Promise<AgentStreamEvent[]> {
@@ -165,6 +173,112 @@ afterEach(() => {
sdkQueryFactory.mockReset();
});
test("exposes and applies auto permission mode", async () => {
const queryMock = createBaseQueryMock(vi.fn(async () => ({ done: true, value: undefined })));
sdkQueryFactory.mockImplementation(() => queryMock);
const session = await createSession();
try {
await expect(session.getAvailableModes()).resolves.toEqual(
expect.arrayContaining([
{
id: "auto",
label: "Auto mode",
description: "Uses a model classifier to review permission prompts automatically",
},
]),
);
await session.setMode("auto");
expect(queryMock.setPermissionMode).toHaveBeenCalledWith("auto");
expect(await session.getCurrentMode()).toBe("auto");
} finally {
await session.close();
}
});
test("rejects auto mode when Claude Code uses Bedrock", async () => {
const previousBedrock = process.env.CLAUDE_CODE_USE_BEDROCK;
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
const session = await createSession();
try {
await expect(session.setMode("auto")).rejects.toThrow(
"Claude Auto mode requires the Anthropic API and is not supported when Claude Code uses Bedrock",
);
expect(sdkQueryFactory).not.toHaveBeenCalled();
} finally {
restoreEnvValue("CLAUDE_CODE_USE_BEDROCK", previousBedrock);
await session.close();
}
});
test("allows launch env to disable inherited Bedrock transport for auto mode", async () => {
const previousBedrock = process.env.CLAUDE_CODE_USE_BEDROCK;
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
const queryMock = createBaseQueryMock(vi.fn(async () => ({ done: true, value: undefined })));
sdkQueryFactory.mockImplementation(() => queryMock);
const client = new ClaudeAgentClient({
logger: createTestLogger(),
queryFactory: sdkQueryFactory,
resolveBinary: async () => "/test/claude/bin",
});
const session = await client.createSession(
{
provider: "claude",
cwd: process.cwd(),
},
{ env: { CLAUDE_CODE_USE_BEDROCK: "0" } },
);
try {
await session.setMode("auto");
expect(queryMock.setPermissionMode).toHaveBeenCalledWith("auto");
expect(await session.getCurrentMode()).toBe("auto");
} finally {
restoreEnvValue("CLAUDE_CODE_USE_BEDROCK", previousBedrock);
await session.close();
}
});
test("fails an auto mode turn when Claude Code uses Vertex", async () => {
const previousVertex = process.env.CLAUDE_CODE_USE_VERTEX;
process.env.CLAUDE_CODE_USE_VERTEX = "true";
sdkQueryFactory.mockImplementation(() => {
throw new Error("query should not start");
});
const client = new ClaudeAgentClient({
logger: createTestLogger(),
queryFactory: sdkQueryFactory,
resolveBinary: async () => "/test/claude/bin",
});
const session = await client.createSession({
provider: "claude",
cwd: process.cwd(),
modeId: "auto",
});
try {
const events = await collectUntilTerminal(streamSession(session, "hello"));
const failure = events.find(
(event): event is Extract<AgentStreamEvent, { type: "turn_failed" }> =>
event.type === "turn_failed",
);
expect(failure?.error).toContain(
"Claude Auto mode requires the Anthropic API and is not supported when Claude Code uses Vertex",
);
expect(sdkQueryFactory).not.toHaveBeenCalled();
} finally {
restoreEnvValue("CLAUDE_CODE_USE_VERTEX", previousVertex);
await session.close();
}
});
test("logs redacted query summary and never leaks sentinel secrets", async () => {
const envSecret = "PASEO_ENV_SENTINEL_SECRET";
const runtimeSecret = "PASEO_RUNTIME_SENTINEL_SECRET";

View File

@@ -196,6 +196,11 @@ const DEFAULT_MODES: AgentMode[] = [
label: "Always Ask",
description: "Prompts for permission the first time a tool is used",
},
{
id: "auto",
label: "Auto mode",
description: "Uses a model classifier to review permission prompts automatically",
},
{
id: "acceptEdits",
label: "Accept File Edits",
@@ -660,6 +665,41 @@ function isPermissionMode(value: string | undefined): value is PermissionMode {
return typeof value === "string" && VALID_CLAUDE_MODES.has(value);
}
function isTruthyEnvValue(value: string | undefined): boolean {
const normalized = value?.trim().toLowerCase();
return (
normalized !== undefined &&
normalized.length > 0 &&
normalized !== "0" &&
normalized !== "false" &&
normalized !== "no" &&
normalized !== "off"
);
}
function detectIneligibleAutoModeTransport(env: NodeJS.ProcessEnv): "Bedrock" | "Vertex" | null {
if (isTruthyEnvValue(env.CLAUDE_CODE_USE_BEDROCK)) {
return "Bedrock";
}
if (isTruthyEnvValue(env.CLAUDE_CODE_USE_VERTEX)) {
return "Vertex";
}
return null;
}
function assertClaudeAutoModeEligible(mode: PermissionMode, env: NodeJS.ProcessEnv): void {
if (mode !== "auto") {
return;
}
const transport = detectIneligibleAutoModeTransport(env);
if (transport === null) {
return;
}
throw new Error(
`Claude Auto mode requires the Anthropic API and is not supported when Claude Code uses ${transport}. Select another permission mode or unset the ${transport === "Bedrock" ? "CLAUDE_CODE_USE_BEDROCK" : "CLAUDE_CODE_USE_VERTEX"} environment variable.`,
);
}
function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial<AgentSessionConfig> {
if (!isMetadata(metadata)) {
return {};
@@ -1735,6 +1775,7 @@ class ClaudeAgentSession implements AgentSession {
}
const normalized = isPermissionMode(modeId) ? modeId : "default";
assertClaudeAutoModeEligible(normalized, this.buildSdkEnv(this.config.extra?.claude));
const previousMode = this.currentMode;
const activeQuery = await this.ensureQuery();
await activeQuery.setPermissionMode(normalized);
@@ -2261,11 +2302,8 @@ class ClaudeAgentSession implements AgentSession {
.join("\n\n");
}
private async buildOptions(): Promise<ClaudeOptions> {
const { thinking, effort } = this.resolveThinkingConfig();
const appendedSystemPrompt = this.buildAppendedSystemPrompt();
const extraClaudeOptions = this.config.extra?.claude;
const sdkEnv = createProviderEnv({
private buildSdkEnv(extraClaudeOptions: Partial<ClaudeOptions> | undefined): NodeJS.ProcessEnv {
return createProviderEnv({
baseEnv: process.env,
runtimeSettings: this.runtimeSettings,
overlays: [
@@ -2278,6 +2316,14 @@ class ClaudeAgentSession implements AgentSession {
this.launchEnv,
],
});
}
private async buildOptions(): Promise<ClaudeOptions> {
const { thinking, effort } = this.resolveThinkingConfig();
const appendedSystemPrompt = this.buildAppendedSystemPrompt();
const extraClaudeOptions = this.config.extra?.claude;
const sdkEnv = this.buildSdkEnv(extraClaudeOptions);
assertClaudeAutoModeEligible(this.currentMode, sdkEnv);
const claudeBinary = await this.resolveBinary();
this.logger.debug(