Add Claude (OAuth Pro/Max) LLM plugin#1
Conversation
📝 WalkthroughWalkthroughAdds a new Claude (OAuth Pro/Max) TypeWhisper plugin: package/manifest/docs, PKCE + OAuth URL builder, OAuth client with exchange/refresh and retries, token persistence and refresh scheduler, Anthropic request builder/client, plugin implementation, and a SwiftUI settings UI. ChangesClaude OAuth LLM Plugin
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
plugins/claude-oauth/README.md (1)
15-18: 💤 Low valueInconsistent language in setup instructions.
The setup instructions contain German UI strings ("Mit Claude verbinden", "Bestätigen") within otherwise English documentation. This creates inconsistency and may confuse users.
📝 Suggested fix for consistency
-2. Click **Mit Claude verbinden**. Your default browser opens at `claude.ai/oauth/authorize`. +2. Click **Connect to Claude**. Your default browser opens at `claude.ai/oauth/authorize`. 3. Log in with your Claude Pro/Max account and confirm the permission grant. 4. Anthropic shows an authorization code of the form `<code>#<state>`. Copy the whole thing. -5. Paste it back into the plugin's code field and click **Bestätigen**. +5. Paste it back into the plugin's code field and click **Confirm**.🤖 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 `@plugins/claude-oauth/README.md` around lines 15 - 18, The README contains German UI labels ("Mit Claude verbinden", "Bestätigen") mixed into English instructions; update those UI strings to match the rest of the doc (e.g., replace "Mit Claude verbinden" with "Connect with Claude" and "Bestätigen" with "Confirm" or the exact English labels used in the plugin UI) so the steps 2 and 5 read consistently in English while preserving the authorization-code instructions and examples (the `<code>#<state>` token).plugins/claude-oauth/src/Package.swift (1)
11-14: ⚡ Quick winConsider tightening the SDK version constraint.
The current constraint
from: "1.0.0"will accept any version >= 1.0.0, including potentially breaking changes in 2.0.0 or later. This could cause unexpected plugin failures when the SDK is updated.🔒 Proposed fix to prevent breaking changes
dependencies: [ .package( url: "https://github.com/TypeWhisper/TypeWhisperPluginSDK.git", - from: "1.0.0" + .upToNextMajor(from: "1.0.0") ), ],🤖 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 `@plugins/claude-oauth/src/Package.swift` around lines 11 - 14, The dependency on TypeWhisperPluginSDK in Package.swift uses a loose from: "1.0.0" constraint; tighten it to avoid automatically accepting a 2.x breaking release by replacing the version specifier for the .package(...) entry referencing "https://github.com/TypeWhisper/TypeWhisperPluginSDK.git" with a more restrictive constraint (for example .upToNextMinor(from: "1.0.0") or an exact/ranged version like 1.0.0..<2.0.0) so only safe or explicitly chosen SDK releases are allowed.
🤖 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 `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicClient.swift`:
- Line 45: The logs in AnthropicClient.swift (e.g., the retry/info log in the
retry loop and the response/error logging in the request/response handling
methods) currently emit raw payloads and response previews using privacy:
.public; change these to avoid exposing bodies by either logging only safe
metadata (status codes, request IDs, attempt counts) with privacy: .private or
by redacting/truncating payload fields before logging, and remove any direct
printing of full response/error bodies in the logger calls referenced (the
logger.info at the retry line and the logger calls around lines 66–67, 77, 83,
91, 99). Ensure only non-public or redacted values are passed to logger and keep
the retry delay and attempt count visible as safe metadata.
In
`@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicRequestBuilder.swift`:
- Line 5: Remove the hardcoded first‑party Claude identity by deleting the
constant claudeCodeIdentity and any usage of it in AnthropicRequestBuilder.swift
(including the similar hardcoded strings at the other occurrence around lines
27–30); instead, use a neutral/default system prompt or accept an optional
configurable identity parameter passed into the request builder (e.g., via the
existing build/request creation method) so requests no longer claim "Claude
Code" by default. Ensure all references to claudeCodeIdentity are removed or
replaced with the neutral/configurable value and update callers to pass an
explicit identity when needed.
In
`@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swift`:
- Around line 39-45: activate(host: HostServices) currently only calls
startRefreshTimer() when currentTokens() != nil, which prevents starting the
background refresh for users who log in after activation; change activate(host:)
to call startRefreshTimer() unconditionally (relying on scheduledRefreshCheck()
to no-op when no tokens exist), and make the same unconditional
startRefreshTimer() change for the other analogous places that guard on
currentTokens() (the other blocks around startRefreshTimer() referenced in the
review, e.g., the similar conditional checks near the settings/login flow and
the refresh-related methods) so the timer always starts and
scheduledRefreshCheck() controls whether refresh work runs.
- Around line 77-80: The token refresh can race between process() and
scheduledRefreshCheck() causing double-rotation and spurious invalid_grant
failures; serialize refreshes by introducing a single in-flight gate around
RefreshScheduler.needsRefresh + refreshAndPersist(currentTokens:store:) (e.g. an
actor, an AsyncMutex, or a shared Task) so only one refresh runs at a time,
and/or on refresh failure with invalid_grant reload the stored tokens and retry
once before treating as terminal; apply the same gate/reload-before-fail logic
to the other similar sites referenced (lines around 144-159 and 176-180) to
ensure all refresh paths use the same serialization and retry behavior.
- Around line 95-108: The catch that handles refreshAndPersist currently
replaces any mapped PluginProcessError with PluginProcessError.authExpired;
change the catch to rethrow the caught error (preserving mapped refresh errors
from refreshAndPersist) instead of always throwing
PluginProcessError.authExpired, and then ensure the retry call to sendAndMap is
wrapped so that if it throws AnthropicError.authExpired you translate that into
PluginProcessError.authExpired before letting it escape. Target the
refreshAndPersist, sendAndMap, AnthropicError.authExpired, and
PluginProcessError.authExpired paths in process().
In `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeSettingsView.swift`:
- Around line 122-127: loadInitialState() and refreshDisplayedTokens() never set
ConnectionState.expired so persisted or refreshed expired tokens show as
.connected; update these functions to inspect loadedTokens (from
TokenStore.load()) and any refreshed tokens for expiration (e.g., check
token.expiration or TokenStore's expiry helper) and set state = .expired if any
token is expired, otherwise set .connected (or .idle when no tokens); ensure you
reference loadedTokens, TokenStore, loadInitialState(),
refreshDisplayedTokens(), and ConnectionState.expired when implementing the
check so the UI reflects expired tokens correctly.
- Around line 215-219: The disconnect() method currently swallows errors from
TokenStore(host: host).clear() by using try?; change it to explicitly catch and
handle failures: call TokenStore(host: host).clear() with try and a do/catch,
and on error present or surface the error (e.g., show an alert or propagate)
instead of proceeding to set loadedTokens = nil, state = .idle, and calling
host.notifyCapabilitiesChanged(); only clear loadedTokens, update state, and
notify host after a successful clear. Ensure you reference the disconnect()
function and TokenStore(host: host).clear() when making the change so failures
aren't silently ignored.
- Around line 163-178: The code extracts statePart in confirmCode() but never
verifies it against the stored state nonce (stateNonce), so add a validation
step before calling oauthClient.exchangeAuthorizationCode: compare statePart
(after trimming) to stateNonce and if they differ or stateNonce is nil, set
errorMessage to an appropriate CSRF/session error, set isWorking = false, and
return without calling exchangeAuthorizationCode; only proceed to call
oauthClient.exchangeAuthorizationCode(...) when the state matches. Ensure you
reference the existing symbols confirmCode(), statePart, stateNonce, and
oauthClient.exchangeAuthorizationCode to locate where to insert this check.
In `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swift`:
- Around line 1-3: PKCE.swift currently calls SecRandomCopyBytes and uses
kSecRandomDefault and errSecSuccess but lacks the Security import; fix this by
adding import Security at the top of PKCE.swift so the symbols
SecRandomCopyBytes, kSecRandomDefault, and errSecSuccess resolve during SwiftPM
compilation and the PKCE generation functions (those invoking
SecRandomCopyBytes) compile cleanly.
---
Nitpick comments:
In `@plugins/claude-oauth/README.md`:
- Around line 15-18: The README contains German UI labels ("Mit Claude
verbinden", "Bestätigen") mixed into English instructions; update those UI
strings to match the rest of the doc (e.g., replace "Mit Claude verbinden" with
"Connect with Claude" and "Bestätigen" with "Confirm" or the exact English
labels used in the plugin UI) so the steps 2 and 5 read consistently in English
while preserving the authorization-code instructions and examples (the
`<code>#<state>` token).
In `@plugins/claude-oauth/src/Package.swift`:
- Around line 11-14: The dependency on TypeWhisperPluginSDK in Package.swift
uses a loose from: "1.0.0" constraint; tighten it to avoid automatically
accepting a 2.x breaking release by replacing the version specifier for the
.package(...) entry referencing
"https://github.com/TypeWhisper/TypeWhisperPluginSDK.git" with a more
restrictive constraint (for example .upToNextMinor(from: "1.0.0") or an
exact/ranged version like 1.0.0..<2.0.0) so only safe or explicitly chosen SDK
releases are allowed.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 42e76541-0275-4cde-b1b8-9faa21c88aba
📒 Files selected for processing (15)
plugins/claude-oauth/LICENSEplugins/claude-oauth/README.mdplugins/claude-oauth/manifest.jsonplugins/claude-oauth/src/Package.resolvedplugins/claude-oauth/src/Package.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicClient.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicRequestBuilder.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeSettingsView.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/Models.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/OAuthClient.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/OAuthURLBuilder.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/RefreshScheduler.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/TokenStore.swift
| lastError = error | ||
| if attempt < retryDelays.count { | ||
| let delay = retryDelays[attempt] | ||
| logger.info("Transient error (\(String(describing: error), privacy: .public)), retry \(attempt + 1)/\(self.retryDelays.count) in \(delay)s") |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect public logging sites in the Claude OAuth plugin that may expose payloads.
rg -n -C2 'privacy:\s*\.public|body=|preview' plugins/claude-oauth/src/Sources/ClaudeOAuthPluginRepository: TypeWhisper/typewhisper-plugins
Length of output: 5061
Don’t log Anthropic response/error payloads with privacy: .public.
plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicClient.swift emits raw payloads/bodies with public logging, including transient/network errors and response previews/bodies (privacy: .public) at lines 45, 66–67, 77, 83, 91, and 99. Redact/truncate or omit these payload/body fields and log only safe metadata (e.g., status codes, request IDs) using non-public privacy.
🤖 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 `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicClient.swift` at
line 45, The logs in AnthropicClient.swift (e.g., the retry/info log in the
retry loop and the response/error logging in the request/response handling
methods) currently emit raw payloads and response previews using privacy:
.public; change these to avoid exposing bodies by either logging only safe
metadata (status codes, request IDs, attempt counts) with privacy: .private or
by redacting/truncating payload fields before logging, and remove any direct
printing of full response/error bodies in the logger calls referenced (the
logger.info at the retry line and the logger calls around lines 66–67, 77, 83,
91, 99). Ensure only non-public or redacted values are passed to logger and keep
the retry delay and attempt count visible as safe metadata.
|
|
||
| public enum AnthropicRequestBuilder { | ||
| public static let messagesEndpoint = "https://api.anthropic.com/v1/messages" | ||
| public static let claudeCodeIdentity = "You are Claude Code, Anthropic's official CLI for Claude." |
There was a problem hiding this comment.
Remove the hardcoded first-party Claude Code identity.
These lines make every request claim it is Anthropic's official CLI, while the PR context explicitly describes this integration as unofficial. Keeping that assertion in the shipped request body creates an avoidable compliance/account-risk beyond the README disclosure.
Also applies to: 27-30
🤖 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
`@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicRequestBuilder.swift`
at line 5, Remove the hardcoded first‑party Claude identity by deleting the
constant claudeCodeIdentity and any usage of it in AnthropicRequestBuilder.swift
(including the similar hardcoded strings at the other occurrence around lines
27–30); instead, use a neutral/default system prompt or accept an optional
configurable identity parameter passed into the request builder (e.g., via the
existing build/request creation method) so requests no longer claim "Claude
Code" by default. Ensure all references to claudeCodeIdentity are removed or
replaced with the neutral/configurable value and update callers to pass an
explicit identity when needed.
| public func activate(host: HostServices) { | ||
| self.host = host | ||
| // Start scheduler only if we already have tokens — otherwise no-op until user logs in. | ||
| if currentTokens() != nil { | ||
| startRefreshTimer() | ||
| } | ||
| } |
There was a problem hiding this comment.
Start the refresh timer after in-session login too.
Line 42 only starts the timer when tokens already exist during activate(). Since settingsView has no callback into the plugin, a user who connects after activation never gets the documented background refresh until restart. Starting the timer unconditionally here is the simplest fix because scheduledRefreshCheck already no-ops when no tokens exist.
Suggested fix
public func activate(host: HostServices) {
self.host = host
- // Start scheduler only if we already have tokens — otherwise no-op until user logs in.
- if currentTokens() != nil {
- startRefreshTimer()
- }
+ refreshTimer?.stop()
+ startRefreshTimer()
}Also applies to: 53-55, 167-180
🤖 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
`@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swift`
around lines 39 - 45, activate(host: HostServices) currently only calls
startRefreshTimer() when currentTokens() != nil, which prevents starting the
background refresh for users who log in after activation; change activate(host:)
to call startRefreshTimer() unconditionally (relying on scheduledRefreshCheck()
to no-op when no tokens exist), and make the same unconditional
startRefreshTimer() change for the other analogous places that guard on
currentTokens() (the other blocks around startRefreshTimer() referenced in the
review, e.g., the similar conditional checks near the settings/login flow and
the refresh-related methods) so the timer always starts and
scheduledRefreshCheck() controls whether refresh work runs.
| // Refresh immediately if we're within the immediate threshold (5 min). | ||
| if RefreshScheduler.needsRefresh(tokens: tokens, threshold: .immediate) { | ||
| tokens = try await refreshAndPersist(currentTokens: tokens, store: store) | ||
| } |
There was a problem hiding this comment.
Serialize refresh-token rotation across foreground and background refreshes.
This plugin can refresh from process() and scheduledRefreshCheck() at the same time, and the PR context says refresh tokens are single-use rotated. Two callers racing on the same stored refresh token will make one succeed and the other hit invalid_grant, which this code treats as terminal even though a newer token set may already have been saved. Refresh needs a single in-flight gate here (actor/lock/shared task), or at minimum a reload-before-fail check on invalid_grant.
Also applies to: 144-159, 176-180
🤖 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
`@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swift`
around lines 77 - 80, The token refresh can race between process() and
scheduledRefreshCheck() causing double-rotation and spurious invalid_grant
failures; serialize refreshes by introducing a single in-flight gate around
RefreshScheduler.needsRefresh + refreshAndPersist(currentTokens:store:) (e.g. an
actor, an AsyncMutex, or a shared Task) so only one refresh runs at a time,
and/or on refresh failure with invalid_grant reload the stored tokens and retry
once before treating as terminal; apply the same gate/reload-before-fail logic
to the other similar sites referenced (lines around 144-159 and 176-180) to
ensure all refresh paths use the same serialization and retry behavior.
| } catch AnthropicError.authExpired { | ||
| // Token was rejected mid-flight (e.g. revoked) — refresh once and retry. | ||
| do { | ||
| tokens = try await refreshAndPersist(currentTokens: tokens, store: store) | ||
| } catch { | ||
| throw PluginProcessError.authExpired | ||
| } | ||
| // Any error from the retry is also mapped to PluginProcessError via sendAndMap. | ||
| return try await sendAndMap( | ||
| accessToken: tokens.accessToken, | ||
| model: chosenModel, | ||
| systemPrompt: systemPrompt, | ||
| userText: userText | ||
| ) |
There was a problem hiding this comment.
Keep the auth-refresh retry path on PluginProcessError.
refreshAndPersist() already maps refresh failures, but Lines 97-101 squash every one of them into .authExpired. Then the retried send can still throw raw AnthropicError.authExpired, which leaks the lower-level error type out of process(). Preserve the mapped refresh error, and explicitly map a second auth failure back to PluginProcessError.authExpired.
Suggested fix
} catch AnthropicError.authExpired {
// Token was rejected mid-flight (e.g. revoked) — refresh once and retry.
- do {
- tokens = try await refreshAndPersist(currentTokens: tokens, store: store)
- } catch {
- throw PluginProcessError.authExpired
- }
- // Any error from the retry is also mapped to PluginProcessError via sendAndMap.
- return try await sendAndMap(
- accessToken: tokens.accessToken,
- model: chosenModel,
- systemPrompt: systemPrompt,
- userText: userText
- )
+ tokens = try await refreshAndPersist(currentTokens: tokens, store: store)
+ do {
+ return try await sendAndMap(
+ accessToken: tokens.accessToken,
+ model: chosenModel,
+ systemPrompt: systemPrompt,
+ userText: userText
+ )
+ } catch AnthropicError.authExpired {
+ throw PluginProcessError.authExpired
+ }
}🤖 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
`@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swift`
around lines 95 - 108, The catch that handles refreshAndPersist currently
replaces any mapped PluginProcessError with PluginProcessError.authExpired;
change the catch to rethrow the caught error (preserving mapped refresh errors
from refreshAndPersist) instead of always throwing
PluginProcessError.authExpired, and then ensure the retry call to sendAndMap is
wrapped so that if it throws AnthropicError.authExpired you translate that into
PluginProcessError.authExpired before letting it escape. Target the
refreshAndPersist, sendAndMap, AnthropicError.authExpired, and
PluginProcessError.authExpired paths in process().
| private func loadInitialState() { | ||
| loadedTokens = TokenStore(host: host).load() | ||
| selectedModel = (host.userDefault(forKey: ClaudeOAuthLLMPlugin.selectedModelKey) as? String) | ||
| ?? ClaudeOAuthLLMPlugin.defaultModel | ||
| state = (loadedTokens == nil) ? .idle : .connected | ||
| } |
There was a problem hiding this comment.
expired state is never entered from persisted/refresh paths.
ConnectionState.expired has UI but isn’t set in loadInitialState() or refreshDisplayedTokens(). Expired tokens will still appear as connected.
Suggested fix
+ private func resolveState(from tokens: TokenSet?) -> ConnectionState {
+ guard let tokens else { return .idle }
+ return tokens.expiresAt <= Date() ? .expired : .connected
+ }
+
private func loadInitialState() {
loadedTokens = TokenStore(host: host).load()
selectedModel = (host.userDefault(forKey: ClaudeOAuthLLMPlugin.selectedModelKey) as? String)
?? ClaudeOAuthLLMPlugin.defaultModel
- state = (loadedTokens == nil) ? .idle : .connected
+ state = resolveState(from: loadedTokens)
}
@@
private func refreshDisplayedTokens() {
guard state == .connected || state == .expired else { return }
let fresh = TokenStore(host: host).load()
- if let fresh {
- loadedTokens = fresh
- if state == .expired { state = .connected }
- } else {
- loadedTokens = nil
- state = .idle
- }
+ loadedTokens = fresh
+ state = resolveState(from: fresh)
}Also applies to: 132-141
🤖 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 `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeSettingsView.swift`
around lines 122 - 127, loadInitialState() and refreshDisplayedTokens() never
set ConnectionState.expired so persisted or refreshed expired tokens show as
.connected; update these functions to inspect loadedTokens (from
TokenStore.load()) and any refreshed tokens for expiration (e.g., check
token.expiration or TokenStore's expiry helper) and set state = .expired if any
token is expired, otherwise set .connected (or .idle when no tokens); ensure you
reference loadedTokens, TokenStore, loadInitialState(),
refreshDisplayedTokens(), and ConnectionState.expired when implementing the
check so the UI reflects expired tokens correctly.
| private func disconnect() { | ||
| try? TokenStore(host: host).clear() | ||
| loadedTokens = nil | ||
| state = .idle | ||
| host.notifyCapabilitiesChanged() |
There was a problem hiding this comment.
Don’t swallow token-clear failures during disconnect.
Line 216 ignores keychain-clear errors. If clear fails, the UI still reports disconnected while credentials may remain stored.
Suggested fix
private func disconnect() {
- try? TokenStore(host: host).clear()
+ do {
+ try TokenStore(host: host).clear()
+ } catch {
+ errorMessage = "Trennen fehlgeschlagen: \(error.localizedDescription)"
+ return
+ }
loadedTokens = nil
state = .idle
host.notifyCapabilitiesChanged()
}🤖 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 `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeSettingsView.swift`
around lines 215 - 219, The disconnect() method currently swallows errors from
TokenStore(host: host).clear() by using try?; change it to explicitly catch and
handle failures: call TokenStore(host: host).clear() with try and a do/catch,
and on error present or surface the error (e.g., show an alert or propagate)
instead of proceeding to set loadedTokens = nil, state = .idle, and calling
host.notifyCapabilitiesChanged(); only clear loadedTokens, update state, and
notify host after a successful clear. Ensure you reference the disconnect()
function and TokenStore(host: host).clear() when making the change so failures
aren't silently ignored.
| import Foundation | ||
| import CryptoKit | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify Security symbols are used and whether Security is explicitly imported.
rg -n -C2 'import Security|SecRandomCopyBytes|kSecRandomDefault|errSecSuccess' plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swiftRepository: TypeWhisper/typewhisper-plugins
Length of output: 457
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swift"
echo "== Top of file =="
nl -ba "$FILE" | head -n 40
echo
echo "== Any 'import Security' in file? =="
rg -n "import Security" "$FILE" || trueRepository: TypeWhisper/typewhisper-plugins
Length of output: 154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swift"
echo "== File head (first 40 lines) =="
sed -n '1,40p' "$FILE"
echo
echo "== Any 'import Security' in file? =="
rg -n "import Security" "$FILE" || true
echo
echo "== Security symbols usage in file =="
rg -n "SecRandomCopyBytes|kSecRandomDefault|errSecSuccess" "$FILE"Repository: TypeWhisper/typewhisper-plugins
Length of output: 1255
🌐 Web query:
SecRandomCopyBytes kSecRandomDefault errSecSuccess Swift import Security module
💡 Result:
In Swift, use the Security framework function SecRandomCopyBytes (from module Security) with kSecRandomDefault and check the returned status for errSecSuccess. Swift API (signature): func SecRandomCopyBytes(_ rnd: SecRandomRef?, _ count: Int, _ bytes: UnsafeMutableRawPointer) -> Int32 [1] Example (generate N secure random bytes): import Foundation import Security let count = 32 var bytes = [UInt8](repeating: 0, count: count) let status = bytes.withUnsafeMutableBytes { rawBuf -> Int32 in SecRandomCopyBytes(kSecRandomDefault, count, rawBuf.baseAddress!) } if status == errSecSuccess { // bytes now contain cryptographically secure random data } else { // handle error } Notes: - Pass kSecRandomDefault as the random number generator to use the system’s default secure RNG. [1] - The function fills the buffer you provide with cryptographically secure random bytes. [1] - Always test the returned Int32 status; on success it is errSecSuccess, otherwise it is some other error code. [1] Import: ensure you have - import Security [1] If you still see errSecSuccess-related issues, they are almost always due to buffer/size/pointer problems (e.g., passing an incorrectly sized buffer or an invalid pointer), and should be diagnosed by checking the returned status value and ensuring the buffer is at least count bytes. [1]
Citations:
Add import Security to support SecRandomCopyBytes in PKCE.swift
PKCE.swift calls SecRandomCopyBytes with kSecRandomDefault and compares against errSecSuccess, but only imports Foundation and CryptoKit; import Security to avoid SwiftPM compile failures.
💡 Proposed fix
import Foundation
import CryptoKit
+import Security🤖 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 `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swift` around lines 1
- 3, PKCE.swift currently calls SecRandomCopyBytes and uses kSecRandomDefault
and errSecSuccess but lacks the Security import; fix this by adding import
Security at the top of PKCE.swift so the symbols SecRandomCopyBytes,
kSecRandomDefault, and errSecSuccess resolve during SwiftPM compilation and the
PKCE generation functions (those invoking SecRandomCopyBytes) compile cleanly.
424429b to
4d0682e
Compare
|
Pushed a force-update addressing internal review findings before this PR gets attention: Code fixes
README
Housekeeping
All 53 tests in the upstream dev tree still pass. Ready for review when you get to it. |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swift (1)
1-11:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winAdd
import Securityfor PKCE RNG symbols.Line 10 and Line 11 use Security APIs, but the module import is missing in this file. That can break SwiftPM compilation with unresolved identifiers.
Proposed fix
import Foundation import CryptoKit +import Security#!/bin/bash set -euo pipefail FILE="plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swift" echo "== imports ==" rg -n '^\s*import\s+' "$FILE" echo echo "== Security symbol usage ==" rg -n 'SecRandomCopyBytes|kSecRandomDefault|errSecSuccess' "$FILE"🤖 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 `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swift` around lines 1 - 11, The file is missing the Security module import used by the PKCE RNG calls; add "import Security" at the top of the file so symbols used in PKCEPair.generate (SecRandomCopyBytes, kSecRandomDefault, errSecSuccess) resolve. Update the imports section of the PKCE.swift file to include Security alongside Foundation and CryptoKit so the PKCEPair.generate function compiles cleanly.
🤖 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 `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeSettingsView.swift`:
- Around line 174-177: The CSRF state check currently only runs when statePart
is non-nil, allowing a missing statePart to bypass validation; update the
handling in the OAuth callback (where statePart, stateNonce, and errorMessage
are used) to treat a nil statePart as a validation failure: if statePart is nil
or statePart != stateNonce set errorMessage to the same CSRF error text and
return. Ensure you reference statePart and stateNonce in a single conditional
that covers both nil and mismatch cases so missing state values no longer bypass
the check.
In `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/TokenStore.swift`:
- Around line 4-31: TokenStore is marked `@unchecked` Sendable but holds shared
JSONEncoder/JSONDecoder instances (encoder/decoder) that are used in save(_:)
and load(), which can race; change to create a fresh JSONEncoder in save(_:) and
a fresh JSONDecoder in load() (setting dateEncodingStrategy/dateDecodingStrategy
on each instance) and remove the stored private let encoder/decoder properties
so encoding/decoding are per-call and thread-safe; keep existing behavior
(including throwing TokenStoreError.encodingFailed and using Self.secretKey and
host.storeSecret/loadSecret) otherwise.
---
Duplicate comments:
In `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swift`:
- Around line 1-11: The file is missing the Security module import used by the
PKCE RNG calls; add "import Security" at the top of the file so symbols used in
PKCEPair.generate (SecRandomCopyBytes, kSecRandomDefault, errSecSuccess)
resolve. Update the imports section of the PKCE.swift file to include Security
alongside Foundation and CryptoKit so the PKCEPair.generate function compiles
cleanly.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 42f2fc40-ef0a-4e62-b48b-a279d32fac22
📒 Files selected for processing (15)
plugins/claude-oauth/LICENSEplugins/claude-oauth/README.mdplugins/claude-oauth/manifest.jsonplugins/claude-oauth/src/Package.resolvedplugins/claude-oauth/src/Package.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicClient.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicRequestBuilder.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeSettingsView.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/Models.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/OAuthClient.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/OAuthURLBuilder.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/RefreshScheduler.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/TokenStore.swift
✅ Files skipped from review due to trivial changes (4)
- plugins/claude-oauth/src/Package.swift
- plugins/claude-oauth/LICENSE
- plugins/claude-oauth/manifest.json
- plugins/claude-oauth/src/Package.resolved
| public final class TokenStore: @unchecked Sendable { | ||
| public static let secretKey = "com.guttmann.typewhisper-claude.oauth" | ||
|
|
||
| private let host: HostServices | ||
| private let encoder: JSONEncoder | ||
| private let decoder: JSONDecoder | ||
|
|
||
| public init(host: HostServices) { | ||
| self.host = host | ||
| self.encoder = JSONEncoder() | ||
| self.encoder.dateEncodingStrategy = .millisecondsSince1970 | ||
| self.decoder = JSONDecoder() | ||
| self.decoder.dateDecodingStrategy = .millisecondsSince1970 | ||
| } | ||
|
|
||
| public func save(_ tokens: TokenSet) throws { | ||
| let data = try encoder.encode(tokens) | ||
| guard let json = String(data: data, encoding: .utf8) else { | ||
| throw TokenStoreError.encodingFailed | ||
| } | ||
| try host.storeSecret(key: Self.secretKey, value: json) | ||
| } | ||
|
|
||
| public func load() -> TokenSet? { | ||
| guard let json = host.loadSecret(key: Self.secretKey), | ||
| let data = json.data(using: .utf8) else { return nil } | ||
| return try? decoder.decode(TokenSet.self, from: data) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/TokenStore.swift"
echo "== sendable declaration and shared coder fields =="
rg -n -C2 '`@unchecked` Sendable|JSONEncoder|JSONDecoder' "$FILE"
echo
echo "== method bodies using shared encoder/decoder =="
rg -n -C3 'func save|func load|encoder\.|decoder\.' "$FILE"Repository: TypeWhisper/typewhisper-plugins
Length of output: 1672
Avoid sharing JSONEncoder/JSONDecoder across concurrent use in @unchecked Sendable TokenStore.
TokenStore is @unchecked Sendable but it keeps shared private let encoder: JSONEncoder and private let decoder: JSONDecoder initialized in init(host:), then uses them in save(_:) (encoder.encode) and load() (decoder.decode), which can race if TokenStore is accessed concurrently. Prefer creating encoder/decoder per call (e.g., like the proposed diff).
🤖 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 `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/TokenStore.swift` around
lines 4 - 31, TokenStore is marked `@unchecked` Sendable but holds shared
JSONEncoder/JSONDecoder instances (encoder/decoder) that are used in save(_:)
and load(), which can race; change to create a fresh JSONEncoder in save(_:) and
a fresh JSONDecoder in load() (setting dateEncodingStrategy/dateDecodingStrategy
on each instance) and remove the stored private let encoder/decoder properties
so encoding/decoding are per-call and thread-safe; keep existing behavior
(including throwing TokenStoreError.encodingFailed and using Self.secretKey and
host.storeSecret/loadSecret) otherwise.
A third-party LLM provider plugin that uses an Anthropic Claude Pro/Max subscription instead of an API key. PKCE OAuth against claude.ai with single-use refresh-token rotation, atomic Keychain persistence via HostServices.storeSecret, and a four-state SwiftUI settings view. The README is upfront about this being an unofficial use of the subscription — flagging it at submission so the catalog maintainer can decide whether it fits the catalog's policy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4d0682e to
3d74bdd
Compare
|
One more iteration based on a second internal review pass: Tightened state-nonce guard
Known follow-up (not blocking this PR) Branch amended + force-pushed: |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swift (1)
99-113:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
AnthropicError.authExpiredleaks from the retry path.If the retried
sendAndMapcall (line 107) throwsAnthropicError.authExpired, it escapesprocess()unmapped since the outer catch only handles the firstauthExpired. This leaks an internal error type. Wrap the retry in a catch that maps a secondauthExpiredtoPluginProcessError.authExpired.🐛 Proposed fix
} catch AnthropicError.authExpired { // Token was rejected mid-flight (e.g. revoked) — refresh once and retry. do { tokens = try await refreshAndPersist(currentTokens: tokens, store: store) } catch { throw PluginProcessError.authExpired } - // Any error from the retry is also mapped to PluginProcessError via sendAndMap. - return try await sendAndMap( - accessToken: tokens.accessToken, - model: chosenModel, - systemPrompt: systemPrompt, - userText: userText - ) + // Retry once; map a second authExpired to PluginProcessError. + do { + return try await sendAndMap( + accessToken: tokens.accessToken, + model: chosenModel, + systemPrompt: systemPrompt, + userText: userText + ) + } catch AnthropicError.authExpired { + throw PluginProcessError.authExpired + } }🤖 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 `@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swift` around lines 99 - 113, The retry path after catching AnthropicError.authExpired currently calls sendAndMap directly and can let a second AnthropicError.authExpired escape process(); wrap the retry call (the second sendAndMap invocation) in its own do/catch so that if it throws AnthropicError.authExpired you rethrow PluginProcessError.authExpired (just as you do for the first refresh failure), keeping refreshAndPersist, sendAndMap and the mapping to PluginProcessError.authExpired in place so no AnthropicError leaks out of process().
🤖 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
`@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicRequestBuilder.swift`:
- Around line 42-46: The doc comment on modelAcceptsTemperature should avoid
quoting the undocumented verbatim 400 message; update the comment to state
generally that newer Opus 4.7+ models reject the temperature parameter rather
than including the specific quoted text, and keep the current prefix check
(!model.hasPrefix("claude-opus-4-7")) as the correct gating logic for Opus 4.7
model IDs in AnthropicRequestBuilder.swift.
---
Duplicate comments:
In
`@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swift`:
- Around line 99-113: The retry path after catching AnthropicError.authExpired
currently calls sendAndMap directly and can let a second
AnthropicError.authExpired escape process(); wrap the retry call (the second
sendAndMap invocation) in its own do/catch so that if it throws
AnthropicError.authExpired you rethrow PluginProcessError.authExpired (just as
you do for the first refresh failure), keeping refreshAndPersist, sendAndMap and
the mapping to PluginProcessError.authExpired in place so no AnthropicError
leaks out of process().
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 94344d8e-8dd3-4076-a26d-41631699590c
📒 Files selected for processing (15)
plugins/claude-oauth/LICENSEplugins/claude-oauth/README.mdplugins/claude-oauth/manifest.jsonplugins/claude-oauth/src/Package.resolvedplugins/claude-oauth/src/Package.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicClient.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicRequestBuilder.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeOAuthLLMPlugin.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/ClaudeSettingsView.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/Models.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/OAuthClient.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/OAuthURLBuilder.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/PKCE.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/RefreshScheduler.swiftplugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/TokenStore.swift
✅ Files skipped from review due to trivial changes (3)
- plugins/claude-oauth/manifest.json
- plugins/claude-oauth/src/Package.resolved
- plugins/claude-oauth/LICENSE
| /// Whether the given model accepts the `temperature` parameter. Newer reasoning models | ||
| /// (Opus 4.7+) reject it with a 400 "deprecated for this model" error. | ||
| public static func modelAcceptsTemperature(_ model: String) -> Bool { | ||
| !model.hasPrefix("claude-opus-4-7") | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Anthropic Claude Opus 4 model name temperature parameter
💡 Result:
Anthropic Claude Opus 4 (Claude API) temperature parameter: - Field name: temperature (part of the Messages API request payload) [1]. - Default: 1.0 if you omit temperature [2]. - Allowed range: 0.0 to 1.0 (values outside this range should be rejected) [2]. - Behavior: lower values make outputs more deterministic; higher values increase randomness/variety [2]. - Note: even at temperature = 0.0, results are not fully deterministic [2]. Opus 4 model name / API ID (use in the model field): - As of the latest Opus 4 line shown in Anthropic’s model overview, the Claude API IDs/aliases include claude-opus-4-7 for Opus 4.7 [3]. (There are also earlier pinned versions such as claude-opus-4-6 shown in the same overview.) [3] Example (Messages API): { "model": "claude-opus-4-7", "max_tokens": 1024, "temperature": 0.2, "messages": [{"role": "user", "content": "Hello, Claude"}] } Key reference: the Anthropic SDK/API type definition for temperature states default, range, and non-determinism at 0.0 [2].
Citations:
- 1: https://platform.claude.com/docs/en/build-with-claude/working-with-messages
- 2: https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/types/beta/message_create_params.py
- 3: https://platform.claude.com/docs/en/about-claude/models/overview?f80ce999_sort_date=desc
Align Opus 4.7 temperature gating with Anthropic model IDs
hasPrefix("claude-opus-4-7")matches the documented Claude API model ID/alias (claude-opus-4-7) for Opus 4.7, so the prefix check is appropriate.- The doc comment’s quoted 400 message text (
"deprecated for this model") isn’t substantiated by the checked references; rewrite it to a non-verbatim/general description.
🤖 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
`@plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/AnthropicRequestBuilder.swift`
around lines 42 - 46, The doc comment on modelAcceptsTemperature should avoid
quoting the undocumented verbatim 400 message; update the comment to state
generally that newer Opus 4.7+ models reject the temperature parameter rather
than including the specific quoted text, and keep the current prefix check
(!model.hasPrefix("claude-opus-4-7")) as the correct gating logic for Opus 4.7
model IDs in AnthropicRequestBuilder.swift.
|
Thanks for being explicit about the policy risk in the PR body. I can also see why you opened this PR here: our public plugin docs currently point contributors to this repository for community plugin submissions. So this is not a case of ignoring the documented submission path. That said, we can't accept this into the TypeWhisper plugin catalog. The implementation relies on Claude Code OAuth credentials and identifies requests as Claude Code so that a Pro/Max subscription accepts them. That crosses the policy/trust boundary we need for cataloged plugins: TypeWhisper should not ship or endorse a plugin that impersonates an Anthropic first-party client or uses subscription credentials as API access. This repository is for plugins that can be reviewed, built, and published through TypeWhisper's community catalog flow. A plugin based on unofficial Claude Code OAuth behavior is not a good fit for that catalog. If you want to keep experimenting with it, it should live in your own repository instead of being submitted for TypeWhisper publication. The acceptable paths for a TypeWhisper-catalog plugin would be:
Closing this PR for policy and catalog-fit reasons, not as a judgment on the implementation quality. |
|
Thanks for the clear and respectful close — that's exactly the right call given the constraints, and I appreciate you spelling out the two acceptable paths. For anyone landing on this thread later (or for the maintainers when they revisit catalog policy): Anthropic announced a relevant change that ships June 15, 2026. Subscription plans get a separate "Agent SDK Credit pool" on top of the existing interactive limit:
Anthropic's own docs: Activation emails with implementation details are scheduled for June 8, with the new flow live on June 15. What's not yet documented publicly is the exact authentication mechanism a third-party Swift app would use to opt into the Agent SDK credit pool — whether the official Python/TypeScript SDK libraries are mandatory, or whether direct OAuth against the new subscription-Agent-SDK auth endpoint is enough. I'll re-evaluate this plugin against the official Agent SDK flow once those details land. If the spec allows a Swift app to authenticate cleanly against the Agent SDK credit pool without impersonating Claude Code, that would address the exact policy concern you raised and might be worth a fresh PR. In the meantime: PR stays closed, plugin stays in my own repo for personal use, no further action expected here. Just leaving the pointer in case it's useful when you next look at catalog-policy questions for subscription-based plugins. |
Summary
Adds a new LLM provider plugin:
Claude (OAuth Pro/Max). It lets TypeWhisper use anAnthropic Claude Pro/Max subscription for text post-processing, instead of an Anthropic
API key.
Layout matches
plugins/example-llm/:plugins/claude-oauth/manifest.jsonplugins/claude-oauth/README.mdplugins/claude-oauth/LICENSE(GPL-3.0)plugins/claude-oauth/src/Package.swift(SDK pinnedfrom: "1.0.0")plugins/claude-oauth/src/Sources/ClaudeOAuthPlugin/*.swift(10 files, source-only)What it does
PKCE OAuth flow against
claude.ai, code exchanged for access + refresh tokens atplatform.claude.com, single-use refresh-token rotation with linear-backoff retries(
[2s, 4s, 6s]), atomic keychain persistence viaHostServices.storeSecret. A 60-secondbackground timer refreshes proactively when less than 15 minutes of validity remain.
Surface area is the standard
LLMProviderPluginprotocol; no extra SDK extensions.Heads-up on policy
This plugin uses the same OAuth client identifier that Claude Code itself uses and
identifies API requests as Claude Code so the Pro/Max subscription accepts them. That is
an unofficial use of the subscription per Anthropic's published policy. The README
documents this clearly. I want to flag it at submission rather than at review — if it
doesn't fit the catalog's policy, please close this PR and I'll keep the plugin in my own
repo instead.
Test Plan
cd plugins/claude-oauth/src swift buildI've also been running the plugin locally against TypeWhisper 1.4.0 (build 803):
<code>#<state>→ keychain stored)200 OKagainstapi.anthropic.com/v1/messagesexpiry timestamp update live in settings)
Summary by CodeRabbit
New Features
Documentation
License