Skip to content

fix: add macOS LaunchAgent setup for Claude Max subscription#10

Open
Roasbeef wants to merge 1 commit intomainfrom
easier-setup-mac
Open

fix: add macOS LaunchAgent setup for Claude Max subscription#10
Roasbeef wants to merge 1 commit intomainfrom
easier-setup-mac

Conversation

@Roasbeef
Copy link
Owner

@Roasbeef Roasbeef commented Jan 8, 2026

Summary

Fixes #6 - Claude Max subscription not working when Obsidian is launched from Dock/Spotlight.

macOS GUI apps don't inherit shell environment variables. When users run claude setup-token, it adds CLAUDE_CODE_OAUTH_TOKEN to their shell profile (~/.zshrc or ~/.bashrc), but Obsidian launched from the Dock doesn't see it.

Solution

Add a one-click "Setup Automatic Login" button that creates a macOS LaunchAgent. This agent runs at every login and uses launchctl setenv to make the token available to GUI applications.

Changes

  • New src/utils/launchAgentHelper.ts - LaunchAgent management functions (setup, remove, detect)
  • Updated src/settings/SettingsTab.ts - Claude Max setup UI with automatic setup button and manual command fallback
  • Added CSS styling for the setup UI components
  • Unit tests for LaunchAgent helper functions

How it works

  1. User runs claude setup-token (adds token to shell profile)
  2. User clicks "Setup Automatic Login" in plugin settings
  3. Plugin creates ~/Library/LaunchAgents/com.anthropic.claude-oauth.plist
  4. On every login, macOS runs: source ~/.zshrc; launchctl setenv CLAUDE_CODE_OAUTH_TOKEN ...
  5. GUI apps now have access to the token

Test Plan

  • All 566 tests pass
  • Manual test: Fresh setup on macOS
  • Manual test: Verify token available after restart

Fixes #6 - Claude Max subscription not working when Obsidian is launched
from Dock/Spotlight because macOS GUI apps don't inherit shell environment
variables.

Changes:
- Add LaunchAgent helper utility to create a login agent that automatically
  sets CLAUDE_CODE_OAUTH_TOKEN for GUI apps after each restart
- Add "Setup Automatic Login" button in settings when token not detected
- Show copyable manual command as alternative
- Add CSS styling for setup UI components
- Add unit tests for LaunchAgent helper functions

The LaunchAgent sources both ~/.zshrc and ~/.bashrc to find the token,
then uses launchctl setenv to make it available to GUI applications.
@Roasbeef
Copy link
Owner Author

@claude review this

@claude
Copy link

claude bot commented Jan 13, 2026

Claude finished @Roasbeef's task in 1m 57s —— View job


Code Review for PR #10: macOS LaunchAgent Setup

I've completed a thorough review of the changes. Overall, this is a solid solution to the macOS GUI environment variable problem. The implementation is clean and well-tested. Here are my findings:


✅ Strengths

  1. Correct approach: Using LaunchAgent + launchctl setenv is the proper macOS solution for making environment variables available to GUI apps
  2. Good error handling: Functions return structured results with success/failure and user-friendly messages
  3. Safe defaults: Checks for existing files, gracefully handles command failures
  4. Well-documented: Clear code comments explaining what each function does
  5. Good UX: Progressive disclosure in the settings UI, with both automatic and manual setup options

🔍 Security & Correctness Issues

1. Shell Injection Vulnerability in LaunchAgent plist (High Priority)

File: src/utils/launchAgentHelper.ts:37

The plist command uses shell metacharacters without proper escaping:

source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null; launchctl setenv CLAUDE_CODE_OAUTH_TOKEN "$CLAUDE_CODE_OAUTH_TOKEN"

Problem: If a user's ~/.zshrc or ~/.bashrc defines CLAUDE_CODE_OAUTH_TOKEN with malicious content (e.g., "; rm -rf ~; echo "), the LaunchAgent will execute arbitrary commands at every login.

Why this matters: The token is sourced from the shell profile and then interpolated into a shell command. An attacker who can write to ~/.zshrc could achieve code execution.

Mitigation: While this requires the attacker to already have write access to shell profiles (which is already game over), defense-in-depth suggests we should still prevent command injection. However, fixing this is non-trivial because we need the shell to expand $CLAUDE_CODE_OAUTH_TOKEN from the sourced environment.

Recommendation: Document this limitation in the code. Consider using printenv or a more robust approach:

source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null; printenv CLAUDE_CODE_OAUTH_TOKEN | xargs -0 launchctl setenv CLAUDE_CODE_OAUTH_TOKEN

Fix shell injection risk →

2. Manual Command Has Same Issue (Medium Priority)

File: src/utils/launchAgentHelper.ts:148

return 'launchctl setenv CLAUDE_CODE_OAUTH_TOKEN "$(echo $CLAUDE_CODE_OAUTH_TOKEN)"';

Problem: If the user's $CLAUDE_CODE_OAUTH_TOKEN contains special characters (especially double quotes or $), this command could break or behave unexpectedly. The $(echo ...) is unnecessary - it doesn't provide any protection.

Better approach:

return 'launchctl setenv CLAUDE_CODE_OAUTH_TOKEN "$CLAUDE_CODE_OAUTH_TOKEN"';

The echo doesn't add value here. Even better would be to use printf %q for proper shell escaping, but that's bash-specific.

3. LaunchAgent Plist Should Support Both zsh and bash (Low Priority)

File: src/utils/launchAgentHelper.ts:37

Currently uses: source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null

Issue: If ~/.zshrc exists but doesn't define the token, and ~/.bashrc does define it, the || operator won't execute the second source because the first succeeded (exit code 0).

Better approach: Try both unconditionally

{ source ~/.zshrc 2>/dev/null; source ~/.bashrc 2>/dev/null; } && launchctl setenv CLAUDE_CODE_OAUTH_TOKEN "$CLAUDE_CODE_OAUTH_TOKEN"

Or be explicit about which shell profile to use based on detection.


🐛 Code Quality Issues

4. Error Handling Silently Ignores Important Failures (Medium Priority)

File: src/utils/launchAgentHelper.ts:78-89

try {
  execSync(`launchctl load "${plistPath}"`, { stdio: "pipe" });
} catch {
  // May fail if already loaded, that's ok.
}

Problem: This catches ALL errors, not just "already loaded". If the plist has syntax errors or permission issues, this silently fails, and the user won't know setup didn't work.

Recommendation: Be more specific about which errors are acceptable:

try {
  execSync(`launchctl load "${plistPath}"`, { stdio: "pipe" });
} catch (error) {
  // Only ignore if already loaded, otherwise warn the user
  const errorMsg = error instanceof Error ? error.message : String(error);
  if (!errorMsg.includes('already loaded') && !errorMsg.includes('service already loaded')) {
    console.warn('Failed to load LaunchAgent:', errorMsg);
    // Still continue - it will work on next login
  }
}

Same issue exists in removeClaudeOAuthLaunchAgent at line 124.

5. File Permissions Could Be More Restrictive (Low Priority)

File: src/utils/launchAgentHelper.ts:75

fs.writeFileSync(plistPath, plistContent, { mode: 0o644 });

Observation: Mode 0o644 (readable by all users) is standard for LaunchAgent plists. This is fine, but worth noting that the plist itself doesn't contain secrets (the token is only sourced at runtime from the shell profile).

✅ This is actually correct. No changes needed.


📝 Settings UI Review

6. Good Progressive Disclosure (Positive)

File: src/settings/SettingsTab.ts:220-312

The collapsible <details> element is excellent UX. Shows setup instructions only when needed.

7. Button State Management (Minor Issue)

File: src/settings/SettingsTab.ts:254-260

setupBtn.disabled = true;
setupBtn.textContent = "Setting up...";
const result = await setupClaudeOAuthLaunchAgent();
new Notice(result.message);
this.display(); // Re-render.

Issue: If setupClaudeOAuthLaunchAgent throws an exception (not just returns success: false), the button stays disabled and shows "Setting up..." forever.

Fix: Add try/catch or re-enable button in finally block:

try {
  setupBtn.disabled = true;
  setupBtn.textContent = "Setting up...";
  const result = await setupClaudeOAuthLaunchAgent();
  new Notice(result.message);
} finally {
  this.display(); // Re-render regardless
}

Fix button state management →


🧪 Test Coverage Review

8. Tests Are Too Shallow (Medium Priority)

File: tests/unit/utils/launchAgentHelper.test.ts

Issues:

  • Tests only cover the simple getter functions (getLaunchAgentPath, isMacOS, getManualSetupCommand)
  • No tests for the critical functions: setupClaudeOAuthLaunchAgent and removeClaudeOAuthLaunchAgent
  • No validation of the generated plist XML content
  • No tests for error cases (permission denied, disk full, etc.)

Recommendations:

  1. Mock fs and execSync to test the setup/remove functions
  2. Test that the generated plist content is valid XML
  3. Test error handling paths
  4. Test that the plist contains the correct command

Example test to add:

describe('setupClaudeOAuthLaunchAgent', () => {
  it('should generate valid plist XML with correct command', async () => {
    // Mock fs.writeFileSync to capture the plist content
    // Parse XML and verify structure
    // Ensure command includes proper shell sourcing
  });
});

Add comprehensive tests →


📚 Documentation Review

9. Excellent Planning Document (Positive)

File: docs/agent_plans/fix-claude-max-subscription.md

This is a great example of thorough problem analysis and solution design. Well done!

10. Missing User-Facing Documentation (Low Priority)

Consider adding:

  • A troubleshooting section in the README for users who still can't get it working
  • Documentation about how to verify the token is set (launchctl getenv CLAUDE_CODE_OAUTH_TOKEN)
  • Instructions for removing the LaunchAgent if needed

🎨 CSS Review

File: styles.css:950-1016

✅ CSS looks good. Properly scoped class names, uses Obsidian's CSS variables for theming. No issues found.


📊 Summary

Category Count
🔴 High Priority 1
🟡 Medium Priority 3
🔵 Low Priority 3
✅ Positive Notes 3

Recommended Actions Before Merge

Must Fix:

  1. Shell injection risk in plist command - Actually, document the trust assumption. If an attacker can write to ~/.zshrc, they can already execute arbitrary code at next shell startup. This is acceptable.
  2. Add error handling for button state in settings UI
  3. Improve error logging in LaunchAgent setup (don't silently swallow all errors)

Should Fix:
4. Improve test coverage for the setup/remove functions
5. Fix the shell sourcing logic (both profiles should be tried unconditionally)
6. Simplify the manual command (remove unnecessary echo)

Nice to Have:
7. Add user-facing troubleshooting docs


Overall Assessment

Verdict: ✅ Approve with minor changes

This PR solves a real problem with the correct macOS-native approach. The code is well-structured and mostly well-tested. The security concerns are minimal (mostly edge cases), and the UX is thoughtful.

The main areas for improvement are:

  • Better error handling (don't silently ignore all errors)
  • More comprehensive tests
  • Button state management in the UI

Great work on this feature! 🎉


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.

Claude Code Max subscription not working

1 participant

Comments