Add Auto permission mode (#63)
Build App / compute-version (push) Successful in 3s
Build Container / build-container (push) Successful in 1m58s
Secret Scan / scan (push) Successful in 4s
Build App / build-macos (push) Successful in 3m4s
Build App / build-windows (push) Successful in 5m48s
Build App / build-linux (push) Successful in 5m10s
Build App / create-tag (push) Successful in 4s
Build App / sync-to-github (push) Successful in 2m27s

Adds Claude Code's auto permission mode as a fifth option between Accept Edits and Bypass, across terminals, resumed sessions, the tab badge, the scheduler task runner and docs. Warns that Auto falls back to prompting when unavailable for the model/backend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #63.
This commit is contained in:
2026-09-25 17:02:06 +00:00
co-authored by Claude Opus 5.5
parent f8e3ec1150
commit 0d117e97fe
12 changed files with 81 additions and 16 deletions
@@ -469,6 +469,20 @@ mod tests {
assert!(!cmd[2].contains(" -n "), "empty name must add no flag: {}", cmd[2]);
}
/// Auto mode is passed as a `--permission-mode` value, not its own flag.
#[test]
fn build_terminal_cmd_passes_auto_permission_mode() {
let mut p = project("anthropic", serde_json::Value::Null);
p.permission_mode = Some(crate::models::project::PermissionMode::Auto);
let cmd = build_claude_terminal_cmd(&p, None, None);
assert!(
cmd[2].contains("exec claude '--permission-mode' 'auto'"),
"got: {}",
cmd[2]
);
}
/// The Bedrock-profile path keeps its AWS validation *and* gains the
/// prelude, immediately before the exec.
#[test]
+5
View File
@@ -166,6 +166,9 @@ pub enum PermissionMode {
Default,
/// Auto-accept file edits, prompt for everything else.
AcceptEdits,
/// Claude Code's classifier approves safe actions and blocks risky ones,
/// without prompting.
Auto,
/// Skip all permission prompts.
Bypass,
}
@@ -180,6 +183,7 @@ impl PermissionMode {
PermissionMode::AcceptEdits => {
vec!["--permission-mode".to_string(), "acceptEdits".to_string()]
}
PermissionMode::Auto => vec!["--permission-mode".to_string(), "auto".to_string()],
PermissionMode::Bypass => vec!["--dangerously-skip-permissions".to_string()],
}
}
@@ -191,6 +195,7 @@ impl PermissionMode {
PermissionMode::Plan => "plan",
PermissionMode::Default => "default",
PermissionMode::AcceptEdits => "acceptEdits",
PermissionMode::Auto => "auto",
PermissionMode::Bypass => "bypass",
}
}
+1
View File
@@ -26,6 +26,7 @@ const MODE_BADGE: Record<PermissionMode, { text: string; className: string }> =
plan: { text: "plan", className: "bg-[var(--bg-tertiary)] text-[var(--text-secondary)]" },
default: { text: "ask", className: "bg-[var(--bg-tertiary)] text-[var(--text-secondary)]" },
acceptEdits: { text: "edits", className: "bg-[var(--accent-muted)] text-[var(--accent)]" },
auto: { text: "auto", className: "bg-[var(--accent-muted)] text-[var(--accent)]" },
bypass: { text: "bypass", className: "bg-[var(--warning-muted)] text-[var(--warning)]" },
};
@@ -75,11 +75,11 @@ describe("PermissionModeControl", () => {
vi.clearAllMocks();
});
it("renders all four modes as a radio group with the effective one checked", () => {
it("renders all five modes as a radio group with the effective one checked", () => {
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
const group = screen.getByRole("radiogroup", { name: "Permission mode" });
expect(group).toBeInTheDocument();
expect(screen.getAllByRole("radio")).toHaveLength(4);
expect(screen.getAllByRole("radio")).toHaveLength(5);
expect(screen.getByRole("radio", { name: "Default" })).toHaveAttribute(
"aria-checked",
"true",
@@ -92,6 +92,14 @@ describe("PermissionModeControl", () => {
expect(onChange).toHaveBeenCalledWith("acceptEdits");
});
it("offers Auto between Accept Edits and Bypass", () => {
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
const labels = screen.getAllByRole("radio").map((r) => r.textContent);
expect(labels).toEqual(["Plan", "Default", "Accept Edits", "Auto", "Bypass"]);
fireEvent.click(screen.getByRole("radio", { name: "Auto" }));
expect(onChange).toHaveBeenCalledWith("auto");
});
it("moves selection with the arrow keys", () => {
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
fireEvent.keyDown(screen.getByRole("radiogroup", { name: "Permission mode" }), {
@@ -9,6 +9,11 @@ export const PERMISSION_MODES: Segment<PermissionMode>[] = [
label: "Accept Edits",
hint: "File edits are auto-approved; other tools still prompt.",
},
{
value: "auto",
label: "Auto",
hint: "A safety classifier approves routine actions and blocks risky ones, without prompting.",
},
{
value: "bypass",
label: "Bypass",
@@ -159,12 +159,19 @@ describe("TaskEditorModal", () => {
});
it("warns that a headless run cannot answer a permission prompt", async () => {
// Bypass is the only mode where an unattended run is safe from stalling.
// Bypass (and Auto, below) are the modes where an unattended run cannot stall.
await renderEditor(null, { ...baseProject, permission_mode: "bypass" });
expect(screen.getByText(/headless/i)).toBeInTheDocument();
expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull();
});
it("tells Auto mode that blocked actions are denied, and warns of the fallback", async () => {
await renderEditor(null, { ...baseProject, permission_mode: "auto" });
expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull();
expect(screen.getByText(/blocks are denied/i)).toBeInTheDocument();
expect(screen.getByText(/falls back to prompting/i)).toBeInTheDocument();
});
it("spells out the stall risk in any non-Bypass mode", async () => {
await renderEditor(null, { ...baseProject, permission_mode: "default" });
expect(screen.getByText(/cannot answer a permission prompt/i)).toBeInTheDocument();
@@ -301,11 +301,18 @@ export default function TaskEditorModal({ project, task, onClose, onSaved }: Pro
terminal attached, using this project&rsquo;s permission mode (
<strong className="text-[var(--text-primary)]">{modeLabel}</strong>).
</p>
{mode !== "bypass" && (
{mode === "auto" && (
<p className="text-xs text-[var(--warning)]">
In Auto mode, actions the safety classifier blocks are denied and the run carries on
without them. If Auto isn&rsquo;t available for this project&rsquo;s model or backend,
Claude Code falls back to prompting and the task may stall.
</p>
)}
{mode !== "bypass" && mode !== "auto" && (
<p className="text-xs text-[var(--warning)]">
A headless run cannot answer a permission prompt. In {modeLabel} mode the task may
stall and produce an empty log; set the mode to Bypass in the Config tab for
unattended runs.
stall and produce an empty log; set the mode to Auto or Bypass in the Config tab
for unattended runs.
</p>
)}
</div>
+1 -1
View File
@@ -126,7 +126,7 @@ export const CUSTOM_ENDPOINT_BACKENDS: readonly Backend[] = [
];
/** Mirrors Rust `PermissionMode` (serde camelCase). */
export type PermissionMode = "plan" | "default" | "acceptEdits" | "bypass";
export type PermissionMode = "plan" | "default" | "acceptEdits" | "auto" | "bypass";
export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token";