diff --git a/electron/chat/claude.test.ts b/electron/chat/claude.test.ts
index 39bd4543..130cf233 100644
--- a/electron/chat/claude.test.ts
+++ b/electron/chat/claude.test.ts
@@ -276,6 +276,20 @@ describe('Claude chat adapter', () => {
});
});
+ // The mode is otherwise known only from a turn's init message, and the chat
+ // view reads it to decide whether a mode can be picked.
+ it('reports the bypass mode as soon as it connects, before any turn', async () => {
+ const h = harness([], undefined, { skipPermissions: true });
+ await h.chat.start();
+ expect(h.chat.state.permissionMode).toBe('bypassPermissions');
+ });
+
+ it('reports no mode before a turn when it does not bypass', async () => {
+ const h = harness([], undefined, { permissionMode: 'acceptEdits' });
+ await h.chat.start();
+ expect(h.chat.state.permissionMode).toBeUndefined();
+ });
+
it('runs the mode the user picked for this chat, and only then overrides their settings', async () => {
const h = harness([], undefined, { permissionMode: 'acceptEdits' });
await h.chat.start();
diff --git a/electron/chat/claude.ts b/electron/chat/claude.ts
index 4b10064b..c0b4d248 100644
--- a/electron/chat/claude.ts
+++ b/electron/chat/claude.ts
@@ -164,6 +164,10 @@ export class ClaudeChat implements AgentChat {
if (this.isClosed())
throw new Error(this.state.error ?? 'Claude disconnected while connecting.');
this.noteUnadoptedSettingsMode(settingsMode);
+ // Claude reports its permission mode only in each turn's init message, so
+ // until the first prompt the state would not say this session bypasses
+ // permissions. It is known from how the session was launched.
+ if (this.opts.skipPermissions) this.state.permissionMode = 'bypassPermissions';
this.state.status = 'ready';
this.publish();
void this.refreshContextUsage();
diff --git a/src/components/AgentChatView.tsx b/src/components/AgentChatView.tsx
index 7dde9b48..5f0b651d 100644
--- a/src/components/AgentChatView.tsx
+++ b/src/components/AgentChatView.tsx
@@ -297,7 +297,9 @@ export function AgentChatView(props: {
...callbacks,
onReview: props.onReview ? reviewFile : undefined,
permissionMode: state()?.permissionMode ?? props.task.chatPermissionMode,
- permissionsDisabled: props.task.skipPermissions,
+ // The session's own mode, not the task flag: the flag can now change while a
+ // chat runs, and the session keeps the permissions it was started with.
+ permissionsDisabled: state()?.permissionMode === 'bypassPermissions',
onPermissionMode: provider() === 'claude' ? selectPermissionMode : undefined,
agentName: agentName(),
connection: connected,
diff --git a/src/components/TaskTitleBar.tsx b/src/components/TaskTitleBar.tsx
index 97535c93..edc6da00 100644
--- a/src/components/TaskTitleBar.tsx
+++ b/src/components/TaskTitleBar.tsx
@@ -9,6 +9,7 @@ import {
getTaskAttentionState,
toggleTaskFocusMode,
clearTaskLandingReview,
+ setTaskSkipPermissions,
getPrChecks,
getVerifyCommand,
isTaskCanvasVisible,
@@ -31,6 +32,7 @@ import { getTaskDockerBadgeLabel } from '../lib/docker';
import { displayTaskNameFromPrompt, shouldUsePromptDerivedTaskName } from '../lib/clean-task-name';
import type { Task } from '../store/types';
import { isLandedTaskState } from '../store/landing';
+import { resolveSkipPermissionsArgs } from '../../electron/shared/skip-permissions';
// Kinds without an entry stay silent: a configured-but-never-run command on
// every task would be noise, and cancelled runs carry no signal.
@@ -56,6 +58,23 @@ interface TaskTitleBarProps {
export function TaskTitleBar(props: TaskTitleBarProps) {
const dockerBadgeLabel = () => getTaskDockerBadgeLabel(props.task.dockerSource);
const isLandedTask = () => isLandedTaskState(props.task.landingState);
+ // Offered when any of the task's agents takes a skip-permissions flag, resolved
+ // by command so a definition restored without its flags still qualifies.
+ // Not for coordinators: sub-task propagation is fixed when the coordinator
+ // registers, so a change there would only partly apply until the next launch.
+ const offersSkipPermissionsToggle = () =>
+ !props.task.coordinatorMode &&
+ !isLandedTask() &&
+ props.task.agentIds.some((id) => {
+ const def = store.agents[id]?.def;
+ return !!def && resolveSkipPermissionsArgs(def).length > 0;
+ });
+ const skipPermissionsOn = () => props.task.skipPermissions === true;
+ const skipPermissionsTitle = () =>
+ (skipPermissionsOn()
+ ? "Skips the agent's permission prompts. Click to turn off."
+ : "Uses the agent's own permission settings. Click to skip its prompts.") +
+ ' Takes effect the next time the agent starts.';
const landingBadge = () => {
switch (props.task.landingState) {
case 'landed_pending_review':
@@ -239,6 +258,21 @@ export function TaskTitleBar(props: TaskTitleBarProps) {
)}
+