diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql
index 39ebe9f1..68aca4e1 100644
--- a/.pql/changelog/ticket_history/2026-06.sql
+++ b/.pql/changelog/ticket_history/2026-06.sql
@@ -7197,3 +7197,5 @@ All actions go through T-480''s CLI verbs; this widget never writes the registry
Done: the Claude pane account badge (ClaudeAccountBadge) in the pane chrome trailing slot — shows the bound account (or ''default''), colour-tinted per account via accountAccent (theme tokens, hash-indexed), tap opens the account picker; hidden when no accounts registered. The welcome-view accounts section is split to T-486 (welcome is builtin-decoupled; needs a contribution point to avoid welcome->claude peer coupling).', NULL, '2026-06-27 22:43:31', '2026-06-27 22:43:31.723', '2026-06-27 22:43:31.723', NULL, 'd3146ee6d4abe36714b1b160e94d3d3a', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WBTT3ESK6QG0XZ1VN2G', 'status', 'in_progress', 'done', NULL, '2026-06-27 22:43:31', '2026-06-27 22:43:31.769', '2026-06-27 22:43:31.769', NULL, '34a8c26c8c71d27fdcb1e60e3ffa1ea0', 2) ON CONFLICT(hash) DO NOTHING;
+INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGN13H42TP2MP3T27CTSCDRW', 'description', NULL, 'Done: ClaudeLoginDialog hosts a parameterized TerminalPane running ''claude login'' with CLAUDE_CONFIG_DIR=
in a modal (ctx.dialog.show), wired from the accountActionChannel ''login'' action. TerminalPane gained argv/env/cwdOverride params (default still the login shell). The CLI owns the OAuth browser flow (D-64). No runtime dep on builtin.terminal — the login pane reuses the TerminalPane widget, which spawns via the always-present pane.spawn IPC. Tests: parameterized spawn args + dialog host wiring.', NULL, '2026-06-27 22:52:31', '2026-06-27 22:52:31.121', '2026-06-27 22:52:31.121', NULL, '1e158c1dcb4bf9eafecb8c26cc4936d2', 2) ON CONFLICT(hash) DO NOTHING;
+INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGN13H42TP2MP3T27CTSCDRW', 'status', 'backlog', 'done', NULL, '2026-06-27 22:52:31', '2026-06-27 22:52:31.156', '2026-06-27 22:52:31.156', NULL, '620a851447b74c2553407fac431b1e18', 2) ON CONFLICT(hash) DO NOTHING;
diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql
index f31869fc..66a18fbb 100644
--- a/.pql/changelog/tickets/2026-06.sql
+++ b/.pql/changelog/tickets/2026-06.sql
@@ -9157,3 +9157,5 @@ All actions go through T-480''s CLI verbs; this widget never writes the registry
Done: the Claude pane account badge (ClaudeAccountBadge) in the pane chrome trailing slot — shows the bound account (or ''default''), colour-tinted per account via accountAccent (theme tokens, hash-indexed), tap opens the account picker; hidden when no accounts registered. The welcome-view accounts section is split to T-486 (welcome is builtin-decoupled; needs a contribution point to avoid welcome->claude peer coupling).', 'done', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-27 22:43:31.769', NULL, '065f5914ac6b963342be92e0ea65ab82', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGN13H42TP2MP3T27CTSCDRW', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'claude account login: spawn a CLAUDE_CONFIG_DIR= claude login terminal pane', 'Done: ClaudeLoginDialog hosts a parameterized TerminalPane running ''claude login'' with CLAUDE_CONFIG_DIR= in a modal (ctx.dialog.show), wired from the accountActionChannel ''login'' action. TerminalPane gained argv/env/cwdOverride params (default still the login shell). The CLI owns the OAuth browser flow (D-64). No runtime dep on builtin.terminal — the login pane reuses the TerminalPane widget, which spawns via the always-present pane.spawn IPC. Tests: parameterized spawn args + dialog host wiring.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-27 19:17:59.712', '2026-06-27 22:52:31.121', NULL, 'd52f43050d3e2b14eecd47aff90ea836', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGN13H42TP2MP3T27CTSCDRW', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'claude account login: spawn a CLAUDE_CONFIG_DIR= claude login terminal pane', 'Done: ClaudeLoginDialog hosts a parameterized TerminalPane running ''claude login'' with CLAUDE_CONFIG_DIR= in a modal (ctx.dialog.show), wired from the accountActionChannel ''login'' action. TerminalPane gained argv/env/cwdOverride params (default still the login shell). The CLI owns the OAuth browser flow (D-64). No runtime dep on builtin.terminal — the login pane reuses the TerminalPane widget, which spawns via the always-present pane.spawn IPC. Tests: parameterized spawn args + dialog host wiring.', 'done', 'medium', NULL, NULL, NULL, '2026-06-27 19:17:59.712', '2026-06-27 22:52:31.156', NULL, 'c0e79a87fa88a49a38e8b469b194a96a', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ed52360..896d3e81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Added
+- **Claude account login pane.** `account login` (and the UI add/re-login
+ affordances) open a modal terminal running `CLAUDE_CONFIG_DIR= claude
+ login`; the CLI owns the OAuth flow, credentials land in that account's dir.
+ (T-485, epic T-476)
- **Claude pane account badge.** The pane header shows which account this repo
is bound to (colour-tinted per account); tap to switch. Hidden when no
accounts are registered. (T-481, epic T-476)
diff --git a/lib/builtin/claude/src/account_login_dialog.dart b/lib/builtin/claude/src/account_login_dialog.dart
new file mode 100644
index 00000000..ecfda5cc
--- /dev/null
+++ b/lib/builtin/claude/src/account_login_dialog.dart
@@ -0,0 +1,94 @@
+/// Modal that hosts `CLAUDE_CONFIG_DIR= claude login` in a terminal pane
+/// (T-485, epic T-476). clide writes no auth code: the Claude CLI owns the OAuth
+/// browser flow, and clide just provides the TTY + the per-account config dir,
+/// so the resulting credentials land in `` rather than the global
+/// `~/.claude` (D-64 — one CLI-initiated browser flow, on explicit action,
+/// nothing in the background). No-Material (D-7); shown via the DialogRouter.
+library;
+
+import 'package:clide/builtin/terminal/src/terminal_pane.dart';
+import 'package:clide/widgets/widgets.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+
+class ClaudeLoginDialog extends StatelessWidget {
+ const ClaudeLoginDialog({super.key, required this.name, required this.dir, required this.onClose, this.cwd});
+
+ /// Account display name (for the title).
+ final String name;
+
+ /// The account's `CLAUDE_CONFIG_DIR` — where `claude login` writes credentials.
+ final String dir;
+
+ /// Working directory for the spawned `claude login` (defaults to the
+ /// workspace); irrelevant to auth, but keeps the pane oriented.
+ final String? cwd;
+
+ final VoidCallback onClose;
+
+ KeyEventResult _onKey(FocusNode node, KeyEvent e) {
+ if (e is KeyDownEvent && e.logicalKey == LogicalKeyboardKey.escape) {
+ onClose();
+ return KeyEventResult.handled;
+ }
+ return KeyEventResult.ignored;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = ClideSettings.theme.of(context).surface;
+ return Focus(
+ autofocus: true,
+ onKeyEvent: _onKey,
+ child: Container(
+ width: 760,
+ decoration: BoxDecoration(
+ color: theme.panelBackground,
+ border: Border.all(color: theme.globalBorder),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(14, 12, 14, 4),
+ child: Row(
+ children: [
+ Expanded(
+ child: ClideText('Sign in: $name', fontSize: clideFontBody, color: theme.globalForeground),
+ ),
+ Semantics(
+ button: true,
+ label: 'Close',
+ excludeSemantics: true,
+ child: ClideTappable(
+ key: const Key('account-login-close'),
+ cursor: SystemMouseCursors.click,
+ onTap: onClose,
+ builder: (ctx, hovered, _) =>
+ ClideIcon(PhosphorIcons.byName('x'), size: 14, color: hovered ? theme.globalForeground : theme.globalTextMuted),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
+ child: ClideText('Running `claude login` against $dir — finish the browser sign-in, then close.', muted: true, fontSize: clideFontSmall),
+ ),
+ // Fixed height — TerminalPane needs a bounded box; the dialog itself
+ // sizes to its content (mainAxisSize.min).
+ Padding(
+ padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
+ child: SizedBox(
+ height: 380,
+ child: TerminalPane(argv: const ['claude', 'login'], env: {'CLAUDE_CONFIG_DIR': dir}, cwdOverride: cwd),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/builtin/claude/src/extension.dart b/lib/builtin/claude/src/extension.dart
index 0768c3ae..7acf5541 100644
--- a/lib/builtin/claude/src/extension.dart
+++ b/lib/builtin/claude/src/extension.dart
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:clide/clide.dart';
import 'package:clide/builtin/claude/src/account_registry.dart';
+import 'package:clide/builtin/claude/src/account_login_dialog.dart';
import 'package:clide/builtin/claude/src/account_settings_control.dart';
import 'package:clide/builtin/claude/src/activity_cluster.dart' show foldLevelFromName, kActivityFoldLevelKey, nextFoldLevel;
import 'package:clide/builtin/claude/src/claude_config.dart';
@@ -47,6 +48,9 @@ class ClaudeExtension extends ClideExtension {
@override
String get version => '0.2.0';
@override
+ // No runtime dependency on builtin.terminal: the login pane (T-485) reuses the
+ // TerminalPane *widget* (a code import), which spawns via the always-present
+ // pane.spawn IPC — it doesn't need the terminal extension activated.
List get dependsOn => const [];
ClideExtensionContext? _ctx;
@@ -542,9 +546,15 @@ class ClaudeExtension extends ClideExtension {
case 'purge':
final dir = m.data['dir'] as String?;
if (dir != null) unawaited(_purgeAccountDir(dir));
- // 'login' would spawn a `CLAUDE_CONFIG_DIR= claude login` terminal
- // pane; that needs argv+env pane support in the terminal builtin and is
- // wired separately. The action is published for that consumer.
+ case 'login':
+ final name = m.data['name'] as String?;
+ final dir = m.data['dir'] as String?;
+ final ctx = _ctx;
+ // Host `CLAUDE_CONFIG_DIR= claude login` in a modal terminal pane
+ // (T-485); the CLI owns the OAuth browser flow.
+ if (name != null && dir != null && ctx != null) {
+ ctx.dialog.show