Add Auto permission mode #63

Merged
jknapp merged 3 commits from feat/auto-permission-mode into main 2026-09-25 17:02:07 +00:00
4 changed files with 15 additions and 10 deletions
Showing only changes of commit 99d8493008 - Show all commits
+5 -3
View File
@@ -484,7 +484,8 @@ the way they did: one that had Full Permissions on becomes **Bypass**, one that
**Auto** sits between Accept Edits and Bypass: Claude Code's own classifier reviews each action, **Auto** sits between Accept Edits and Bypass: Claude Code's own classifier reviews each action,
lets routine work through and blocks things that look risky (such as destructive or lets routine work through and blocks things that look risky (such as destructive or
exfiltrating commands) — no prompts either way. Whether it is available depends on your Claude exfiltrating commands) — no prompts either way. Whether it is available depends on your Claude
Code account and model; see Claude Code's documentation on permission modes. Code account, model and backend (local and OpenAI-compatible backends won't qualify). When it
isn't available, Claude Code quietly starts in its normal prompting mode instead.
### When a change takes effect ### When a change takes effect
@@ -502,7 +503,8 @@ Code account and model; see Claude Code's documentation on permission modes.
> Scheduled tasks run headless (`claude -p`) and cannot answer a permission prompt. In any mode > Scheduled tasks run headless (`claude -p`) and cannot answer a permission prompt. In any mode
> other than **Auto** or **Bypass**, a task may simply stop early when Claude Code asks for > other than **Auto** or **Bypass**, a task may simply stop early when Claude Code asks for
> approval. In **Auto**, actions the classifier blocks are denied and the run carries on without > approval. In **Auto**, actions the classifier blocks are denied and the run carries on without
> them. Its run log records which mode it used. > them — but if Auto isn't available for the project's model or backend, Claude Code falls back
> to prompting and the task can stall the same way. Its run log records which mode it used.
--- ---
@@ -1357,7 +1359,7 @@ with `--dangerously-skip-permissions`. Because the mode travels into the contain
environment variable, **stop and start the project** after changing it for the scheduler to see the environment variable, **stop and start the project** after changing it for the scheduler to see the
change. Remember that a headless run cannot answer a permission prompt, so in any mode other than change. Remember that a headless run cannot answer a permission prompt, so in any mode other than
**Auto** or **Bypass** a task may stop early when Claude Code asks for approval (in Auto, blocked **Auto** or **Bypass** a task may stop early when Claude Code asks for approval (in Auto, blocked
actions are denied instead); the run log records the mode actions are denied instead, unless Auto is unavailable and Claude Code falls back to prompting); the run log records the mode
that was used. that was used.
### Creating Tasks ### Creating Tasks
@@ -165,10 +165,11 @@ describe("TaskEditorModal", () => {
expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull(); expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull();
}); });
it("tells Auto mode that blocked actions are denied, not prompted", async () => { it("tells Auto mode that blocked actions are denied, and warns of the fallback", async () => {
await renderEditor(null, { ...baseProject, permission_mode: "auto" }); await renderEditor(null, { ...baseProject, permission_mode: "auto" });
expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull(); expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull();
expect(screen.getByText(/blocks are denied/i)).toBeInTheDocument(); 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 () => { it("spells out the stall risk in any non-Bypass mode", async () => {
@@ -302,9 +302,10 @@ export default function TaskEditorModal({ project, task, onClose, onSaved }: Pro
<strong className="text-[var(--text-primary)]">{modeLabel}</strong>). <strong className="text-[var(--text-primary)]">{modeLabel}</strong>).
</p> </p>
{mode === "auto" && ( {mode === "auto" && (
<p className="text-xs text-[var(--text-secondary)]"> <p className="text-xs text-[var(--warning)]">
In Auto mode nothing prompts: actions the safety classifier blocks are denied and the In Auto mode, actions the safety classifier blocks are denied and the run carries on
run carries on without them, so check the log if a task seems to have skipped a step. 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> </p>
)} )}
{mode !== "bypass" && mode !== "auto" && ( {mode !== "bypass" && mode !== "auto" && (
+4 -3
View File
@@ -61,9 +61,10 @@ TASK_TYPE=$(jq -r '.type' "$TASK_FILE")
# project's permission setting. Keep this mapping in sync with # project's permission setting. Keep this mapping in sync with
# PermissionMode::cli_args() in app/src-tauri/src/models/project.rs. # PermissionMode::cli_args() in app/src-tauri/src/models/project.rs.
# NOTE: headless `claude -p` runs cannot answer a permission prompt, so any # NOTE: headless `claude -p` runs cannot answer a permission prompt, so any
# mode other than "bypass" or "auto" means the task may stop early when Claude # mode other than "bypass" means the task may stop early when Claude Code asks
# Code asks for permission. In "auto", actions the classifier blocks are # for permission. In "auto", classifier-blocked actions are denied instead of
# denied rather than prompted, so the run continues without them. Unset or unrecognized values pass no flag (Claude's default). # prompted, but if auto mode is unavailable for the session's model/backend,
# Claude Code falls back to prompting and the same stall applies. Unset or unrecognized values pass no flag (Claude's default).
PERMISSION_ARGS=() PERMISSION_ARGS=()
case "${TRIPLE_C_PERMISSION_MODE:-}" in case "${TRIPLE_C_PERMISSION_MODE:-}" in
plan) PERMISSION_ARGS=(--permission-mode plan) ;; plan) PERMISSION_ARGS=(--permission-mode plan) ;;