Skip to content

feat: opt-in Claude Connect + stable dev signing#15

Merged
Balanced02 merged 1 commit into
mainfrom
feat/claude-connect
Jul 14, 2026
Merged

feat: opt-in Claude Connect + stable dev signing#15
Balanced02 merged 1 commit into
mainfrom
feat/claude-connect

Conversation

@Balanced02

@Balanced02 Balanced02 commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Makes reading the Claude token from the Keychain opt-in, so nothing prompts for credentials at launch.

  • Persisted claudeConnected (default off) gates allowKeychain; Connect banner in the Claude tab + a Connect/Disconnect control in Settings → Claude. Account + cost still load without connecting.
  • build-app.sh auto-signs local builds with an installed Developer ID (stable → no per-rebuild re-prompt); ad-hoc fallback via awk so cert-less contributors aren't aborted by set -euo pipefail.

Adversarially reviewed (4 dimensions). Fixed the confirmed build-script abort bug; documented the intentional opt-in migration (upgraders click Connect once). Tests: 11 UI + 15 Core green, incl. a new connect-gate test.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Claude live limits are now opt-in. Click Connect in the Claude tab or Settings to grant Keychain access.
    • Claude connection status is saved between launches and can be disconnected at any time.
    • Account and cost information remains available before connecting.
  • Bug Fixes

    • The app no longer prompts for Claude credentials at launch.
    • Builds automatically use an available Developer ID for signing, with ad-hoc signing as a fallback.
  • Documentation

    • Updated usage and installation guidance for Claude connection and Keychain access.

…ev signing

The app no longer reads the Claude Code token from the Keychain at launch, so
nothing prompts for credentials on startup. A persisted `claudeConnected` flag
(default off) gates `allowKeychain`; the Claude tab shows a Connect banner and
Settings → Claude has a Connect/Disconnect control. Clicking Connect reads the
token in a user-initiated context (macOS asks once, then remembers). Account +
cost data still load without connecting.

Also: Scripts/build-app.sh auto-signs local builds with an installed Developer
ID (stable signature → no per-rebuild Keychain re-prompt), falling back to
ad-hoc via awk (exits 0 on no match, so `set -euo pipefail` doesn't abort for
cert-less contributors). Explicit SIGN_IDENTITY still overrides; CI unaffected.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Claude opt-in release

Layer / File(s) Summary
Persisted Claude connection state
Sources/AIUsageBarUI/AppModel.swift, Tests/AIUsageBarUITests/ProviderSettingsTests.swift
claudeConnected is persisted and controls Keychain access, with connect/disconnect actions and lifecycle coverage.
Claude connection controls
Sources/AIUsageBarUI/MenuContentView.swift, Sources/AIUsageBarUI/SettingsView.swift
Claude displays connection prompts and settings controls for enabling or disabling live limits.
Release documentation and signing
Scripts/build-app.sh, CHANGELOG.md, README.md
Build signing discovers a Developer ID certificate with ad-hoc fallback, while release notes and usage instructions describe the updated Claude flow.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ClaudeConnectBanner
  participant AppModel
  participant UsageConfig
  participant Keychain
  User->>ClaudeConnectBanner: Click Connect
  ClaudeConnectBanner->>AppModel: connectClaude()
  AppModel->>UsageConfig: Set allowKeychain to true
  AppModel->>Keychain: Read Claude token during refresh
  Keychain-->>AppModel: Return token
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: opt-in Claude Connect and improved developer signing fallback.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/claude-connect

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Balanced02
Balanced02 merged commit 10f40a3 into main Jul 14, 2026
1 of 2 checks passed
@Balanced02
Balanced02 deleted the feat/claude-connect branch July 14, 2026 14:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/AIUsageBarUI/AppModel.swift`:
- Around line 396-415: Update connectClaude() and disconnectClaude() to use the
existing isApplying mechanism when changing claudeConnected, bypassing its
didSet-triggered update task. Within one Task, apply the configuration change
through the service update first, then await refresh(), ensuring both operations
are sequenced; preserve the existing guards and connection-state behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6a3581e-65fc-4ccd-8fba-329fa64995f1

📥 Commits

Reviewing files that changed from the base of the PR and between c48686e and baac0a0.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • README.md
  • Scripts/build-app.sh
  • Sources/AIUsageBarUI/AppModel.swift
  • Sources/AIUsageBarUI/MenuContentView.swift
  • Sources/AIUsageBarUI/SettingsView.swift
  • Tests/AIUsageBarUITests/ProviderSettingsTests.swift

Comment on lines +396 to +415
defaults.set(claudeConnected, forKey: "claudeConnected")
providerSettings.save(to: defaults)
}

/// Opt in to live Claude limits. Reads the Claude Code token from the Keychain
/// (macOS asks once, then remembers) and refreshes immediately so the prompt
/// appears in response to the user's click, not silently at launch.
public func connectClaude() {
guard !claudeConnected else { return }
claudeConnected = true // didSet → reconfigure with allowKeychain = true
Task { await refresh() }
}

/// Stop reading the Keychain / live endpoint; keep local account + cost data.
public func disconnectClaude() {
guard claudeConnected else { return }
claudeConnected = false
Task { await refresh() }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Race condition between configuration update and refresh.

Mutating claudeConnected synchronously triggers its didSet handler, which eventually spawns an unstructured Task { await service.update(config:) }. Immediately after, connectClaude()/disconnectClaude() spawn another Task { await refresh() }.

Because Swift Concurrency does not guarantee the execution order of these tasks, refresh() may execute before service.update() completes. If this happens, the refresh will use the stale configuration (e.g. failing to read the Keychain right after connecting), missing the immediate UI update until the next polling cycle.

Bypass the didSet handler using isApplying to properly sequence the service update and refresh within a single Task.

🔒️ Proposed fix to sequence the tasks safely
     /// Opt in to live Claude limits. Reads the Claude Code token from the Keychain
     /// (macOS asks once, then remembers) and refreshes immediately so the prompt
     /// appears in response to the user's click, not silently at launch.
     public func connectClaude() {
         guard !claudeConnected else { return }
-        claudeConnected = true            // didSet → reconfigure with allowKeychain = true
-        Task { await refresh() }
+        updateConnectionState(true)
     }
 
     /// Stop reading the Keychain / live endpoint; keep local account + cost data.
     public func disconnectClaude() {
         guard claudeConnected else { return }
-        claudeConnected = false
-        Task { await refresh() }
+        updateConnectionState(false)
+    }
+
+    private func updateConnectionState(_ connected: Bool) {
+        isApplying = true
+        claudeConnected = connected
+        isApplying = false
+        persist()
+
+        let config = Self.usageConfig(providerSettings: providerSettings,
+                                      codexEnabled: codexEnabled,
+                                      claudeEnabled: claudeEnabled,
+                                      geminiEnabled: geminiEnabled,
+                                      claudeConnected: connected,
+                                      discovered: discoverClaudeProfiles())
+        appliedClaudeProfiles = Self.effectiveClaudeProfiles(for: config)
+        if !claudeEnabled { lastClaude = [] }
+        
+        Task {
+            await service.update(config: config)
+            await refresh()
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defaults.set(claudeConnected, forKey: "claudeConnected")
providerSettings.save(to: defaults)
}
/// Opt in to live Claude limits. Reads the Claude Code token from the Keychain
/// (macOS asks once, then remembers) and refreshes immediately so the prompt
/// appears in response to the user's click, not silently at launch.
public func connectClaude() {
guard !claudeConnected else { return }
claudeConnected = true // didSet → reconfigure with allowKeychain = true
Task { await refresh() }
}
/// Stop reading the Keychain / live endpoint; keep local account + cost data.
public func disconnectClaude() {
guard claudeConnected else { return }
claudeConnected = false
Task { await refresh() }
}
defaults.set(claudeConnected, forKey: "claudeConnected")
providerSettings.save(to: defaults)
}
/// Opt in to live Claude limits. Reads the Claude Code token from the Keychain
/// (macOS asks once, then remembers) and refreshes immediately so the prompt
/// appears in response to the user's click, not silently at launch.
public func connectClaude() {
guard !claudeConnected else { return }
updateConnectionState(true)
}
/// Stop reading the Keychain / live endpoint; keep local account + cost data.
public func disconnectClaude() {
guard claudeConnected else { return }
updateConnectionState(false)
}
private func updateConnectionState(_ connected: Bool) {
isApplying = true
claudeConnected = connected
isApplying = false
persist()
let config = Self.usageConfig(providerSettings: providerSettings,
codexEnabled: codexEnabled,
claudeEnabled: claudeEnabled,
geminiEnabled: geminiEnabled,
claudeConnected: connected,
discovered: discoverClaudeProfiles())
appliedClaudeProfiles = Self.effectiveClaudeProfiles(for: config)
if !claudeEnabled { lastClaude = [] }
Task {
await service.update(config: config)
await refresh()
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AIUsageBarUI/AppModel.swift` around lines 396 - 415, Update
connectClaude() and disconnectClaude() to use the existing isApplying mechanism
when changing claudeConnected, bypassing its didSet-triggered update task.
Within one Task, apply the configuration change through the service update
first, then await refresh(), ensuring both operations are sequenced; preserve
the existing guards and connection-state behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant